Coverage for trlc/lint.py: 89%

167 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-11-06 14:44 +0000

1#!/usr/bin/env python3 

2# 

3# TRLC - Treat Requirements Like Code 

4# Copyright (C) 2022-2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) 

5# 

6# This file is part of the TRLC Python Reference Implementation. 

7# 

8# TRLC is free software: you can redistribute it and/or modify it 

9# under the terms of the GNU General Public License as published by 

10# the Free Software Foundation, either version 3 of the License, or 

11# (at your option) any later version. 

12# 

13# TRLC is distributed in the hope that it will be useful, but WITHOUT 

14# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 

15# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public 

16# License for more details. 

17# 

18# You should have received a copy of the GNU General Public License 

19# along with TRLC. If not, see <https://www.gnu.org/licenses/>. 

20 

21from trlc import ast 

22from trlc.errors import Message_Handler, TRLC_Error 

23from trlc.vcg import VCG 

24 

25 

26class Linter: 

27 def __init__(self, mh, stab, verify_checks, debug_vcg): 

28 # lobster-exclude: Not safety relevant 

29 assert isinstance(mh, Message_Handler) 

30 assert isinstance(stab, ast.Symbol_Table) 

31 assert isinstance(verify_checks, bool) 

32 assert isinstance(debug_vcg, bool) 

33 

34 self.mh = mh 

35 self.stab = stab 

36 self.verify_checks = verify_checks 

37 self.debug_vcg = debug_vcg 

38 

39 self.abstract_extensions = {} 

40 self.checked_types = set() 

41 

42 def perform_sanity_checks(self): 

43 # lobster-exclude: Not safety relevant 

44 ok = True 

45 for package in self.stab.values(ast.Package): 

46 for n_typ in package.symbols.values(ast.Type): 

47 try: 

48 self.verify_type(n_typ) 

49 except TRLC_Error: 

50 ok = False 

51 

52 # Complain about abstract types without extensions 

53 # lobster-trace: LRM.Abstract_Type_Not_Extended 

54 for package in self.stab.values(ast.Package): 

55 for n_typ in package.symbols.values(ast.Record_Type): 

56 if n_typ.is_abstract and not self.abstract_extensions[n_typ]: 

57 self.mh.check( 

58 n_typ.location, 

59 f"abstract type {n_typ.name} does not have any extensions", 

60 "abstract_leaf_types", 

61 ) 

62 

63 return ok 

64 

65 def verify_type(self, n_typ): 

66 # lobster-exclude: Not safety relevant 

67 assert isinstance(n_typ, ast.Type) 

68 

69 if n_typ in self.checked_types: 

70 return 

71 else: 

72 self.checked_types.add(n_typ) 

73 

74 if isinstance(n_typ, ast.Record_Type): 

75 self.verify_record_type(n_typ) 

76 

77 elif isinstance(n_typ, ast.Tuple_Type): 

78 self.verify_tuple_type(n_typ) 

79 

80 elif isinstance(n_typ, ast.Array_Type): 

81 self.verify_array_type(n_typ) 

82 

83 elif isinstance(n_typ, ast.Union_Type): 

84 invalid_members = [ 

85 t for t in n_typ.types if not isinstance(t, ast.Record_Type) 

86 ] 

87 if invalid_members: 87 ↛ 88line 87 didn't jump to line 88 because the condition on line 87 was never true

88 for t in invalid_members: 

89 self.mh.error( 

90 n_typ.location, 

91 f"{t.__class__.__name__} {t.name} is not a Record_Type", 

92 fatal=False, 

93 ) 

94 return 

95 # lobster-trace: LRM.Union_Type_Minimum_Members 

96 if len(n_typ.types) == 1: 

97 self.mh.check( 

98 n_typ.location, 

99 "union type with a single member is equivalent to a" 

100 " plain record reference", 

101 "union_single_type", 

102 ) 

103 # lobster-trace: LRM.Union_Type_No_Subtype_Relations 

104 for i, t_i in enumerate(n_typ.types): 

105 for j, t_j in enumerate(n_typ.types): 

106 if ( 

107 i != j 

108 and isinstance(t_i, ast.Record_Type) 

109 and isinstance(t_j, ast.Record_Type) 

110 and t_i is not t_j 

111 and t_i.is_subclass_of(t_j) 

112 ): 

113 self.mh.check( 

114 n_typ.location, 

115 "%s is a subtype of %s which is already" 

116 " in this union" % (t_i.name, t_j.name), 

117 "union_redundant_subtype", 

118 ) 

119 for member_type in n_typ.types: 

120 self.verify_type(member_type) 

121 

122 def verify_tuple_type(self, n_tuple_type): 

123 assert isinstance(n_tuple_type, ast.Tuple_Type) 

124 

125 # Detect confusing separators 

126 # lobster-trace: LRM.Tuple_Based_Literal_Ambiguity 

127 previous_was_int = False 

128 previous_was_bad_sep = False 

129 bad_separator = None 

130 location = None 

131 for n_item in n_tuple_type.iter_sequence(): 

132 if previous_was_bad_sep: 

133 assert isinstance(n_item, ast.Composite_Component) 

134 if isinstance(n_item.n_typ, ast.Builtin_Integer): 

135 explanation = [ 

136 "For example 0%s100 would be a base %u literal" 

137 % (bad_separator, {"b": 2, "x": 16}[bad_separator]), 

138 "instead of the tuple segment 0 %s 100." % bad_separator, 

139 ] 

140 else: 

141 explanation = [ 

142 "For example 0%s%s would be a lexer error" 

143 % (bad_separator, n_item.n_typ.get_example_value()), 

144 "instead of the tuple segment 0 %s %s." 

145 % (bad_separator, n_item.n_typ.get_example_value()), 

146 ] 

147 

148 self.mh.check( 

149 location, 

150 "%s separator after integer component" 

151 " creates ambiguities" % bad_separator, 

152 "separator_based_literal_ambiguity", 

153 "\n".join(explanation), 

154 ) 

155 

156 elif isinstance(n_item, ast.Composite_Component) and isinstance( 

157 n_item.n_typ, ast.Builtin_Integer 

158 ): 

159 previous_was_int = True 

160 

161 elif ( 

162 isinstance(n_item, ast.Separator) 

163 and previous_was_int 

164 and n_item.to_string() in ("x", "b") 

165 ): 

166 previous_was_bad_sep = True 

167 bad_separator = n_item.to_string() 

168 location = n_item.location 

169 

170 else: 

171 previous_was_int = False 

172 previous_was_bad_sep = False 

173 

174 # Walk over components 

175 for n_component in n_tuple_type.components.values(): 

176 self.verify_type(n_component.n_typ) 

177 

178 # Verify checks 

179 if self.verify_checks: 

180 vcg = VCG(mh=self.mh, n_ctyp=n_tuple_type, debug=self.debug_vcg) 

181 vcg.analyze() 

182 

183 def verify_record_type(self, n_record_type): 

184 # lobster-exclude: Not safety relevant 

185 assert isinstance(n_record_type, ast.Record_Type) 

186 

187 # Mark abstract extensions 

188 if n_record_type.is_abstract: 

189 if n_record_type not in self.abstract_extensions: 

190 self.abstract_extensions[n_record_type] = set() 

191 elif n_record_type.parent: 

192 ancestor = n_record_type.parent 

193 while ancestor is not None and ancestor.is_abstract: 

194 if ancestor not in self.abstract_extensions: 

195 self.abstract_extensions[ancestor] = set() 

196 self.abstract_extensions[ancestor].add(n_record_type) 

197 ancestor = ancestor.parent 

198 

199 # Walk over components 

200 for n_component in n_record_type.components.values(): 

201 self.verify_type(n_component.n_typ) 

202 

203 # Verify checks 

204 if self.verify_checks: 

205 vcg = VCG(mh=self.mh, n_ctyp=n_record_type, debug=self.debug_vcg) 

206 vcg.analyze() 

207 

208 def verify_array_type(self, n_typ): 

209 # lobster-exclude: Not safety relevant 

210 assert isinstance(n_typ, ast.Array_Type) 

211 

212 if n_typ.upper_bound is None: 

213 pass 

214 elif n_typ.lower_bound > n_typ.upper_bound: 

215 self.mh.check( 

216 n_typ.loc_upper, 

217 "upper bound must be at least %u" % n_typ.lower_bound, 

218 "impossible_array_types", 

219 ) 

220 elif n_typ.upper_bound == 0: 

221 self.mh.check( 

222 n_typ.loc_upper, "this array makes no sense", "impossible_array_types" 

223 ) 

224 elif n_typ.upper_bound == 1 and n_typ.lower_bound == 1: 

225 self.mh.check( 

226 n_typ.loc_upper, 

227 "array of fixed size 1 should not be an array", 

228 "weird_array_types", 

229 "An array with a fixed size of 1 should not\nbe an array at all.", 

230 ) 

231 elif n_typ.upper_bound == 1 and n_typ.lower_bound == 0: 

232 self.mh.check( 

233 n_typ.loc_upper, 

234 "consider making this array an optional %s" % n_typ.element_type.name, 

235 "weird_array_types", 

236 "An array with 0 to 1 components should just\n" 

237 "be an optional %s instead." % n_typ.element_type.name, 

238 ) 

239 

240 def _find_import_location(self, cu, name, is_wildcard): 

241 # lobster-exclude: Utility function 

242 for raw_name, raw_loc, raw_wildcard, _tokens in cu.raw_imports: 242 ↛ 245line 242 didn't jump to line 245 because the loop on line 242 didn't complete

243 if raw_name == name and raw_wildcard == is_wildcard: 

244 return raw_loc 

245 return None 

246 

247 def _import_in_markup(self, file, item, include_descendants=False): 

248 # lobster-exclude: Utility function 

249 # A package referenced in a markup string counts as used. For a 

250 # wildcard root, a reference to any descendant package also counts. 

251 for token in file.lexer.tokens: 

252 literal = token.ast_link 

253 if not isinstance(literal, ast.String_Literal): 

254 continue 

255 if not literal.has_references: 255 ↛ 257line 255 didn't jump to line 257 because the condition on line 255 was always true

256 continue 

257 for reference in literal.references: 

258 if reference.package.name == item.name: 

259 return True 

260 if include_descendants and reference.package.name.startswith( 

261 item.name + "." 

262 ): 

263 return True 

264 return False 

265 

266 def verify_imports(self): 

267 # lobster-trace: LRM.Wildcard_Import 

268 for file in self.mh.sm.all_files.values(): 

269 if not file.primary and not file.secondary: 

270 continue 

271 cu = file.cu 

272 if not cu.imports and not cu.wildcard_roots: 

273 continue 

274 

275 # Redundant explicit imports already covered by a wildcard import. 

276 for item in cu.imports: 

277 root = cu.covered_by_wildcard(item) 

278 if root is None: 

279 continue 

280 imp_location = self._find_import_location(cu, item.name, False) 

281 if imp_location is not None: 281 ↛ 276line 281 didn't jump to line 276 because the condition on line 281 was always true

282 self.mh.check( 

283 imp_location, 

284 "redundant import %s, already covered by" 

285 " wildcard import %s.*" % (item.name, root.name), 

286 "unused_imports", 

287 "Consider deleting this import statement.", 

288 ) 

289 

290 # Unused explicit imports. 

291 for item in cu.imports: 

292 if item in cu.referenced_imports: 

293 continue 

294 # Skip the ones already reported as redundant above. 

295 if cu.covered_by_wildcard(item) is not None: 

296 continue 

297 if self._import_in_markup(file, item): 297 ↛ 298line 297 didn't jump to line 298 because the condition on line 297 was never true

298 continue 

299 imp_location = self._find_import_location(cu, item.name, False) 

300 if imp_location is not None: 300 ↛ 291line 300 didn't jump to line 291 because the condition on line 300 was always true

301 self.mh.check( 

302 imp_location, 

303 "unused import %s" % item.name, 

304 "unused_imports", 

305 "Consider deleting this import statement if not needed.", 

306 ) 

307 

308 # Unused wildcard imports (no package in the subtree referenced). 

309 for root in cu.wildcard_roots: 

310 if root in cu.referenced_imports: 

311 continue 

312 if self._import_in_markup(file, root, include_descendants=True): 312 ↛ 313line 312 didn't jump to line 313 because the condition on line 312 was never true

313 continue 

314 imp_location = self._find_import_location(cu, root.name, True) 

315 if imp_location is not None: 315 ↛ 309line 315 didn't jump to line 309 because the condition on line 315 was always true

316 self.mh.check( 

317 imp_location, 

318 "unused wildcard import %s.*" % root.name, 

319 "unused_imports", 

320 "Consider deleting this import statement if not needed.", 

321 ) 

322 

323 # Trivial wildcard imports (root has no sub-packages). 

324 # lobster-trace: LRM.Wildcard_Trivial 

325 for root in cu.wildcard_roots: 

326 if not root.sub_packages.table: 

327 # Skip if already reported as unused (deleting is better 

328 # advice than replacing). 

329 if root not in cu.referenced_imports: 

330 continue 

331 if self._import_in_markup(file, root, include_descendants=True): 331 ↛ 332line 331 didn't jump to line 332 because the condition on line 331 was never true

332 continue 

333 imp_location = self._find_import_location(cu, root.name, True) 

334 if imp_location is not None: 334 ↛ 325line 334 didn't jump to line 325 because the condition on line 334 was always true

335 self.mh.check( 

336 imp_location, 

337 "wildcard import %s.* is equivalent to import %s" 

338 " since %s has no sub-packages" 

339 % (root.name, root.name, root.name), 

340 "unused_imports", 

341 "Consider replacing with: import %s" % root.name, 

342 )