Coverage for lobster/common/parser.py: 82%

115 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2026-05-12 15: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/>. 

19 

20import sys 

21import os.path 

22import collections 

23 

24from lobster.common import lexer 

25from lobster.common.level_definition import LevelDefinition 

26from lobster.common import errors 

27from lobster.common import location 

28 

29 

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

34 

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

36 

37 self.ct = None 

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

39 

40 self.levels = collections.OrderedDict() 

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: 47 ↛ 48line 47 didn't jump to line 48 because the condition on line 47 was never true

48 return kind is None 

49 if 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 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): 58 ↛ 60line 58 didn't jump to line 60 because the condition on line 58 was always true

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 while self.nt: 

79 if self.peek("KEYWORD", "requirements") or \ 79 ↛ 84line 79 didn't jump to line 84 because the condition on line 79 was always true

80 self.peek("KEYWORD", "implementation") or \ 

81 self.peek("KEYWORD", "activity"): 

82 self.parse_level_declaration() 

83 else: 

84 self.error(self.nt.loc, 

85 "expected: requirements|implementation|activity," 

86 f" found {self.nt.value()} instead") 

87 

88 return self.levels 

89 

90 def parse_level_declaration(self): 

91 self.match("KEYWORD") 

92 level_kind = self.ct.value() 

93 

94 self.match("STRING") 

95 level_name = self.ct.value() 

96 if level_name in self.levels: 96 ↛ 97line 96 didn't jump to line 97 because the condition on line 96 was never true

97 self.error(self.ct.loc, 

98 "duplicate declaration") 

99 

100 item = LevelDefinition( 

101 name=level_name, 

102 kind=level_kind, 

103 ) 

104 self.levels[level_name] = item 

105 

106 self.match("C_BRA") 

107 

108 while not self.peek("C_KET"): 

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

110 self.advance() 

111 self.match("COLON") 

112 self.match("STRING") 

113 source_info = { 

114 "file" : self.ct.value(), 

115 } 

116 if not os.path.isfile(source_info["file"]): 

117 self.error(self.ct.loc, 

118 f"cannot find file {source_info['file']}") 

119 item.source.append(source_info) 

120 

121 if self.peek("KEYWORD", "with"): 121 ↛ 122line 121 didn't jump to line 122 because the condition on line 121 was never true

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

123 

124 self.match("SEMI") 

125 

126 elif self.peek("KEYWORD", "trace"): 

127 self.match("KEYWORD", "trace") 

128 self.match("KEYWORD", "to") 

129 self.match("COLON") 

130 self.match("STRING") 

131 if self.ct.value() == level_name: 131 ↛ 132line 131 didn't jump to line 132 because the condition on line 131 was never true

132 self.error(self.ct.loc, 

133 "cannot trace to yourself") 

134 elif self.ct.value() not in self.levels: 134 ↛ 135line 134 didn't jump to line 135 because the condition on line 134 was never true

135 self.error(self.ct.loc, 

136 f"unknown item {self.ct.value()}") 

137 else: 

138 self.levels[self.ct.value()].needs_tracing_down = True 

139 item.traces.append(self.ct.value()) 

140 item.needs_tracing_up = True 

141 

142 self.match("SEMI") 

143 

144 elif self.peek("KEYWORD", "requires"): 144 ↛ 163line 144 didn't jump to line 163 because the condition on line 144 was always true

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

146 self.match("COLON") 

147 

148 req_list = [] 

149 

150 self.match("STRING") 

151 req_list.append(self.ct) 

152 

153 while self.peek("KEYWORD", "or"): 

154 self.match("KEYWORD", "or") 

155 self.match("STRING") 

156 req_list.append(self.ct) 

157 

158 self.match("SEMI") 

159 

160 item.raw_trace_requirements.append(req_list) 

161 

162 else: 

163 self.error(self.nt.loc, 

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

165 

166 self.match("C_KET") 

167 

168 

169def load(mh, file_name): 

170 parser = Parser(mh, file_name) 

171 ast = parser.parse() 

172 

173 # Resolve requires links now 

174 for item in ast.values(): 

175 item.breakdown_requirements = [] 

176 if item.raw_trace_requirements: 

177 for chain in item.raw_trace_requirements: 

178 new_chain = [] 

179 for tok in chain: 

180 if tok.value() not in ast: 180 ↛ 181line 180 didn't jump to line 181 because the condition on line 180 was never true

181 mh.error(tok.loc, f"unknown level {tok.value()}") 

182 if item.name not in ast[tok.value()].traces: 182 ↛ 183line 182 didn't jump to line 183 because the condition on line 182 was never true

183 mh.error(tok.loc, 

184 f"{tok.value()} cannot trace to {item.name} items") 

185 new_chain.append(tok.value()) 

186 item.breakdown_requirements.append(new_chain) 

187 else: 

188 for src in ast.values(): 

189 if item.name in src.traces: 

190 item.breakdown_requirements.append([src.name]) 

191 item.raw_trace_requirements.clear() 

192 

193 return ast 

194 

195 

196def sanity_test(): 

197 mh = errors.Message_Handler() 

198 

199 try: 

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

201 print(config) 

202 except errors.LOBSTER_Error: 

203 return 1 

204 return 0 

205 

206 

207if __name__ == "__main__": 

208 sanity_test()