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

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/>. 

19 

20import sys 

21import os.path 

22 

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 

30 

31 

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}") 

36 

37 self.lexer = lexer.Lexer(mh, file_name) 

38 

39 self.ct = None 

40 self.nt = self.lexer.token() 

41 

42 def advance(self): 

43 self.ct = self.nt 

44 self.nt = self.lexer.token() 

45 

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 

56 

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()}") 

70 

71 def warning(self, loc, message): 

72 self.lexer.mh.warning(loc, message) 

73 

74 def error(self, loc, message): 

75 self.lexer.mh.error(loc, message) 

76 

77 def parse(self): 

78 levels = [] 

79 

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") 

89 

90 return RawPolicy(levels=levels) 

91 

92 def parse_level_declaration(self): 

93 self.match("KEYWORD") 

94 level_kind = self.ct.value() 

95 

96 self.match("STRING") 

97 level_name = self.ct.value() 

98 level = RawLevel(name=level_name, name_loc=self.ct.loc, kind=level_kind) 

99 

100 self.match("C_BRA") 

101 

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)) 

108 

109 if self.peek("KEYWORD", "with"): 

110 self.match("KEYWORD", "with") 

111 

112 self.match("SEMI") 

113 

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)) 

121 

122 self.match("SEMI") 

123 

124 elif self.peek("KEYWORD", "requires"): 

125 self.match("KEYWORD", "requires") 

126 self.match("COLON") 

127 

128 req_list = [] 

129 

130 self.match("STRING") 

131 req_list.append( 

132 RawRequiresCandidate(name=self.ct.value(), loc=self.ct.loc)) 

133 

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)) 

139 

140 self.match("SEMI") 

141 

142 level.requires.append(req_list) 

143 

144 else: 

145 self.error(self.nt.loc, 

146 f"unexpected directive {self.nt.value()}") 

147 

148 self.match("C_KET") 

149 

150 return level 

151 

152 

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() 

158 

159 # Note: build_tracing_policy raises exceptions instead of using the Message_Handler. 

160 return build_tracing_policy(raw_policy) 

161 

162 

163def sanity_test(): 

164 mh = errors.Message_Handler() 

165 

166 try: 

167 config = load(mh, sys.argv[1]) 

168 print(config) 

169 except errors.LOBSTER_Error: 

170 return 1 

171 return 0 

172 

173 

174if __name__ == "__main__": 

175 sanity_test()