Coverage for trlc/lint.py: 89%
167 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-11-06 14:44 +0000
« 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/>.
21from trlc import ast
22from trlc.errors import Message_Handler, TRLC_Error
23from trlc.vcg import VCG
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)
34 self.mh = mh
35 self.stab = stab
36 self.verify_checks = verify_checks
37 self.debug_vcg = debug_vcg
39 self.abstract_extensions = {}
40 self.checked_types = set()
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
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 )
63 return ok
65 def verify_type(self, n_typ):
66 # lobster-exclude: Not safety relevant
67 assert isinstance(n_typ, ast.Type)
69 if n_typ in self.checked_types:
70 return
71 else:
72 self.checked_types.add(n_typ)
74 if isinstance(n_typ, ast.Record_Type):
75 self.verify_record_type(n_typ)
77 elif isinstance(n_typ, ast.Tuple_Type):
78 self.verify_tuple_type(n_typ)
80 elif isinstance(n_typ, ast.Array_Type):
81 self.verify_array_type(n_typ)
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)
122 def verify_tuple_type(self, n_tuple_type):
123 assert isinstance(n_tuple_type, ast.Tuple_Type)
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 ]
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 )
156 elif isinstance(n_item, ast.Composite_Component) and isinstance(
157 n_item.n_typ, ast.Builtin_Integer
158 ):
159 previous_was_int = True
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
170 else:
171 previous_was_int = False
172 previous_was_bad_sep = False
174 # Walk over components
175 for n_component in n_tuple_type.components.values():
176 self.verify_type(n_component.n_typ)
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()
183 def verify_record_type(self, n_record_type):
184 # lobster-exclude: Not safety relevant
185 assert isinstance(n_record_type, ast.Record_Type)
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
199 # Walk over components
200 for n_component in n_record_type.components.values():
201 self.verify_type(n_component.n_typ)
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()
208 def verify_array_type(self, n_typ):
209 # lobster-exclude: Not safety relevant
210 assert isinstance(n_typ, ast.Array_Type)
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 )
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
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
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
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 )
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 )
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 )
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 )