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

115 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2026-04-16 05:31 +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 "expected %s, found EOF" % kind) 

64 elif value is None: 

65 self.error(self.nt.loc, 

66 "expected %s, found %s %s" % (kind, 

67 self.nt.kind, 

68 self.nt.value())) 

69 else: 

70 self.error(self.nt.loc, 

71 "expected %s, found %s" % (value, self.nt.value())) 

72 

73 def warning(self, loc, message): 

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

75 

76 def error(self, loc, message): 

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

78 

79 def parse(self): 

80 while self.nt: 

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

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

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

84 self.parse_level_declaration() 

85 else: 

86 self.error(self.nt.loc, 

87 "expected: requirements|implementation|activity," 

88 " found %s instead" % self.nt.value()) 

89 

90 return self.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 if level_name in self.levels: 98 ↛ 99line 98 didn't jump to line 99 because the condition on line 98 was never true

99 self.error(self.ct.loc, 

100 "duplicate declaration") 

101 

102 item = LevelDefinition( 

103 name=level_name, 

104 kind=level_kind, 

105 ) 

106 self.levels[level_name] = item 

107 

108 self.match("C_BRA") 

109 

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

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

112 self.advance() 

113 self.match("COLON") 

114 self.match("STRING") 

115 source_info = { 

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

117 } 

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

119 self.error(self.ct.loc, 

120 "cannot find file %s" % source_info["file"]) 

121 item.source.append(source_info) 

122 

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

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

125 

126 self.match("SEMI") 

127 

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

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

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

131 self.match("COLON") 

132 self.match("STRING") 

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

134 self.error(self.ct.loc, 

135 "cannot trace to yourself") 

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

137 self.error(self.ct.loc, 

138 "unknown item %s" % self.ct.value()) 

139 else: 

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

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

142 item.needs_tracing_up = True 

143 

144 self.match("SEMI") 

145 

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

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

148 self.match("COLON") 

149 

150 req_list = [] 

151 

152 self.match("STRING") 

153 req_list.append(self.ct) 

154 

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

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

157 self.match("STRING") 

158 req_list.append(self.ct) 

159 

160 self.match("SEMI") 

161 

162 item.raw_trace_requirements.append(req_list) 

163 

164 else: 

165 self.error(self.nt.loc, 

166 "unexpected directive %s" % self.nt.value()) 

167 

168 self.match("C_KET") 

169 

170 

171def load(mh, file_name): 

172 parser = Parser(mh, file_name) 

173 ast = parser.parse() 

174 

175 # Resolve requires links now 

176 for item in ast.values(): 

177 item.breakdown_requirements = [] 

178 if item.raw_trace_requirements: 

179 for chain in item.raw_trace_requirements: 

180 new_chain = [] 

181 for tok in chain: 

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

183 mh.error(tok.loc, "unknown level %s" % tok.value()) 

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

185 mh.error(tok.loc, 

186 "%s cannot trace to %s items" % 

187 (tok.value(), 

188 item.name)) 

189 new_chain.append(tok.value()) 

190 item.breakdown_requirements.append(new_chain) 

191 else: 

192 for src in ast.values(): 

193 if item.name in src.traces: 

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

195 item.raw_trace_requirements.clear() 

196 

197 return ast 

198 

199 

200def sanity_test(): 

201 mh = errors.Message_Handler() 

202 

203 try: 

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

205 print(config) 

206 except errors.LOBSTER_Error: 

207 return 1 

208 return 0 

209 

210 

211if __name__ == "__main__": 

212 sanity_test()