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

115 statements  

« prev     ^ index     » next       coverage.py v7.10.5, created at 2025-08-27 13: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 elif 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 elif self.nt.kind == kind: 

52 if value is None: 

53 return True 

54 else: 

55 return self.nt.value() == value 

56 else: 

57 return False 

58 

59 def match(self, kind, value=None): 

60 if self.peek(kind, value): 60 ↛ 62line 60 didn't jump to line 62 because the condition on line 60 was always true

61 self.advance() 

62 elif self.nt is None: 

63 self.error( 

64 location.File_Reference(filename = self.lexer.file_name), 

65 "expected %s, found EOF" % kind) 

66 elif value is None: 

67 self.error(self.nt.loc, 

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

69 self.nt.kind, 

70 self.nt.value())) 

71 else: 

72 self.error(self.nt.loc, 

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

74 

75 def warning(self, loc, message): 

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

77 

78 def error(self, loc, message): 

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

80 

81 def parse(self): 

82 while self.nt: 

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

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

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

86 self.parse_level_declaration() 

87 else: 

88 self.error(self.nt.loc, 

89 "expected: requirements|implementation|activity," 

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

91 

92 return self.levels 

93 

94 def parse_level_declaration(self): 

95 self.match("KEYWORD") 

96 level_kind = self.ct.value() 

97 

98 self.match("STRING") 

99 level_name = self.ct.value() 

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

101 self.error(self.ct.loc, 

102 "duplicate declaration") 

103 

104 item = LevelDefinition( 

105 name=level_name, 

106 kind=level_kind, 

107 ) 

108 self.levels[level_name] = item 

109 

110 self.match("C_BRA") 

111 

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

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

114 self.advance() 

115 self.match("COLON") 

116 self.match("STRING") 

117 source_info = { 

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

119 } 

120 if not os.path.isfile(source_info["file"]): 120 ↛ 121line 120 didn't jump to line 121 because the condition on line 120 was never true

121 self.error(self.ct.loc, 

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

123 item.source.append(source_info) 

124 

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

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

127 

128 self.match("SEMI") 

129 

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

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

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

133 self.match("COLON") 

134 self.match("STRING") 

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

136 self.error(self.ct.loc, 

137 "cannot trace to yourself") 

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

139 self.error(self.ct.loc, 

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

141 else: 

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

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

144 item.needs_tracing_up = True 

145 

146 self.match("SEMI") 

147 

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

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

150 self.match("COLON") 

151 

152 req_list = [] 

153 

154 self.match("STRING") 

155 req_list.append(self.ct) 

156 

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

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

159 self.match("STRING") 

160 req_list.append(self.ct) 

161 

162 self.match("SEMI") 

163 

164 item.raw_trace_requirements.append(req_list) 

165 

166 else: 

167 self.error(self.nt.loc, 

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

169 

170 self.match("C_KET") 

171 

172 

173def load(mh, file_name): 

174 parser = Parser(mh, file_name) 

175 ast = parser.parse() 

176 

177 # Resolve requires links now 

178 for item in ast.values(): 

179 item.breakdown_requirements = [] 

180 if item.raw_trace_requirements: 180 ↛ 181line 180 didn't jump to line 181 because the condition on line 180 was never true

181 for chain in item.raw_trace_requirements: 

182 new_chain = [] 

183 for tok in chain: 

184 if tok.value() not in ast: 

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

186 if item.name not in ast[tok.value()].traces: 

187 mh.error(tok.loc, 

188 "%s cannot trace to %s items" % 

189 (tok.value(), 

190 item.name)) 

191 new_chain.append(tok.value()) 

192 item.breakdown_requirements.append(new_chain) 

193 else: 

194 for src in ast.values(): 

195 if item.name in src.traces: 

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

197 item.raw_trace_requirements.clear() 

198 

199 return ast 

200 

201 

202def sanity_test(): 

203 mh = errors.Message_Handler() 

204 

205 try: 

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

207 print(config) 

208 except errors.LOBSTER_Error: 

209 return 1 

210 return 0 

211 

212 

213if __name__ == "__main__": 

214 sanity_test()