Coverage for lobster/config/parser.py: 61%
114 statements
« prev ^ index » next coverage.py v7.10.2, created at 2025-08-06 09:51 +0000
« prev ^ index » next coverage.py v7.10.2, created at 2025-08-06 09:51 +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 if not os.path.isfile(file_name): 31 ↛ 32line 31 didn't jump to line 32 because the condition on line 31 was never true
32 raise FileNotFoundError(f"Config file not found: {file_name}")
34 self.lexer = lexer.Lexer(mh, file_name)
36 self.ct = None
37 self.nt = self.lexer.token()
39 self.levels = collections.OrderedDict()
41 def advance(self):
42 self.ct = self.nt
43 self.nt = self.lexer.token()
45 def peek(self, kind, value=None):
46 if self.nt is None: 46 ↛ 47line 46 didn't jump to line 47 because the condition on line 46 was never true
47 return kind is None
48 elif kind is None: 48 ↛ 49line 48 didn't jump to line 49 because the condition on line 48 was never true
49 return False
50 elif self.nt.kind == kind:
51 if value is None:
52 return True
53 else:
54 return self.nt.value() == value
55 else:
56 return False
58 def match(self, kind, value=None):
59 if self.peek(kind, value): 59 ↛ 61line 59 didn't jump to line 61 because the condition on line 59 was always true
60 self.advance()
61 elif self.nt is None:
62 self.error(
63 location.File_Reference(filename = self.lexer.file_name),
64 "expected %s, found EOF" % kind)
65 elif value is None:
66 self.error(self.nt.loc,
67 "expected %s, found %s %s" % (kind,
68 self.nt.kind,
69 self.nt.value()))
70 else:
71 self.error(self.nt.loc,
72 "expected %s, found %s" % (value, self.nt.value()))
74 def warning(self, loc, message):
75 self.lexer.mh.warning(loc, message)
77 def error(self, loc, message):
78 self.lexer.mh.error(loc, message)
80 def parse(self):
81 while self.nt:
82 if self.peek("KEYWORD", "requirements") or \ 82 ↛ 87line 82 didn't jump to line 87 because the condition on line 82 was always true
83 self.peek("KEYWORD", "implementation") or \
84 self.peek("KEYWORD", "activity"):
85 self.parse_level_declaration()
86 else:
87 self.error(self.nt.loc,
88 "expected: requirements|implementation|activity,"
89 " found %s instead" % self.nt.value())
91 return self.levels
93 def parse_level_declaration(self):
94 self.match("KEYWORD")
95 level_kind = self.ct.value()
97 self.match("STRING")
98 level_name = self.ct.value()
99 if level_name in self.levels: 99 ↛ 100line 99 didn't jump to line 100 because the condition on line 99 was never true
100 self.error(self.ct.loc,
101 "duplicate declaration")
103 item = {
104 "name" : level_name,
105 "kind" : level_kind,
106 "traces" : [],
107 "source" : [],
108 "needs_tracing_up" : False,
109 "needs_tracing_down" : False,
110 "raw_trace_requirements" : []
111 }
112 self.levels[level_name] = item
114 self.match("C_BRA")
116 while not self.peek("C_KET"):
117 if self.peek("KEYWORD", "source"):
118 self.advance()
119 self.match("COLON")
120 self.match("STRING")
121 source_info = {
122 "file" : self.ct.value(),
123 }
124 if not os.path.isfile(source_info["file"]): 124 ↛ 125line 124 didn't jump to line 125 because the condition on line 124 was never true
125 self.error(self.ct.loc,
126 "cannot find file %s" % source_info["file"])
127 item["source"].append(source_info)
129 if self.peek("KEYWORD", "with"): 129 ↛ 130line 129 didn't jump to line 130 because the condition on line 129 was never true
130 self.match("KEYWORD", "with")
132 self.match("SEMI")
134 elif self.peek("KEYWORD", "trace"): 134 ↛ 152line 134 didn't jump to line 152 because the condition on line 134 was always true
135 self.match("KEYWORD", "trace")
136 self.match("KEYWORD", "to")
137 self.match("COLON")
138 self.match("STRING")
139 if self.ct.value() == level_name: 139 ↛ 140line 139 didn't jump to line 140 because the condition on line 139 was never true
140 self.error(self.ct.loc,
141 "cannot trace to yourself")
142 elif self.ct.value() not in self.levels: 142 ↛ 143line 142 didn't jump to line 143 because the condition on line 142 was never true
143 self.error(self.ct.loc,
144 "unknown item %s" % self.ct.value())
145 else:
146 self.levels[self.ct.value()]["needs_tracing_down"] = True
147 item["traces"].append(self.ct.value())
148 item["needs_tracing_up"] = True
150 self.match("SEMI")
152 elif self.peek("KEYWORD", "requires"):
153 self.match("KEYWORD", "requires")
154 self.match("COLON")
156 req_list = []
158 self.match("STRING")
159 req_list.append(self.ct)
161 while self.peek("KEYWORD", "or"):
162 self.match("KEYWORD", "or")
163 self.match("STRING")
164 req_list.append(self.ct)
166 self.match("SEMI")
168 item["raw_trace_requirements"].append(req_list)
170 else:
171 self.error(self.nt.loc,
172 "unexpected directive %s" % self.nt.value())
174 self.match("C_KET")
177def load(mh, file_name):
178 parser = Parser(mh, file_name)
179 ast = parser.parse()
181 # Resolve requires links now
182 for item in ast.values():
183 item["breakdown_requirements"] = []
184 if len(item["raw_trace_requirements"]) > 0: 184 ↛ 185line 184 didn't jump to line 185 because the condition on line 184 was never true
185 for chain in item["raw_trace_requirements"]:
186 new_chain = []
187 for tok in chain:
188 if tok.value() not in ast:
189 mh.error(tok.loc, "unknown level %s" % tok.value())
190 if item["name"] not in ast[tok.value()]["traces"]:
191 mh.error(tok.loc,
192 "%s cannot trace to %s items" %
193 (tok.value(),
194 item["name"]))
195 new_chain.append(tok.value())
196 item["breakdown_requirements"].append(new_chain)
197 else:
198 for src in ast.values():
199 if item["name"] in src["traces"]:
200 item["breakdown_requirements"].append([src["name"]])
201 del item["raw_trace_requirements"]
203 return ast
206def sanity_test():
207 mh = errors.Message_Handler()
209 try:
210 config = load(mh, sys.argv[1])
211 print(config)
212 except errors.LOBSTER_Error:
213 return 1
214 return 0
217if __name__ == "__main__":
218 sanity_test()