Coverage for lobster/config/parser.py: 52%
139 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-26 14:55 +0000
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-26 14:55 +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.config import lexer
25from lobster import errors
26from lobster import location
29class Parser:
30 def __init__(self, mh, file_name):
31 assert isinstance(mh, errors.Message_Handler)
32 assert isinstance(file_name, str)
33 assert os.path.isfile(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 = {
105 "name" : level_name,
106 "kind" : level_kind,
107 "traces" : [],
108 "source" : [],
109 "needs_tracing_up" : False,
110 "needs_tracing_down" : False,
111 "raw_trace_requirements" : []
112 }
113 self.levels[level_name] = item
115 self.match("C_BRA")
117 while not self.peek("C_KET"):
118 if self.peek("KEYWORD", "source"):
119 self.advance()
120 self.match("COLON")
121 self.match("STRING")
122 source_info = {
123 "file" : self.ct.value(),
124 "filters" : [],
125 }
126 if level_kind == "requirements":
127 source_info["valid_status"] = []
128 if not os.path.isfile(source_info["file"]): 128 ↛ 129line 128 didn't jump to line 129 because the condition on line 128 was never true
129 self.error(self.ct.loc,
130 "cannot find file %s" % source_info["file"])
131 item["source"].append(source_info)
133 if self.peek("KEYWORD", "with"): 133 ↛ 134line 133 didn't jump to line 134 because the condition on line 133 was never true
134 self.match("KEYWORD", "with")
136 while not self.peek("SEMI"):
137 self.match("KEYWORD")
138 if self.ct.value() == "prefix":
139 self.match("STRING")
140 source_info["filters"].append(("prefix",
141 self.ct.value()))
143 elif self.ct.value() == "kind":
144 self.match("STRING")
145 source_info["filters"].append(("kind",
146 self.ct.value()))
148 elif self.ct.value() == "valid_status":
149 if level_kind != "requirements":
150 self.error(self.ct.loc,
151 "property valid_status is only "
152 "applicable for requirements")
153 self.match("C_BRA")
154 while True:
155 self.match("STRING")
156 value = self.ct.value()
157 if value in source_info["valid_status"]:
158 self.warning(self.ct.loc,
159 "duplicate status %s" %
160 value)
161 else:
162 source_info["valid_status"].append(value)
163 if self.peek("COMMA"):
164 self.match("COMMA")
165 else:
166 break
167 self.match("C_KET")
169 else:
170 self.error(self.ct.loc,
171 "unknown property '%s'" %
172 self.ct.value())
174 self.match("SEMI")
176 elif self.peek("KEYWORD", "trace"): 176 ↛ 194line 176 didn't jump to line 194 because the condition on line 176 was always true
177 self.match("KEYWORD", "trace")
178 self.match("KEYWORD", "to")
179 self.match("COLON")
180 self.match("STRING")
181 if self.ct.value() == level_name: 181 ↛ 182line 181 didn't jump to line 182 because the condition on line 181 was never true
182 self.error(self.ct.loc,
183 "cannot trace to yourself")
184 elif self.ct.value() not in self.levels: 184 ↛ 185line 184 didn't jump to line 185 because the condition on line 184 was never true
185 self.error(self.ct.loc,
186 "unknown item %s" % self.ct.value())
187 else:
188 self.levels[self.ct.value()]["needs_tracing_down"] = True
189 item["traces"].append(self.ct.value())
190 item["needs_tracing_up"] = True
192 self.match("SEMI")
194 elif self.peek("KEYWORD", "requires"):
195 self.match("KEYWORD", "requires")
196 self.match("COLON")
198 req_list = []
200 self.match("STRING")
201 req_list.append(self.ct)
203 while self.peek("KEYWORD", "or"):
204 self.match("KEYWORD", "or")
205 self.match("STRING")
206 req_list.append(self.ct)
208 self.match("SEMI")
210 item["raw_trace_requirements"].append(req_list)
212 else:
213 self.error(self.nt.loc,
214 "unexpected directive %s" % self.nt.value())
216 self.match("C_KET")
219def load(mh, file_name):
220 parser = Parser(mh, file_name)
221 ast = parser.parse()
223 # Resolve requires links now
224 for item in ast.values():
225 item["breakdown_requirements"] = []
226 if len(item["raw_trace_requirements"]) > 0: 226 ↛ 227line 226 didn't jump to line 227 because the condition on line 226 was never true
227 for chain in item["raw_trace_requirements"]:
228 new_chain = []
229 for tok in chain:
230 if tok.value() not in ast:
231 mh.error(tok.loc, "unknown level %s" % tok.value())
232 if item["name"] not in ast[tok.value()]["traces"]:
233 mh.error(tok.loc,
234 "%s cannot trace to %s items" %
235 (tok.value(),
236 item["name"]))
237 new_chain.append(tok.value())
238 item["breakdown_requirements"].append(new_chain)
239 else:
240 for src in ast.values():
241 if item["name"] in src["traces"]:
242 item["breakdown_requirements"].append([src["name"]])
243 del item["raw_trace_requirements"]
245 return ast
248def sanity_test():
249 mh = errors.Message_Handler()
251 try:
252 config = load(mh, sys.argv[1])
253 print(config)
254 except errors.LOBSTER_Error:
255 return 1
256 return 0
259if __name__ == "__main__":
260 sanity_test()