Coverage for lobster/config/parser.py: 61%

114 statements  

« prev     ^ index     » next       coverage.py v7.10.2, created at 2025-08-06 09:51 +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.config import lexer 

25from lobster import errors 

26from lobster import location 

27 

28 

29class Parser: 

30 def __init__(self, mh, file_name): 

31 if not os.path.isfile(file_name): 31 ↛ 32line 31 didn't jump to line 32 because the condition on line 31 was never true

32 raise FileNotFoundError(f"Config file not found: {file_name}") 

33 

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

35 

36 self.ct = None 

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

38 

39 self.levels = collections.OrderedDict() 

40 

41 def advance(self): 

42 self.ct = self.nt 

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

44 

45 def peek(self, kind, value=None): 

46 if self.nt is None: 46 ↛ 47line 46 didn't jump to line 47 because the condition on line 46 was never true

47 return kind is None 

48 elif kind is None: 48 ↛ 49line 48 didn't jump to line 49 because the condition on line 48 was never true

49 return False 

50 elif self.nt.kind == kind: 

51 if value is None: 

52 return True 

53 else: 

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

55 else: 

56 return False 

57 

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

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

60 self.advance() 

61 elif self.nt is None: 

62 self.error( 

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

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

65 elif value is None: 

66 self.error(self.nt.loc, 

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

68 self.nt.kind, 

69 self.nt.value())) 

70 else: 

71 self.error(self.nt.loc, 

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

73 

74 def warning(self, loc, message): 

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

76 

77 def error(self, loc, message): 

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

79 

80 def parse(self): 

81 while self.nt: 

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

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

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

85 self.parse_level_declaration() 

86 else: 

87 self.error(self.nt.loc, 

88 "expected: requirements|implementation|activity," 

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

90 

91 return self.levels 

92 

93 def parse_level_declaration(self): 

94 self.match("KEYWORD") 

95 level_kind = self.ct.value() 

96 

97 self.match("STRING") 

98 level_name = self.ct.value() 

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

100 self.error(self.ct.loc, 

101 "duplicate declaration") 

102 

103 item = { 

104 "name" : level_name, 

105 "kind" : level_kind, 

106 "traces" : [], 

107 "source" : [], 

108 "needs_tracing_up" : False, 

109 "needs_tracing_down" : False, 

110 "raw_trace_requirements" : [] 

111 } 

112 self.levels[level_name] = item 

113 

114 self.match("C_BRA") 

115 

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

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

118 self.advance() 

119 self.match("COLON") 

120 self.match("STRING") 

121 source_info = { 

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

123 } 

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

125 self.error(self.ct.loc, 

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

127 item["source"].append(source_info) 

128 

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

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

131 

132 self.match("SEMI") 

133 

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

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

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

137 self.match("COLON") 

138 self.match("STRING") 

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

140 self.error(self.ct.loc, 

141 "cannot trace to yourself") 

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

143 self.error(self.ct.loc, 

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

145 else: 

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

147 item["traces"].append(self.ct.value()) 

148 item["needs_tracing_up"] = True 

149 

150 self.match("SEMI") 

151 

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

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

154 self.match("COLON") 

155 

156 req_list = [] 

157 

158 self.match("STRING") 

159 req_list.append(self.ct) 

160 

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

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

163 self.match("STRING") 

164 req_list.append(self.ct) 

165 

166 self.match("SEMI") 

167 

168 item["raw_trace_requirements"].append(req_list) 

169 

170 else: 

171 self.error(self.nt.loc, 

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

173 

174 self.match("C_KET") 

175 

176 

177def load(mh, file_name): 

178 parser = Parser(mh, file_name) 

179 ast = parser.parse() 

180 

181 # Resolve requires links now 

182 for item in ast.values(): 

183 item["breakdown_requirements"] = [] 

184 if len(item["raw_trace_requirements"]) > 0: 184 ↛ 185line 184 didn't jump to line 185 because the condition on line 184 was never true

185 for chain in item["raw_trace_requirements"]: 

186 new_chain = [] 

187 for tok in chain: 

188 if tok.value() not in ast: 

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

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

191 mh.error(tok.loc, 

192 "%s cannot trace to %s items" % 

193 (tok.value(), 

194 item["name"])) 

195 new_chain.append(tok.value()) 

196 item["breakdown_requirements"].append(new_chain) 

197 else: 

198 for src in ast.values(): 

199 if item["name"] in src["traces"]: 

200 item["breakdown_requirements"].append([src["name"]]) 

201 del item["raw_trace_requirements"] 

202 

203 return ast 

204 

205 

206def sanity_test(): 

207 mh = errors.Message_Handler() 

208 

209 try: 

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

211 print(config) 

212 except errors.LOBSTER_Error: 

213 return 1 

214 return 0 

215 

216 

217if __name__ == "__main__": 

218 sanity_test()