Coverage for lobster/common/parser.py: 14%
88 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-09-22 04:43 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-09-22 04:43 +0000
1#!/usr/bin/env python3
2#
3# LOBSTER - Lightweight Open BMW Software Traceability Evidence Report
4# Copyright (C) 2022-2026 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
23from lobster.common import lexer
24from lobster.common import errors
25from lobster.common import location
26from lobster.common.raw_policy import (
27 RawLevel, RawPolicy, RawRequiresCandidate, RawSource, RawTraceTo,
28)
29from lobster.common.policy_builder import build_tracing_policy
32class Parser:
33 def __init__(self, mh, file_name):
34 if not os.path.isfile(file_name):
35 raise FileNotFoundError(f"Config file not found: {file_name}")
37 self.lexer = lexer.Lexer(mh, file_name)
39 self.ct = None
40 self.nt = self.lexer.token()
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:
48 return kind is None
49 if kind is None:
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):
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 levels = []
80 while self.nt:
81 if self.peek("KEYWORD", "requirements") or \
82 self.peek("KEYWORD", "implementation") or \
83 self.peek("KEYWORD", "activity"):
84 levels.append(self.parse_level_declaration())
85 else:
86 self.error(self.nt.loc,
87 "expected: requirements|implementation|activity,"
88 f" found {self.nt.value()} instead")
90 return RawPolicy(levels=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 level = RawLevel(name=level_name, name_loc=self.ct.loc, kind=level_kind)
100 self.match("C_BRA")
102 while not self.peek("C_KET"):
103 if self.peek("KEYWORD", "source"):
104 self.advance()
105 self.match("COLON")
106 self.match("STRING")
107 level.source.append(RawSource(file=self.ct.value(), loc=self.ct.loc))
109 if self.peek("KEYWORD", "with"):
110 self.match("KEYWORD", "with")
112 self.match("SEMI")
114 elif self.peek("KEYWORD", "trace"):
115 self.match("KEYWORD", "trace")
116 self.match("KEYWORD", "to")
117 self.match("COLON")
118 self.match("STRING")
119 level.trace_to.append(
120 RawTraceTo(target=self.ct.value(), loc=self.ct.loc))
122 self.match("SEMI")
124 elif self.peek("KEYWORD", "requires"):
125 self.match("KEYWORD", "requires")
126 self.match("COLON")
128 req_list = []
130 self.match("STRING")
131 req_list.append(
132 RawRequiresCandidate(name=self.ct.value(), loc=self.ct.loc))
134 while self.peek("KEYWORD", "or"):
135 self.match("KEYWORD", "or")
136 self.match("STRING")
137 req_list.append(
138 RawRequiresCandidate(name=self.ct.value(), loc=self.ct.loc))
140 self.match("SEMI")
142 level.requires.append(req_list)
144 else:
145 self.error(self.nt.loc,
146 f"unexpected directive {self.nt.value()}")
148 self.match("C_KET")
150 return level
153def load(mh, file_name):
154 # Note: "Parser" uses a legacy interface that still requires a Message_Handler
155 # instance instead of raising exceptions.
156 parser = Parser(mh, file_name)
157 raw_policy = parser.parse()
159 # Note: build_tracing_policy raises exceptions instead of using the Message_Handler.
160 return build_tracing_policy(raw_policy)
163def sanity_test():
164 mh = errors.Message_Handler()
166 try:
167 config = load(mh, sys.argv[1])
168 print(config)
169 except errors.LOBSTER_Error:
170 return 1
171 return 0
174if __name__ == "__main__":
175 sanity_test()