Coverage for lobster/common/parser.py: 82%
115 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-04-16 05:31 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-04-16 05:31 +0000
1#!/usr/bin/env python3
2#
3# LOBSTER - Lightweight Open BMW Software Traceability Evidence Report
4# Copyright (C) 2022-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
5#
6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU Affero General Public License as
8# published by the Free Software Foundation, either version 3 of the
9# License, or (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful, but
12# WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14# Affero General Public License for more details.
15#
16# You should have received a copy of the GNU Affero General Public
17# License along with this program. If not, see
18# <https://www.gnu.org/licenses/>.
20import sys
21import os.path
22import collections
24from lobster.common import lexer
25from lobster.common.level_definition import LevelDefinition
26from lobster.common import errors
27from lobster.common import location
30class Parser:
31 def __init__(self, mh, file_name):
32 if not os.path.isfile(file_name):
33 raise FileNotFoundError(f"Config file not found: {file_name}")
35 self.lexer = lexer.Lexer(mh, file_name)
37 self.ct = None
38 self.nt = self.lexer.token()
40 self.levels = collections.OrderedDict()
42 def advance(self):
43 self.ct = self.nt
44 self.nt = self.lexer.token()
46 def peek(self, kind, value=None):
47 if self.nt is None: 47 ↛ 48line 47 didn't jump to line 48 because the condition on line 47 was never true
48 return kind is None
49 if kind is None: 49 ↛ 50line 49 didn't jump to line 50 because the condition on line 49 was never true
50 return False
51 if self.nt.kind == kind:
52 if value is None:
53 return True
54 return self.nt.value() == value
55 return False
57 def match(self, kind, value=None):
58 if self.peek(kind, value): 58 ↛ 60line 58 didn't jump to line 60 because the condition on line 58 was always true
59 self.advance()
60 elif self.nt is None:
61 self.error(
62 location.File_Reference(filename = self.lexer.file_name),
63 "expected %s, found EOF" % kind)
64 elif value is None:
65 self.error(self.nt.loc,
66 "expected %s, found %s %s" % (kind,
67 self.nt.kind,
68 self.nt.value()))
69 else:
70 self.error(self.nt.loc,
71 "expected %s, found %s" % (value, self.nt.value()))
73 def warning(self, loc, message):
74 self.lexer.mh.warning(loc, message)
76 def error(self, loc, message):
77 self.lexer.mh.error(loc, message)
79 def parse(self):
80 while self.nt:
81 if self.peek("KEYWORD", "requirements") or \ 81 ↛ 86line 81 didn't jump to line 86 because the condition on line 81 was always true
82 self.peek("KEYWORD", "implementation") or \
83 self.peek("KEYWORD", "activity"):
84 self.parse_level_declaration()
85 else:
86 self.error(self.nt.loc,
87 "expected: requirements|implementation|activity,"
88 " found %s instead" % self.nt.value())
90 return self.levels
92 def parse_level_declaration(self):
93 self.match("KEYWORD")
94 level_kind = self.ct.value()
96 self.match("STRING")
97 level_name = self.ct.value()
98 if level_name in self.levels: 98 ↛ 99line 98 didn't jump to line 99 because the condition on line 98 was never true
99 self.error(self.ct.loc,
100 "duplicate declaration")
102 item = LevelDefinition(
103 name=level_name,
104 kind=level_kind,
105 )
106 self.levels[level_name] = item
108 self.match("C_BRA")
110 while not self.peek("C_KET"):
111 if self.peek("KEYWORD", "source"):
112 self.advance()
113 self.match("COLON")
114 self.match("STRING")
115 source_info = {
116 "file" : self.ct.value(),
117 }
118 if not os.path.isfile(source_info["file"]):
119 self.error(self.ct.loc,
120 "cannot find file %s" % source_info["file"])
121 item.source.append(source_info)
123 if self.peek("KEYWORD", "with"): 123 ↛ 124line 123 didn't jump to line 124 because the condition on line 123 was never true
124 self.match("KEYWORD", "with")
126 self.match("SEMI")
128 elif self.peek("KEYWORD", "trace"):
129 self.match("KEYWORD", "trace")
130 self.match("KEYWORD", "to")
131 self.match("COLON")
132 self.match("STRING")
133 if self.ct.value() == level_name: 133 ↛ 134line 133 didn't jump to line 134 because the condition on line 133 was never true
134 self.error(self.ct.loc,
135 "cannot trace to yourself")
136 elif self.ct.value() not in self.levels: 136 ↛ 137line 136 didn't jump to line 137 because the condition on line 136 was never true
137 self.error(self.ct.loc,
138 "unknown item %s" % self.ct.value())
139 else:
140 self.levels[self.ct.value()].needs_tracing_down = True
141 item.traces.append(self.ct.value())
142 item.needs_tracing_up = True
144 self.match("SEMI")
146 elif self.peek("KEYWORD", "requires"): 146 ↛ 165line 146 didn't jump to line 165 because the condition on line 146 was always true
147 self.match("KEYWORD", "requires")
148 self.match("COLON")
150 req_list = []
152 self.match("STRING")
153 req_list.append(self.ct)
155 while self.peek("KEYWORD", "or"):
156 self.match("KEYWORD", "or")
157 self.match("STRING")
158 req_list.append(self.ct)
160 self.match("SEMI")
162 item.raw_trace_requirements.append(req_list)
164 else:
165 self.error(self.nt.loc,
166 "unexpected directive %s" % self.nt.value())
168 self.match("C_KET")
171def load(mh, file_name):
172 parser = Parser(mh, file_name)
173 ast = parser.parse()
175 # Resolve requires links now
176 for item in ast.values():
177 item.breakdown_requirements = []
178 if item.raw_trace_requirements:
179 for chain in item.raw_trace_requirements:
180 new_chain = []
181 for tok in chain:
182 if tok.value() not in ast: 182 ↛ 183line 182 didn't jump to line 183 because the condition on line 182 was never true
183 mh.error(tok.loc, "unknown level %s" % tok.value())
184 if item.name not in ast[tok.value()].traces: 184 ↛ 185line 184 didn't jump to line 185 because the condition on line 184 was never true
185 mh.error(tok.loc,
186 "%s cannot trace to %s items" %
187 (tok.value(),
188 item.name))
189 new_chain.append(tok.value())
190 item.breakdown_requirements.append(new_chain)
191 else:
192 for src in ast.values():
193 if item.name in src.traces:
194 item.breakdown_requirements.append([src.name])
195 item.raw_trace_requirements.clear()
197 return ast
200def sanity_test():
201 mh = errors.Message_Handler()
203 try:
204 config = load(mh, sys.argv[1])
205 print(config)
206 except errors.LOBSTER_Error:
207 return 1
208 return 0
211if __name__ == "__main__":
212 sanity_test()