Coverage for lobster/common/parser.py: 63%
115 statements
« prev ^ index » next coverage.py v7.10.5, created at 2025-08-27 13:02 +0000
« prev ^ index » next coverage.py v7.10.5, created at 2025-08-27 13: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 elif 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 elif self.nt.kind == kind:
52 if value is None:
53 return True
54 else:
55 return self.nt.value() == value
56 else:
57 return False
59 def match(self, kind, value=None):
60 if self.peek(kind, value): 60 ↛ 62line 60 didn't jump to line 62 because the condition on line 60 was always true
61 self.advance()
62 elif self.nt is None:
63 self.error(
64 location.File_Reference(filename = self.lexer.file_name),
65 "expected %s, found EOF" % kind)
66 elif value is None:
67 self.error(self.nt.loc,
68 "expected %s, found %s %s" % (kind,
69 self.nt.kind,
70 self.nt.value()))
71 else:
72 self.error(self.nt.loc,
73 "expected %s, found %s" % (value, self.nt.value()))
75 def warning(self, loc, message):
76 self.lexer.mh.warning(loc, message)
78 def error(self, loc, message):
79 self.lexer.mh.error(loc, message)
81 def parse(self):
82 while self.nt:
83 if self.peek("KEYWORD", "requirements") or \ 83 ↛ 88line 83 didn't jump to line 88 because the condition on line 83 was always true
84 self.peek("KEYWORD", "implementation") or \
85 self.peek("KEYWORD", "activity"):
86 self.parse_level_declaration()
87 else:
88 self.error(self.nt.loc,
89 "expected: requirements|implementation|activity,"
90 " found %s instead" % self.nt.value())
92 return self.levels
94 def parse_level_declaration(self):
95 self.match("KEYWORD")
96 level_kind = self.ct.value()
98 self.match("STRING")
99 level_name = self.ct.value()
100 if level_name in self.levels: 100 ↛ 101line 100 didn't jump to line 101 because the condition on line 100 was never true
101 self.error(self.ct.loc,
102 "duplicate declaration")
104 item = LevelDefinition(
105 name=level_name,
106 kind=level_kind,
107 )
108 self.levels[level_name] = item
110 self.match("C_BRA")
112 while not self.peek("C_KET"):
113 if self.peek("KEYWORD", "source"):
114 self.advance()
115 self.match("COLON")
116 self.match("STRING")
117 source_info = {
118 "file" : self.ct.value(),
119 }
120 if not os.path.isfile(source_info["file"]): 120 ↛ 121line 120 didn't jump to line 121 because the condition on line 120 was never true
121 self.error(self.ct.loc,
122 "cannot find file %s" % source_info["file"])
123 item.source.append(source_info)
125 if self.peek("KEYWORD", "with"): 125 ↛ 126line 125 didn't jump to line 126 because the condition on line 125 was never true
126 self.match("KEYWORD", "with")
128 self.match("SEMI")
130 elif self.peek("KEYWORD", "trace"): 130 ↛ 148line 130 didn't jump to line 148 because the condition on line 130 was always true
131 self.match("KEYWORD", "trace")
132 self.match("KEYWORD", "to")
133 self.match("COLON")
134 self.match("STRING")
135 if self.ct.value() == level_name: 135 ↛ 136line 135 didn't jump to line 136 because the condition on line 135 was never true
136 self.error(self.ct.loc,
137 "cannot trace to yourself")
138 elif self.ct.value() not in self.levels: 138 ↛ 139line 138 didn't jump to line 139 because the condition on line 138 was never true
139 self.error(self.ct.loc,
140 "unknown item %s" % self.ct.value())
141 else:
142 self.levels[self.ct.value()].needs_tracing_down = True
143 item.traces.append(self.ct.value())
144 item.needs_tracing_up = True
146 self.match("SEMI")
148 elif self.peek("KEYWORD", "requires"):
149 self.match("KEYWORD", "requires")
150 self.match("COLON")
152 req_list = []
154 self.match("STRING")
155 req_list.append(self.ct)
157 while self.peek("KEYWORD", "or"):
158 self.match("KEYWORD", "or")
159 self.match("STRING")
160 req_list.append(self.ct)
162 self.match("SEMI")
164 item.raw_trace_requirements.append(req_list)
166 else:
167 self.error(self.nt.loc,
168 "unexpected directive %s" % self.nt.value())
170 self.match("C_KET")
173def load(mh, file_name):
174 parser = Parser(mh, file_name)
175 ast = parser.parse()
177 # Resolve requires links now
178 for item in ast.values():
179 item.breakdown_requirements = []
180 if item.raw_trace_requirements: 180 ↛ 181line 180 didn't jump to line 181 because the condition on line 180 was never true
181 for chain in item.raw_trace_requirements:
182 new_chain = []
183 for tok in chain:
184 if tok.value() not in ast:
185 mh.error(tok.loc, "unknown level %s" % tok.value())
186 if item.name not in ast[tok.value()].traces:
187 mh.error(tok.loc,
188 "%s cannot trace to %s items" %
189 (tok.value(),
190 item.name))
191 new_chain.append(tok.value())
192 item.breakdown_requirements.append(new_chain)
193 else:
194 for src in ast.values():
195 if item.name in src.traces:
196 item.breakdown_requirements.append([src.name])
197 item.raw_trace_requirements.clear()
199 return ast
202def sanity_test():
203 mh = errors.Message_Handler()
205 try:
206 config = load(mh, sys.argv[1])
207 print(config)
208 except errors.LOBSTER_Error:
209 return 1
210 return 0
213if __name__ == "__main__":
214 sanity_test()