Coverage for trlc/lint.py: 94%
123 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-07-07 15:09 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-07-07 15:09 +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")
62 return ok
64 def verify_type(self, n_typ):
65 # lobster-exclude: Not safety relevant
66 assert isinstance(n_typ, ast.Type)
68 if n_typ in self.checked_types:
69 return
70 else:
71 self.checked_types.add(n_typ)
73 if isinstance(n_typ, ast.Record_Type):
74 self.verify_record_type(n_typ)
76 elif isinstance(n_typ, ast.Tuple_Type):
77 self.verify_tuple_type(n_typ)
79 elif isinstance(n_typ, ast.Array_Type):
80 self.verify_array_type(n_typ)
82 elif isinstance(n_typ, ast.Union_Type):
83 # lobster-trace: LRM.Union_Type_Minimum_Members
84 if len(n_typ.types) == 1:
85 self.mh.check(
86 n_typ.location,
87 "union type with a single member is equivalent to a"
88 " plain record reference",
89 "union_single_type")
90 # lobster-trace: LRM.Union_Type_No_Subtype_Relations
91 for i, t_i in enumerate(n_typ.types):
92 for j, t_j in enumerate(n_typ.types):
93 if i != j and t_i is not t_j and \
94 t_i.is_subclass_of(t_j):
95 self.mh.check(
96 n_typ.location,
97 "%s is a subtype of %s which is already"
98 " in this union" % (t_i.name, t_j.name),
99 "union_redundant_subtype")
100 for member_type in n_typ.types:
101 self.verify_type(member_type)
103 def verify_tuple_type(self, n_tuple_type):
104 assert isinstance(n_tuple_type, ast.Tuple_Type)
106 # Detect confusing separators
107 # lobster-trace: LRM.Tuple_Based_Literal_Ambiguity
108 previous_was_int = False
109 previous_was_bad_sep = False
110 bad_separator = None
111 location = None
112 for n_item in n_tuple_type.iter_sequence():
113 if previous_was_bad_sep:
114 assert isinstance(n_item, ast.Composite_Component)
115 if isinstance(n_item.n_typ, ast.Builtin_Integer):
116 explanation = [
117 "For example 0%s100 would be a base %u literal" %
118 (bad_separator,
119 {"b" : 2, "x" : 16}[bad_separator]),
120 "instead of the tuple segment 0 %s 100." %
121 bad_separator
122 ]
123 else:
124 explanation = [
125 "For example 0%s%s would be a lexer error" %
126 (bad_separator,
127 n_item.n_typ.get_example_value()),
128 "instead of the tuple segment 0 %s %s." %
129 (bad_separator,
130 n_item.n_typ.get_example_value())
131 ]
133 self.mh.check(
134 location,
135 "%s separator after integer component"
136 " creates ambiguities" % bad_separator,
137 "separator_based_literal_ambiguity",
138 "\n".join(explanation))
140 elif isinstance(n_item, ast.Composite_Component) and \
141 isinstance(n_item.n_typ, ast.Builtin_Integer):
142 previous_was_int = True
144 elif isinstance(n_item, ast.Separator) and \
145 previous_was_int and \
146 n_item.to_string() in ("x", "b"):
147 previous_was_bad_sep = True
148 bad_separator = n_item.to_string()
149 location = n_item.location
151 else:
152 previous_was_int = False
153 previous_was_bad_sep = False
155 # Walk over components
156 for n_component in n_tuple_type.components.values():
157 self.verify_type(n_component.n_typ)
159 # Verify checks
160 if self.verify_checks: 160 ↛ exitline 160 didn't return from function 'verify_tuple_type' because the condition on line 160 was always true
161 vcg = VCG(mh = self.mh,
162 n_ctyp = n_tuple_type,
163 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,
189 n_ctyp = n_record_type,
190 debug = self.debug_vcg)
191 vcg.analyze()
193 def verify_array_type(self, n_typ):
194 # lobster-exclude: Not safety relevant
195 assert isinstance(n_typ, ast.Array_Type)
197 if n_typ.upper_bound is None:
198 pass
199 elif n_typ.lower_bound > n_typ.upper_bound:
200 self.mh.check(n_typ.loc_upper,
201 "upper bound must be at least %u" %
202 n_typ.lower_bound,
203 "impossible_array_types")
204 elif n_typ.upper_bound == 0:
205 self.mh.check(n_typ.loc_upper,
206 "this array makes no sense",
207 "impossible_array_types")
208 elif n_typ.upper_bound == 1 and n_typ.lower_bound == 1:
209 self.mh.check(n_typ.loc_upper,
210 "array of fixed size 1 "
211 "should not be an array",
212 "weird_array_types",
213 "An array with a fixed size of 1 should not\n"
214 "be an array at all.")
215 elif n_typ.upper_bound == 1 and n_typ.lower_bound == 0:
216 self.mh.check(n_typ.loc_upper,
217 "consider making this array an"
218 " optional %s" % n_typ.element_type.name,
219 "weird_array_types",
220 "An array with 0 to 1 components should just\n"
221 "be an optional %s instead." %
222 n_typ.element_type.name)
224 def markup_ref(self, item, string_literals):
225 for string_literal in string_literals: 225 ↛ 226line 225 didn't jump to line 226 because the loop on line 225 never started
226 for reference in string_literal.references:
227 if reference.package.name == item.name:
228 return string_literal
229 return None
231 def verify_imports(self):
232 for file in self.mh.sm.all_files.values():
233 if not file.primary and not file.secondary:
234 continue
235 if not file.cu.imports:
236 continue
237 for item in file.cu.imports:
238 import_tokens = [t for t in file.lexer.tokens
239 if t.value == item.name]
240 markup = self.markup_ref(item,
241 (m.ast_link for m in
242 file.lexer.tokens if
243 isinstance(m.ast_link,
244 ast.String_Literal) and
245 m.ast_link.has_references))
246 if markup is not None: 246 ↛ 247line 246 didn't jump to line 247 because the condition on line 246 was never true
247 import_tokens.append(markup)
248 if len(import_tokens) == 1:
249 import_tk = import_tokens[0]
250 self.mh.check(import_tk.location,
251 "unused import %s" % import_tk.value,
252 "unused_imports",
253 "Consider deleting this import"
254 " statement if not needed.")