Coverage for trlc/lint.py: 94%

123 statements  

« 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/>. 

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 return ok 

63 

64 def verify_type(self, n_typ): 

65 # lobster-exclude: Not safety relevant 

66 assert isinstance(n_typ, ast.Type) 

67 

68 if n_typ in self.checked_types: 

69 return 

70 else: 

71 self.checked_types.add(n_typ) 

72 

73 if isinstance(n_typ, ast.Record_Type): 

74 self.verify_record_type(n_typ) 

75 

76 elif isinstance(n_typ, ast.Tuple_Type): 

77 self.verify_tuple_type(n_typ) 

78 

79 elif isinstance(n_typ, ast.Array_Type): 

80 self.verify_array_type(n_typ) 

81 

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) 

102 

103 def verify_tuple_type(self, n_tuple_type): 

104 assert isinstance(n_tuple_type, ast.Tuple_Type) 

105 

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 ] 

132 

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

139 

140 elif isinstance(n_item, ast.Composite_Component) and \ 

141 isinstance(n_item.n_typ, ast.Builtin_Integer): 

142 previous_was_int = True 

143 

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 

150 

151 else: 

152 previous_was_int = False 

153 previous_was_bad_sep = False 

154 

155 # Walk over components 

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

157 self.verify_type(n_component.n_typ) 

158 

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

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, 

189 n_ctyp = n_record_type, 

190 debug = self.debug_vcg) 

191 vcg.analyze() 

192 

193 def verify_array_type(self, n_typ): 

194 # lobster-exclude: Not safety relevant 

195 assert isinstance(n_typ, ast.Array_Type) 

196 

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) 

223 

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 

230 

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.")