Coverage for lobster/common/parser.py: 82%
115 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-05-12 15:02 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-05-12 15:02 +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 f"expected {kind}, found EOF")
64 elif value is None:
65 self.error(self.nt.loc,
66 f"expected {kind}, found {self.nt.kind} {self.nt.value()}")
67 else:
68 self.error(self.nt.loc,
69 f"expected {value}, found {self.nt.value()}")
71 def warning(self, loc, message):
72 self.lexer.mh.warning(loc, message)
74 def error(self, loc, message):
75 self.lexer.mh.error(loc, message)
77 def parse(self):
78 while self.nt:
79 if self.peek("KEYWORD", "requirements") or \ 79 ↛ 84line 79 didn't jump to line 84 because the condition on line 79 was always true
80 self.peek("KEYWORD", "implementation") or \
81 self.peek("KEYWORD", "activity"):
82 self.parse_level_declaration()
83 else:
84 self.error(self.nt.loc,
85 "expected: requirements|implementation|activity,"
86 f" found {self.nt.value()} instead")
88 return self.levels
90 def parse_level_declaration(self):
91 self.match("KEYWORD")
92 level_kind = self.ct.value()
94 self.match("STRING")
95 level_name = self.ct.value()
96 if level_name in self.levels: 96 ↛ 97line 96 didn't jump to line 97 because the condition on line 96 was never true
97 self.error(self.ct.loc,
98 "duplicate declaration")
100 item = LevelDefinition(
101 name=level_name,
102 kind=level_kind,
103 )
104 self.levels[level_name] = item
106 self.match("C_BRA")
108 while not self.peek("C_KET"):
109 if self.peek("KEYWORD", "source"):
110 self.advance()
111 self.match("COLON")
112 self.match("STRING")
113 source_info = {
114 "file" : self.ct.value(),
115 }
116 if not os.path.isfile(source_info["file"]):
117 self.error(self.ct.loc,
118 f"cannot find file {source_info['file']}")
119 item.source.append(source_info)
121 if self.peek("KEYWORD", "with"): 121 ↛ 122line 121 didn't jump to line 122 because the condition on line 121 was never true
122 self.match("KEYWORD", "with")
124 self.match("SEMI")
126 elif self.peek("KEYWORD", "trace"):
127 self.match("KEYWORD", "trace")
128 self.match("KEYWORD", "to")
129 self.match("COLON")
130 self.match("STRING")
131 if self.ct.value() == level_name: 131 ↛ 132line 131 didn't jump to line 132 because the condition on line 131 was never true
132 self.error(self.ct.loc,
133 "cannot trace to yourself")
134 elif self.ct.value() not in self.levels: 134 ↛ 135line 134 didn't jump to line 135 because the condition on line 134 was never true
135 self.error(self.ct.loc,
136 f"unknown item {self.ct.value()}")
137 else:
138 self.levels[self.ct.value()].needs_tracing_down = True
139 item.traces.append(self.ct.value())
140 item.needs_tracing_up = True
142 self.match("SEMI")
144 elif self.peek("KEYWORD", "requires"): 144 ↛ 163line 144 didn't jump to line 163 because the condition on line 144 was always true
145 self.match("KEYWORD", "requires")
146 self.match("COLON")
148 req_list = []
150 self.match("STRING")
151 req_list.append(self.ct)
153 while self.peek("KEYWORD", "or"):
154 self.match("KEYWORD", "or")
155 self.match("STRING")
156 req_list.append(self.ct)
158 self.match("SEMI")
160 item.raw_trace_requirements.append(req_list)
162 else:
163 self.error(self.nt.loc,
164 f"unexpected directive {self.nt.value()}")
166 self.match("C_KET")
169def load(mh, file_name):
170 parser = Parser(mh, file_name)
171 ast = parser.parse()
173 # Resolve requires links now
174 for item in ast.values():
175 item.breakdown_requirements = []
176 if item.raw_trace_requirements:
177 for chain in item.raw_trace_requirements:
178 new_chain = []
179 for tok in chain:
180 if tok.value() not in ast: 180 ↛ 181line 180 didn't jump to line 181 because the condition on line 180 was never true
181 mh.error(tok.loc, f"unknown level {tok.value()}")
182 if item.name not in ast[tok.value()].traces: 182 ↛ 183line 182 didn't jump to line 183 because the condition on line 182 was never true
183 mh.error(tok.loc,
184 f"{tok.value()} cannot trace to {item.name} items")
185 new_chain.append(tok.value())
186 item.breakdown_requirements.append(new_chain)
187 else:
188 for src in ast.values():
189 if item.name in src.traces:
190 item.breakdown_requirements.append([src.name])
191 item.raw_trace_requirements.clear()
193 return ast
196def sanity_test():
197 mh = errors.Message_Handler()
199 try:
200 config = load(mh, sys.argv[1])
201 print(config)
202 except errors.LOBSTER_Error:
203 return 1
204 return 0
207if __name__ == "__main__":
208 sanity_test()