Coverage for trlc/lint.py: 94%

123 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2026-09-04 11:46 +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 # 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) 

104 

105 def verify_tuple_type(self, n_tuple_type): 

106 assert isinstance(n_tuple_type, ast.Tuple_Type) 

107 

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 ] 

130 

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 ) 

138 

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

140 n_item.n_typ, ast.Builtin_Integer 

141 ): 

142 previous_was_int = True 

143 

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 

152 

153 else: 

154 previous_was_int = False 

155 previous_was_bad_sep = False 

156 

157 # Walk over components 

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

159 self.verify_type(n_component.n_typ) 

160 

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

165 

166 def verify_record_type(self, n_record_type): 

167 # lobster-exclude: Not safety relevant 

168 assert isinstance(n_record_type, ast.Record_Type) 

169 

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 

181 

182 # Walk over components 

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

184 self.verify_type(n_component.n_typ) 

185 

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

190 

191 def verify_array_type(self, n_typ): 

192 # lobster-exclude: Not safety relevant 

193 assert isinstance(n_typ, ast.Array_Type) 

194 

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 ) 

222 

223 def markup_ref(self, item, string_literals): 

224 for string_literal in string_literals: 224 ↛ 225line 224 didn't jump to line 225 because the loop on line 224 never started

225 for reference in string_literal.references: 

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

227 return string_literal 

228 return None 

229 

230 def verify_imports(self): 

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

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

233 continue 

234 if not file.cu.imports: 

235 continue 

236 for item in file.cu.imports: 

237 import_tokens = [t for t in file.lexer.tokens if t.value == item.name] 

238 markup = self.markup_ref( 

239 item, 

240 ( 

241 m.ast_link 

242 for m in file.lexer.tokens 

243 if isinstance(m.ast_link, ast.String_Literal) 

244 and m.ast_link.has_references 

245 ), 

246 ) 

247 if markup is not None: 247 ↛ 248line 247 didn't jump to line 248 because the condition on line 247 was never true

248 import_tokens.append(markup) 

249 if len(import_tokens) == 1: 

250 import_tk = import_tokens[0] 

251 self.mh.check( 

252 import_tk.location, 

253 "unused import %s" % import_tk.value, 

254 "unused_imports", 

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

256 )