Coverage for trlc/lint.py: 91%
162 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-09-30 11:03 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-09-30 11:03 +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 # lobster-trace: LRM.Union_Type_Minimum_Members
85 if len(n_typ.types) == 1:
86 self.mh.check(
87 n_typ.location,
88 "union type with a single member is equivalent to a"
89 " plain record reference",
90 "union_single_type",
91 )
92 # lobster-trace: LRM.Union_Type_No_Subtype_Relations
93 for i, t_i in enumerate(n_typ.types):
94 for j, t_j in enumerate(n_typ.types):
95 if i != j and t_i is not t_j and t_i.is_subclass_of(t_j):
96 self.mh.check(
97 n_typ.location,
98 "%s is a subtype of %s which is already"
99 " in this union" % (t_i.name, t_j.name),
100 "union_redundant_subtype",
101 )
102 for member_type in n_typ.types:
103 self.verify_type(member_type)
105 def verify_tuple_type(self, n_tuple_type):
106 assert isinstance(n_tuple_type, ast.Tuple_Type)
108 # Detect confusing separators
109 # lobster-trace: LRM.Tuple_Based_Literal_Ambiguity
110 previous_was_int = False
111 previous_was_bad_sep = False
112 bad_separator = None
113 location = None
114 for n_item in n_tuple_type.iter_sequence():
115 if previous_was_bad_sep:
116 assert isinstance(n_item, ast.Composite_Component)
117 if isinstance(n_item.n_typ, ast.Builtin_Integer):
118 explanation = [
119 "For example 0%s100 would be a base %u literal"
120 % (bad_separator, {"b": 2, "x": 16}[bad_separator]),
121 "instead of the tuple segment 0 %s 100." % bad_separator,
122 ]
123 else:
124 explanation = [
125 "For example 0%s%s would be a lexer error"
126 % (bad_separator, n_item.n_typ.get_example_value()),
127 "instead of the tuple segment 0 %s %s."
128 % (bad_separator, n_item.n_typ.get_example_value()),
129 ]
131 self.mh.check(
132 location,
133 "%s separator after integer component"
134 " creates ambiguities" % bad_separator,
135 "separator_based_literal_ambiguity",
136 "\n".join(explanation),
137 )
139 elif isinstance(n_item, ast.Composite_Component) and isinstance(
140 n_item.n_typ, ast.Builtin_Integer
141 ):
142 previous_was_int = True
144 elif (
145 isinstance(n_item, ast.Separator)
146 and previous_was_int
147 and n_item.to_string() in ("x", "b")
148 ):
149 previous_was_bad_sep = True
150 bad_separator = n_item.to_string()
151 location = n_item.location
153 else:
154 previous_was_int = False
155 previous_was_bad_sep = False
157 # Walk over components
158 for n_component in n_tuple_type.components.values():
159 self.verify_type(n_component.n_typ)
161 # Verify checks
162 if self.verify_checks: 162 ↛ exitline 162 didn't return from function 'verify_tuple_type' because the condition on line 162 was always true
163 vcg = VCG(mh=self.mh, n_ctyp=n_tuple_type, debug=self.debug_vcg)
164 vcg.analyze()
166 def verify_record_type(self, n_record_type):
167 # lobster-exclude: Not safety relevant
168 assert isinstance(n_record_type, ast.Record_Type)
170 # Mark abstract extensions
171 if n_record_type.is_abstract:
172 if n_record_type not in self.abstract_extensions:
173 self.abstract_extensions[n_record_type] = set()
174 elif n_record_type.parent:
175 ancestor = n_record_type.parent
176 while ancestor is not None and ancestor.is_abstract:
177 if ancestor not in self.abstract_extensions:
178 self.abstract_extensions[ancestor] = set()
179 self.abstract_extensions[ancestor].add(n_record_type)
180 ancestor = ancestor.parent
182 # Walk over components
183 for n_component in n_record_type.components.values():
184 self.verify_type(n_component.n_typ)
186 # Verify checks
187 if self.verify_checks:
188 vcg = VCG(mh=self.mh, n_ctyp=n_record_type, debug=self.debug_vcg)
189 vcg.analyze()
191 def verify_array_type(self, n_typ):
192 # lobster-exclude: Not safety relevant
193 assert isinstance(n_typ, ast.Array_Type)
195 if n_typ.upper_bound is None:
196 pass
197 elif n_typ.lower_bound > n_typ.upper_bound:
198 self.mh.check(
199 n_typ.loc_upper,
200 "upper bound must be at least %u" % n_typ.lower_bound,
201 "impossible_array_types",
202 )
203 elif n_typ.upper_bound == 0:
204 self.mh.check(
205 n_typ.loc_upper, "this array makes no sense", "impossible_array_types"
206 )
207 elif n_typ.upper_bound == 1 and n_typ.lower_bound == 1:
208 self.mh.check(
209 n_typ.loc_upper,
210 "array of fixed size 1 should not be an array",
211 "weird_array_types",
212 "An array with a fixed size of 1 should not\nbe an array at all.",
213 )
214 elif n_typ.upper_bound == 1 and n_typ.lower_bound == 0:
215 self.mh.check(
216 n_typ.loc_upper,
217 "consider making this array an optional %s" % n_typ.element_type.name,
218 "weird_array_types",
219 "An array with 0 to 1 components should just\n"
220 "be an optional %s instead." % n_typ.element_type.name,
221 )
223 def _find_import_location(self, cu, name, is_wildcard):
224 # lobster-exclude: Utility function
225 for raw_name, raw_loc, raw_wildcard, _tokens in cu.raw_imports: 225 ↛ 228line 225 didn't jump to line 228 because the loop on line 225 didn't complete
226 if raw_name == name and raw_wildcard == is_wildcard:
227 return raw_loc
228 return None
230 def _import_in_markup(self, file, item, include_descendants=False):
231 # lobster-exclude: Utility function
232 # A package referenced in a markup string counts as used. For a
233 # wildcard root, a reference to any descendant package also counts.
234 for token in file.lexer.tokens:
235 literal = token.ast_link
236 if not isinstance(literal, ast.String_Literal):
237 continue
238 if not literal.has_references: 238 ↛ 240line 238 didn't jump to line 240 because the condition on line 238 was always true
239 continue
240 for reference in literal.references:
241 if reference.package.name == item.name:
242 return True
243 if include_descendants and reference.package.name.startswith(
244 item.name + "."
245 ):
246 return True
247 return False
249 def verify_imports(self):
250 # lobster-trace: LRM.Wildcard_Import
251 for file in self.mh.sm.all_files.values():
252 if not file.primary and not file.secondary:
253 continue
254 cu = file.cu
255 if not cu.imports and not cu.wildcard_roots:
256 continue
258 # Redundant explicit imports already covered by a wildcard import.
259 for item in cu.imports:
260 root = cu.covered_by_wildcard(item)
261 if root is None:
262 continue
263 imp_location = self._find_import_location(cu, item.name, False)
264 if imp_location is not None: 264 ↛ 259line 264 didn't jump to line 259 because the condition on line 264 was always true
265 self.mh.check(
266 imp_location,
267 "redundant import %s, already covered by"
268 " wildcard import %s.*" % (item.name, root.name),
269 "unused_imports",
270 "Consider deleting this import statement.",
271 )
273 # Unused explicit imports.
274 for item in cu.imports:
275 if item in cu.referenced_imports:
276 continue
277 # Skip the ones already reported as redundant above.
278 if cu.covered_by_wildcard(item) is not None:
279 continue
280 if self._import_in_markup(file, item): 280 ↛ 281line 280 didn't jump to line 281 because the condition on line 280 was never true
281 continue
282 imp_location = self._find_import_location(cu, item.name, False)
283 if imp_location is not None: 283 ↛ 274line 283 didn't jump to line 274 because the condition on line 283 was always true
284 self.mh.check(
285 imp_location,
286 "unused import %s" % item.name,
287 "unused_imports",
288 "Consider deleting this import statement if not needed.",
289 )
291 # Unused wildcard imports (no package in the subtree referenced).
292 for root in cu.wildcard_roots:
293 if root in cu.referenced_imports:
294 continue
295 if self._import_in_markup(file, root, include_descendants=True): 295 ↛ 296line 295 didn't jump to line 296 because the condition on line 295 was never true
296 continue
297 imp_location = self._find_import_location(cu, root.name, True)
298 if imp_location is not None: 298 ↛ 292line 298 didn't jump to line 292 because the condition on line 298 was always true
299 self.mh.check(
300 imp_location,
301 "unused wildcard import %s.*" % root.name,
302 "unused_imports",
303 "Consider deleting this import statement if not needed.",
304 )
306 # Trivial wildcard imports (root has no sub-packages).
307 # lobster-trace: LRM.Wildcard_Trivial
308 for root in cu.wildcard_roots:
309 if not root.sub_packages.table:
310 # Skip if already reported as unused (deleting is better
311 # advice than replacing).
312 if root not in cu.referenced_imports:
313 continue
314 if self._import_in_markup(file, root, include_descendants=True): 314 ↛ 315line 314 didn't jump to line 315 because the condition on line 314 was never true
315 continue
316 imp_location = self._find_import_location(cu, root.name, True)
317 if imp_location is not None: 317 ↛ 308line 317 didn't jump to line 308 because the condition on line 317 was always true
318 self.mh.check(
319 imp_location,
320 "wildcard import %s.* is equivalent to import %s"
321 " since %s has no sub-packages"
322 % (root.name, root.name, root.name),
323 "unused_imports",
324 "Consider replacing with: import %s" % root.name,
325 )