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

139 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-26 14:55 +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 assert isinstance(mh, errors.Message_Handler) 

32 assert isinstance(file_name, str) 

33 assert os.path.isfile(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: 

48 return kind is None 

49 elif kind is None: 

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

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 \ 

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: 

101 self.error(self.ct.loc, 

102 "duplicate declaration") 

103 

104 item = { 

105 "name" : level_name, 

106 "kind" : level_kind, 

107 "traces" : [], 

108 "source" : [], 

109 "needs_tracing_up" : False, 

110 "needs_tracing_down" : False, 

111 "raw_trace_requirements" : [] 

112 } 

113 self.levels[level_name] = item 

114 

115 self.match("C_BRA") 

116 

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

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

119 self.advance() 

120 self.match("COLON") 

121 self.match("STRING") 

122 source_info = { 

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

124 "filters" : [], 

125 } 

126 if level_kind == "requirements": 

127 source_info["valid_status"] = [] 

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

129 self.error(self.ct.loc, 

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

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

132 

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

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

135 

136 while not self.peek("SEMI"): 

137 self.match("KEYWORD") 

138 if self.ct.value() == "prefix": 

139 self.match("STRING") 

140 source_info["filters"].append(("prefix", 

141 self.ct.value())) 

142 

143 elif self.ct.value() == "kind": 

144 self.match("STRING") 

145 source_info["filters"].append(("kind", 

146 self.ct.value())) 

147 

148 elif self.ct.value() == "valid_status": 

149 if level_kind != "requirements": 

150 self.error(self.ct.loc, 

151 "property valid_status is only " 

152 "applicable for requirements") 

153 self.match("C_BRA") 

154 while True: 

155 self.match("STRING") 

156 value = self.ct.value() 

157 if value in source_info["valid_status"]: 

158 self.warning(self.ct.loc, 

159 "duplicate status %s" % 

160 value) 

161 else: 

162 source_info["valid_status"].append(value) 

163 if self.peek("COMMA"): 

164 self.match("COMMA") 

165 else: 

166 break 

167 self.match("C_KET") 

168 

169 else: 

170 self.error(self.ct.loc, 

171 "unknown property '%s'" % 

172 self.ct.value()) 

173 

174 self.match("SEMI") 

175 

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

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

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

179 self.match("COLON") 

180 self.match("STRING") 

181 if self.ct.value() == level_name: 

182 self.error(self.ct.loc, 

183 "cannot trace to yourself") 

184 elif self.ct.value() not in self.levels: 

185 self.error(self.ct.loc, 

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

187 else: 

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

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

190 item["needs_tracing_up"] = True 

191 

192 self.match("SEMI") 

193 

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

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

196 self.match("COLON") 

197 

198 req_list = [] 

199 

200 self.match("STRING") 

201 req_list.append(self.ct) 

202 

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

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

205 self.match("STRING") 

206 req_list.append(self.ct) 

207 

208 self.match("SEMI") 

209 

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

211 

212 else: 

213 self.error(self.nt.loc, 

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

215 

216 self.match("C_KET") 

217 

218 

219def load(mh, file_name): 

220 parser = Parser(mh, file_name) 

221 ast = parser.parse() 

222 

223 # Resolve requires links now 

224 for item in ast.values(): 

225 item["breakdown_requirements"] = [] 

226 if len(item["raw_trace_requirements"]) > 0: 

227 for chain in item["raw_trace_requirements"]: 

228 new_chain = [] 

229 for tok in chain: 

230 if tok.value() not in ast: 

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

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

233 mh.error(tok.loc, 

234 "%s cannot trace to %s items" % 

235 (tok.value(), 

236 item["name"])) 

237 new_chain.append(tok.value()) 

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

239 else: 

240 for src in ast.values(): 

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

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

243 del item["raw_trace_requirements"] 

244 

245 return ast 

246 

247 

248def sanity_test(): 

249 mh = errors.Message_Handler() 

250 

251 try: 

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

253 print(config) 

254 except errors.LOBSTER_Error: 

255 return 1 

256 return 0 

257 

258 

259if __name__ == "__main__": 

260 sanity_test()