@h2loop/autosar 0.1.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/agents/autosar-integrator.md +34 -2
- package/package.json +4 -1
- package/plugins/index.js +53 -23
- package/python/ecuc_engine/__init__.py +1 -1
- package/python/ecuc_engine/add_signal.py +351 -0
- package/python/ecuc_engine/arxml_edit.py +387 -0
- package/python/ecuc_engine/audit.py +372 -69
- package/python/ecuc_engine/cli.py +119 -10
- package/python/ecuc_engine/codegen/__init__.py +185 -0
- package/python/ecuc_engine/codegen/binding.py +210 -0
- package/python/ecuc_engine/codegen/bindings/sip_standin.json +111 -0
- package/python/ecuc_engine/codegen/cdiff.py +395 -0
- package/python/ecuc_engine/codegen/cwriter.py +150 -0
- package/python/ecuc_engine/codegen/emit.py +395 -0
- package/python/ecuc_engine/codegen/handles.py +89 -0
- package/python/ecuc_engine/codegen/mcal/tc4d.json +60 -0
- package/python/ecuc_engine/codegen/model.py +259 -0
- package/python/ecuc_engine/codegen/modules/__init__.py +7 -0
- package/python/ecuc_engine/codegen/modules/comstack.py +14 -0
- package/python/ecuc_engine/codegen/modules/dem.py +22 -0
- package/python/ecuc_engine/codegen/modules/mcal.py +170 -0
- package/python/ecuc_engine/codegen/modules/memstack.py +24 -0
- package/python/ecuc_engine/codegen/registry.py +133 -0
- package/python/ecuc_engine/codegen/schm_memmap.py +159 -0
- package/python/ecuc_engine/context.py +481 -0
- package/python/ecuc_engine/csrc.py +189 -0
- package/python/ecuc_engine/defs.py +30 -0
- package/python/ecuc_engine/diff.py +13 -5
- package/python/ecuc_engine/find.py +162 -0
- package/python/ecuc_engine/gen.py +105 -0
- package/python/ecuc_engine/history.py +146 -0
- package/python/ecuc_engine/model.py +33 -29
- package/python/ecuc_engine/paths.py +221 -0
- package/python/ecuc_engine/probe.py +229 -0
- package/python/ecuc_engine/rte_api.py +935 -0
- package/python/ecuc_engine/rules/__init__.py +5 -3
- package/python/ecuc_engine/rules/mem.py +84 -0
- package/python/ecuc_engine/rules/nvm.py +131 -0
- package/python/ecuc_engine/rules/pdur.py +87 -0
- package/python/ecuc_engine/rules/system.py +162 -0
- package/python/ecuc_engine/sysmodel.py +152 -0
- package/python/ecuc_engine/validate.py +52 -3
- package/python/ecuc_engine/verify_rte.py +445 -0
- package/python/pyproject.toml +1 -1
- package/skills/autosar/SKILL.md +57 -1
- package/skills/autosar/references/codegen.md +30 -0
- package/skills/autosar/references/project-layout.md +11 -2
- package/skills/autosar/references/runnable-code.md +50 -0
|
@@ -24,8 +24,23 @@ from typing import Optional
|
|
|
24
24
|
from .model import Elem, Project
|
|
25
25
|
from .values import parse_int
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
from . import paths
|
|
28
|
+
|
|
29
|
+
GENERATED_MARK = re.compile(r"generated by|automatically generated|auto-generated|this file (?:is|was) generated|do not edit",
|
|
30
|
+
re.IGNORECASE)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def is_generated(path: str) -> bool:
|
|
34
|
+
"""Generator output, recognised by the file itself (name pattern or header banner), not its folder."""
|
|
35
|
+
name = os.path.basename(path)
|
|
36
|
+
if re.search(r"(?:_(?:Cfg|PBcfg|Lcfg)\.[ch]|^Rte\w*\.c)$", name, re.IGNORECASE):
|
|
37
|
+
return True
|
|
38
|
+
try:
|
|
39
|
+
with open(path, "rb") as fh:
|
|
40
|
+
head = fh.read(2048).decode("utf-8", "replace")
|
|
41
|
+
except OSError:
|
|
42
|
+
return False
|
|
43
|
+
return bool(GENERATED_MARK.search(head))
|
|
29
44
|
|
|
30
45
|
|
|
31
46
|
@dataclass
|
|
@@ -54,6 +69,7 @@ CALL = re.compile(r"\b(Com_(?:Send|Receive)Signal(?:Group)?|NvM_(?:ReadBlock|Wri
|
|
|
54
69
|
r"Dem_(?:SetEventStatus|ReportErrorStatus)|PduR_\w*Transmit|Rte_(?:Write|Read)_\w+)\s*\(")
|
|
55
70
|
NUM = re.compile(r"^\(?\s*(?:\(\s*\w+\s*\)\s*)?(0[xX][0-9a-fA-F]+|\d+)[uUlL]*\s*\)?$")
|
|
56
71
|
SYM = re.compile(r"^(\w+?)Conf_(\w+?)_(\w+)$")
|
|
72
|
+
INSTANCE = re.compile(r"^(?:[A-Z]{1,3}\d{0,2}|\d{1,3}|[A-Za-z]?\d{1,3})$") # FL, RR, L, 2, 02, R1
|
|
57
73
|
NULLS = {"NULL_PTR", "NULL", "0", "((void*)0)", "(void*)0"}
|
|
58
74
|
TYPE_BITS = {"boolean": 8, "uint8": 8, "sint8": 8, "uint16": 16, "sint16": 16, "uint32": 32, "sint32": 32,
|
|
59
75
|
"uint64": 64, "sint64": 64, "float32": 32, "float64": 64, "char": 8, "int8_t": 8, "uint8_t": 8,
|
|
@@ -119,6 +135,23 @@ def split_args(src: str, open_idx: int):
|
|
|
119
135
|
return None, i
|
|
120
136
|
|
|
121
137
|
|
|
138
|
+
def iter_calls(unit: str, rx: re.Pattern = CALL):
|
|
139
|
+
"""The audit's call scanner: (name, offset, line, args) for each call matched by ``rx`` (group 1 = callee,
|
|
140
|
+
match ends at '(') in comment-stripped C, skipping #define bodies and declarations/prototypes."""
|
|
141
|
+
for m in rx.finditer(unit):
|
|
142
|
+
ls = unit.rfind("\n", 0, m.start()) + 1
|
|
143
|
+
if unit[ls:m.start()].lstrip().startswith("#"):
|
|
144
|
+
continue # macro definition, not a call
|
|
145
|
+
before = unit[ls:m.start()].rstrip()
|
|
146
|
+
prev = re.search(r"(\w+)\s*$", before)
|
|
147
|
+
if prev and prev.group(1) not in ("return", "void", "else", "do") and not before.endswith(")"):
|
|
148
|
+
continue # a declaration/prototype ("Std_ReturnType Com_SendSignal(")
|
|
149
|
+
args, _ = split_args(unit, m.end() - 1)
|
|
150
|
+
if args is None:
|
|
151
|
+
continue
|
|
152
|
+
yield m.group(1), m.start(), unit.count("\n", 0, m.start()) + 1, args
|
|
153
|
+
|
|
154
|
+
|
|
122
155
|
def parse_defines(files: list) -> dict:
|
|
123
156
|
"""NAME -> (params|None, body, file, line) for every #define in the given headers."""
|
|
124
157
|
out: dict = {}
|
|
@@ -144,19 +177,6 @@ def parse_defines(files: list) -> dict:
|
|
|
144
177
|
return out
|
|
145
178
|
|
|
146
179
|
|
|
147
|
-
def _files(root: str, paths: list, exts=C_EXT, skip=SKIP_DIRS) -> list:
|
|
148
|
-
res = []
|
|
149
|
-
for p in paths:
|
|
150
|
-
full = p if os.path.isabs(p) else os.path.join(root, p)
|
|
151
|
-
if os.path.isfile(full):
|
|
152
|
-
res.append(full)
|
|
153
|
-
continue
|
|
154
|
-
for dp, dns, fns in os.walk(full):
|
|
155
|
-
dns[:] = sorted(d for d in dns if d not in skip and not d.startswith("."))
|
|
156
|
-
res.extend(os.path.join(dp, fn) for fn in sorted(fns) if fn.endswith(exts))
|
|
157
|
-
return res
|
|
158
|
-
|
|
159
|
-
|
|
160
180
|
class Auditor:
|
|
161
181
|
def __init__(self, project: Project, defines: dict):
|
|
162
182
|
self.p = project
|
|
@@ -165,21 +185,32 @@ class Auditor:
|
|
|
165
185
|
self.unchecked: list = []
|
|
166
186
|
self.calls = 0
|
|
167
187
|
self._handles: dict = {}
|
|
188
|
+
self.referenced: set = set()
|
|
189
|
+
self.sibling_candidates: list = []
|
|
190
|
+
self.rte_written: dict = {}
|
|
191
|
+
self.tu_defines: dict = {}
|
|
192
|
+
self._aliases: set = set()
|
|
193
|
+
self._struct_cache: dict = {}
|
|
194
|
+
self.header_text = ""
|
|
168
195
|
|
|
169
196
|
# ---------- ECUC lookups ----------
|
|
170
197
|
def module(self, name: str) -> list:
|
|
171
198
|
return [m for m in self.p.modules if (m.def_ref or "").rsplit("/", 1)[-1] == name or m.name == name]
|
|
172
199
|
|
|
173
200
|
def containers(self, module: str, defnames: tuple) -> list:
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
stack
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
201
|
+
key = (module, tuple(defnames))
|
|
202
|
+
cache = self.__dict__.setdefault("_cont_cache", {})
|
|
203
|
+
if key not in cache:
|
|
204
|
+
out = []
|
|
205
|
+
for m in self.module(module):
|
|
206
|
+
stack = list(m.children)
|
|
207
|
+
while stack:
|
|
208
|
+
e = stack.pop()
|
|
209
|
+
stack.extend(e.children)
|
|
210
|
+
if (e.def_ref or "").rsplit("/", 1)[-1] in defnames:
|
|
211
|
+
out.append(e)
|
|
212
|
+
cache[key] = out
|
|
213
|
+
return cache[key]
|
|
183
214
|
|
|
184
215
|
def val(self, e: Elem, name: str) -> Optional[str]:
|
|
185
216
|
for g in self.p.find(e.path):
|
|
@@ -206,27 +237,28 @@ class Auditor:
|
|
|
206
237
|
mod, dname, short = m.groups()
|
|
207
238
|
if mod != api.module:
|
|
208
239
|
return None
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
return
|
|
240
|
+
idx = self.__dict__.setdefault("_sym_cache", {})
|
|
241
|
+
if (mod, dname) not in idx:
|
|
242
|
+
idx[(mod, dname)] = {e.name: e for e in self.containers(api.module, (dname,))}
|
|
243
|
+
return idx[(mod, dname)].get(short)
|
|
213
244
|
|
|
214
245
|
def symbol_of(self, e: Elem, api: Api) -> str:
|
|
215
246
|
return f"{api.module}Conf_{(e.def_ref or '').rsplit('/', 1)[-1]}_{e.name}"
|
|
216
247
|
|
|
217
248
|
def ipdus_of(self, sig: Elem) -> list:
|
|
218
249
|
"""I-PDUs that reference this signal (or, for a group signal, its signal group)."""
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
250
|
+
if "_owners" not in self.__dict__:
|
|
251
|
+
own: dict = {}
|
|
252
|
+
for ip in self.containers("Com", ("ComIPdu",)):
|
|
253
|
+
for g in self.p.find(ip.path):
|
|
254
|
+
for pv in g.params:
|
|
255
|
+
if pv.is_ref and pv.name in ("ComIPduSignalRef", "ComIPduSignalGroupRef") and pv.value:
|
|
256
|
+
lst = own.setdefault(pv.value.strip(), [])
|
|
257
|
+
if ip not in lst:
|
|
258
|
+
lst.append(ip)
|
|
259
|
+
self._owners = own
|
|
260
|
+
key = sig.parent.path if (sig.def_ref or "").endswith("/ComGroupSignal") and sig.parent is not None else sig.path
|
|
261
|
+
return list(self._owners.get(key, []))
|
|
230
262
|
|
|
231
263
|
# ---------- findings ----------
|
|
232
264
|
def add(self, rule, sev, f, line, call, msg, cite, evidence, fix=None):
|
|
@@ -246,7 +278,7 @@ class Auditor:
|
|
|
246
278
|
while e.startswith("(") and e.endswith(")") and split_args("x" + e, 1)[1] == len(e) + 1:
|
|
247
279
|
e = e[1:-1].strip()
|
|
248
280
|
if re.fullmatch(r"\w+", e):
|
|
249
|
-
d = self.defines.get(e)
|
|
281
|
+
d = self.tu_defines.get(e) or self.defines.get(e)
|
|
250
282
|
num = None
|
|
251
283
|
if d and d[0] is None:
|
|
252
284
|
mm = NUM.match(d[1])
|
|
@@ -280,10 +312,29 @@ class Auditor:
|
|
|
280
312
|
"survives handle re-assignment", f"{cite_sym}; TPS_ECUConfiguration 3.4.1.1",
|
|
281
313
|
{"path": target.path, api.handle_param: h[1]}, fix=f"replace {args[0]} with {sym}")
|
|
282
314
|
elif h[0] == "sym":
|
|
315
|
+
m = SYM.match(h[1])
|
|
316
|
+
if m and m.group(1) != api.module and self.module(m.group(1)):
|
|
317
|
+
other = next((e for e in self.containers(m.group(1), (m.group(2),)) if e.name == m.group(3)), None)
|
|
318
|
+
self.add("H2L-API-009", "error", f, line, call,
|
|
319
|
+
f"{h[1]} is a {m.group(1)} handle ({other.path if other else m.group(2)}) but {name} takes a "
|
|
320
|
+
f"{api.module} {'/'.join(api.defs)} handle ({api.handle_param}); the numeric value addresses an "
|
|
321
|
+
f"unrelated {api.module} PDU/object", f"{api.cite}; TPS_ECUC_02108 symbolic names are per module",
|
|
322
|
+
{"symbol": h[1], "belongsTo": other.path if other else None, "expects": f"{api.module}Conf_{api.defs[0]}_<name>"})
|
|
323
|
+
return
|
|
283
324
|
target = self.by_symbol(api, h[1])
|
|
325
|
+
if target is None and m and m.group(1) == api.module:
|
|
326
|
+
d = self.tu_defines.get(h[1]) or self.defines.get(h[1])
|
|
327
|
+
self.add("H2L-API-002", "error", f, line, call,
|
|
328
|
+
f"{h[1]} names no configured {m.group(2)} '{m.group(3)}' in {api.module}"
|
|
329
|
+
+ (f"; the generated define ({self.p.rel(d[2])}:{d[3]} = {h[2]}) is stale — regenerate and fix "
|
|
330
|
+
"the configuration or the call" if d else ""),
|
|
331
|
+
f"{api.cite}; TPS_ECUC_02108 symbolic name values",
|
|
332
|
+
{"symbol": h[1], "define": h[2], "defineAt": f"{self.p.rel(d[2])}:{d[3]}" if d else None})
|
|
333
|
+
return
|
|
284
334
|
if target is None and h[2] is not None:
|
|
285
335
|
hits = self.by_handle(api, h[2])
|
|
286
336
|
target = hits[0] if hits else None
|
|
337
|
+
self.check_alias(api, h[1], h[2], target)
|
|
287
338
|
if target is None:
|
|
288
339
|
if SYM.match(h[1]) or h[2] is not None:
|
|
289
340
|
self.add("H2L-API-002", "error", f, line, call,
|
|
@@ -296,12 +347,23 @@ class Auditor:
|
|
|
296
347
|
else:
|
|
297
348
|
self.unchecked.append({"file": self.p.rel(f), "line": line, "call": call, "reason": "handle is an expression"})
|
|
298
349
|
return
|
|
350
|
+
self.referenced.add(target.path)
|
|
351
|
+
if h[0] == "sym":
|
|
352
|
+
self.sibling_candidates.append((f, line, call, api, target, h[1], args, unit))
|
|
353
|
+
if api.kind == "send" and via is None and target.path in self.rte_written:
|
|
354
|
+
mac = self.rte_written[target.path]
|
|
355
|
+
self.add("H2L-API-010", "warning", f, line, call,
|
|
356
|
+
f"{target.name} is written directly through Com here, but it is also the RTE-mapped signal of "
|
|
357
|
+
f"{mac} (generated RTE): two writers for one signal, bypassing the RTE",
|
|
358
|
+
"SWS_Rte inter-ECU sender-receiver communication (one sender per data element); TPS_SystemTemplate "
|
|
359
|
+
"SenderReceiverToSignalMapping", {"signal": target.path, "rteApi": mac})
|
|
299
360
|
if api.kind in ("send", "receive"):
|
|
300
361
|
self.check_direction(f, line, call, api, target)
|
|
301
362
|
if api.data is not None and len(args) > api.data:
|
|
302
363
|
self.check_width(f, line, call, target, args[api.data], unit)
|
|
303
364
|
if api.kind == "nvm" and api.data is not None and len(args) > api.data:
|
|
304
365
|
self.check_nvm_null(f, line, call, name, target, args[api.data])
|
|
366
|
+
self.check_nvm_ram(f, line, call, name, target, args[api.data])
|
|
305
367
|
|
|
306
368
|
def audit_rte(self, f, line, name, args, unit):
|
|
307
369
|
d = self.defines.get(name)
|
|
@@ -332,29 +394,63 @@ class Auditor:
|
|
|
332
394
|
f"ECUC_Com_00493 ComIPduDirection; {api.cite}",
|
|
333
395
|
{"signal": sig.path, "ipdu": ip.path, "ComIPduDirection": d})
|
|
334
396
|
|
|
397
|
+
def struct_member(self, type_name: str, member: str, unit: str) -> Optional[tuple]:
|
|
398
|
+
key = (type_name, member)
|
|
399
|
+
if key in self._struct_cache:
|
|
400
|
+
return self._struct_cache[key]
|
|
401
|
+
res = None
|
|
402
|
+
for text in (unit, self.header_text):
|
|
403
|
+
m = re.search(rf"typedef\s+struct\s*\w*\s*\{{([^{{}}]*)\}}\s*{re.escape(type_name)}\s*;", text) or \
|
|
404
|
+
re.search(rf"struct\s+{re.escape(type_name)}\s*\{{([^{{}}]*)\}}", text)
|
|
405
|
+
if m:
|
|
406
|
+
mm = re.search(rf"\b(\w+)\s+{re.escape(member)}\s*(\[\s*(\w*)\s*\])?\s*;", m.group(1))
|
|
407
|
+
if mm:
|
|
408
|
+
res = (mm.group(1), mm.group(3) if mm.group(2) else None)
|
|
409
|
+
break
|
|
410
|
+
self._struct_cache[key] = res
|
|
411
|
+
return res
|
|
412
|
+
|
|
413
|
+
def decl(self, var: str, unit: str):
|
|
414
|
+
m = re.search(rf"\b(?:(?:static|volatile|const|extern)\s+)*(\w+)\s*(\*?)\s*(?:const\s+)?{re.escape(var)}"
|
|
415
|
+
rf"\s*(\[\s*(\w*)\s*\])?\s*[;=,)]", unit)
|
|
416
|
+
if not m or m.group(1) in ("return", "else", "case", "goto", "sizeof", "struct", "union", "enum"):
|
|
417
|
+
return None
|
|
418
|
+
return m.group(1), bool(m.group(2)), bool(m.group(3)), m.group(4), unit.count("\n", 0, m.start()) + 1
|
|
419
|
+
|
|
335
420
|
def c_type(self, expr: str, unit: str):
|
|
336
421
|
"""Type of the object a data-pointer argument points to -> (type, array_len|None), or None if unknown."""
|
|
422
|
+
self.last_decl_line = None
|
|
337
423
|
e = expr.strip()
|
|
338
424
|
while e.startswith("(") and e.endswith(")") and split_args("x" + e, 1)[1] == len(e) + 1:
|
|
339
425
|
e = e[1:-1].strip()
|
|
340
426
|
lit = re.fullmatch(r"&\s*\(\s*(\w+)\s*\)\s*\{.*\}", e, re.S) # &(uint8){ x }
|
|
341
427
|
if lit:
|
|
342
428
|
return lit.group(1), None
|
|
429
|
+
mem = re.fullmatch(r"&\s*\(?\s*(\w+)\s*(\.|->)\s*(\w+)\s*\)?", e) # &var.member / &ptr->member
|
|
430
|
+
if mem:
|
|
431
|
+
d = self.decl(mem.group(1), unit)
|
|
432
|
+
if d is None or (mem.group(2) == "->") != d[1]:
|
|
433
|
+
return None
|
|
434
|
+
t = self.struct_member(d[0], mem.group(3), unit)
|
|
435
|
+
if t is None:
|
|
436
|
+
return None
|
|
437
|
+
self.last_decl_line = d[4]
|
|
438
|
+
return t
|
|
343
439
|
ref = re.fullmatch(r"&\s*(\w+)", e) # &var
|
|
344
440
|
elem = re.fullmatch(r"&\s*(\w+)\s*\[[^\]]*\]", e) # &buf[i]
|
|
345
441
|
bare = re.fullmatch(r"(\w+)", e) # buf (array decays) or a pointer variable
|
|
346
442
|
var = (ref or elem or bare).group(1) if (ref or elem or bare) else None
|
|
347
443
|
if var is None:
|
|
348
444
|
return None
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
if not decl or decl.group(1) in ("return", "else", "case", "goto", "sizeof", "struct", "union", "enum"):
|
|
445
|
+
d = self.decl(var, unit)
|
|
446
|
+
if d is None or d[1]:
|
|
352
447
|
return None
|
|
353
|
-
is_array =
|
|
448
|
+
typ, _ptr, is_array, alen, dline = d
|
|
449
|
+
self.last_decl_line = dline
|
|
354
450
|
if ref:
|
|
355
|
-
return
|
|
451
|
+
return typ, (alen if is_array else None)
|
|
356
452
|
if elem or (bare and is_array):
|
|
357
|
-
return
|
|
453
|
+
return typ, None
|
|
358
454
|
return None # bare non-array variable: a pointer whose pointee is not visible here
|
|
359
455
|
|
|
360
456
|
def check_width(self, f, line, call, sig, arg, unit):
|
|
@@ -377,9 +473,136 @@ class Auditor:
|
|
|
377
473
|
self.add("H2L-API-004", "error", f, line, call,
|
|
378
474
|
f"{arg} is a {ct[0]}{'[' + ct[1] + ']' if ct[1] else ''} ({cbits} bits) but {sig.name} is "
|
|
379
475
|
f"ComSignalType={typ} ({expect // 8}-byte object, ComBitSize={bs}); Com accesses {expect // 8} "
|
|
380
|
-
f"byte(s) at that address —
|
|
476
|
+
f"byte(s) at that address — " + ("Com writes past the object (overflow)" if "Receive" in call else
|
|
477
|
+
"wrong signal or wrong variable") + (f"; object declared at line {self.last_decl_line}" if self.last_decl_line else ""),
|
|
381
478
|
"SWS_Com_00197/00198 SignalDataPtr points to an object of the ComSignalType; ECUC_Com_00127 ComSignalType",
|
|
382
|
-
{"signal": sig.path, "ComSignalType": typ, "ComBitSize": bs, "cType": ct[0]
|
|
479
|
+
{"signal": sig.path, "ComSignalType": typ, "ComBitSize": bs, "cType": ct[0],
|
|
480
|
+
"declLine": self.last_decl_line})
|
|
481
|
+
|
|
482
|
+
def check_alias(self, api, sym, value, target):
|
|
483
|
+
"""A hand-written #define standing in for a generated handle (H2L-API-007), reported at the #define."""
|
|
484
|
+
d = self.tu_defines.get(sym) or self.defines.get(sym)
|
|
485
|
+
if d is None or sym in self._aliases:
|
|
486
|
+
return
|
|
487
|
+
self._aliases.add(sym)
|
|
488
|
+
dfile, dline = d[2], d[3]
|
|
489
|
+
try:
|
|
490
|
+
raw = open(dfile, encoding="utf-8", errors="replace").read().split("\n")[dline - 1]
|
|
491
|
+
except (OSError, IndexError):
|
|
492
|
+
raw = ""
|
|
493
|
+
named = re.search(r"\b(\w+Conf_\w+?_\w+)\b", raw.split("define", 1)[-1].split(sym, 1)[-1])
|
|
494
|
+
call = f"#define {sym} {d[1]}"
|
|
495
|
+
if named:
|
|
496
|
+
meant = self.by_symbol(api, named.group(1))
|
|
497
|
+
mv = parse_int(self.val(meant, api.handle_param) or "") if meant is not None else None
|
|
498
|
+
if meant is not None and mv != value:
|
|
499
|
+
self.add("H2L-API-007", "error", dfile, dline, call,
|
|
500
|
+
f"stale handle alias: {sym} = {value} is commented as {named.group(1)}, whose {api.handle_param} is "
|
|
501
|
+
f"{mv}; {value} addresses {target.path if target else 'no configured element'}",
|
|
502
|
+
"TPS_ECUC_02108 symbolic name values; TPS_ECUConfiguration 3.4.1.1",
|
|
503
|
+
{"alias": sym, "value": value, "meant": meant.path, api.handle_param: mv,
|
|
504
|
+
"actual": target.path if target else None}, fix=f"use {named.group(1)} directly")
|
|
505
|
+
return
|
|
506
|
+
self.add("H2L-API-007", "warning", dfile, dline, call,
|
|
507
|
+
f"hand-coded handle macro {sym} = {value} ({target.path if target else 'matches nothing'}); it breaks "
|
|
508
|
+
"silently when the generator re-assigns handles",
|
|
509
|
+
"TPS_ECUC_02108 symbolic name values", {"alias": sym, "value": value, "path": target.path if target else None},
|
|
510
|
+
fix=f"use {self.symbol_of(target, api)}" if target else None)
|
|
511
|
+
|
|
512
|
+
def check_nvm_ram(self, f, line, call, name, blk, arg):
|
|
513
|
+
"""Data pointer is the permanent RAM block of a *different* NvM block (H2L-API-008)."""
|
|
514
|
+
m = re.fullmatch(r"\(?\s*&?\s*(\w+)\s*\)?", arg.strip())
|
|
515
|
+
if not m or arg.replace(" ", "") in NULLS:
|
|
516
|
+
return
|
|
517
|
+
sym = m.group(1)
|
|
518
|
+
own = (self.val(blk, "NvMRamBlockDataAddress") or "").strip()
|
|
519
|
+
if re.search(rf"\b{re.escape(sym)}\b", own):
|
|
520
|
+
return
|
|
521
|
+
for other in self.containers("NvM", ("NvMBlockDescriptor",)):
|
|
522
|
+
if other.path == blk.path:
|
|
523
|
+
continue
|
|
524
|
+
addr = (self.val(other, "NvMRamBlockDataAddress") or "").strip()
|
|
525
|
+
if addr and re.search(rf"(?:^|[^\w]){re.escape(sym)}\b", addr):
|
|
526
|
+
self.add("H2L-API-008", "error", f, line, call,
|
|
527
|
+
f"{name} on {blk.name} is given {arg}, the permanent RAM block of {other.name} "
|
|
528
|
+
f"(NvMRamBlockDataAddress={addr}); {blk.name}'s own RAM block is {own or '(none)'} — wrong data is "
|
|
529
|
+
f"stored/overwritten", "SWS_NvM_00454/00455 NvM_DstPtr/NvM_SrcPtr; ECUC_NvM_00482 NvMRamBlockDataAddress",
|
|
530
|
+
{"block": blk.path, "ownRam": own or None, "givenRamOf": other.path,
|
|
531
|
+
"NvMNvBlockLength": self.val(blk, "NvMNvBlockLength"),
|
|
532
|
+
"otherLength": self.val(other, "NvMNvBlockLength")},
|
|
533
|
+
fix=f"pass {own}" if own else None)
|
|
534
|
+
return
|
|
535
|
+
|
|
536
|
+
def check_siblings(self):
|
|
537
|
+
"""Handle whose instance token (FL/FR/2/...) disagrees with the call's context, while the sibling with the
|
|
538
|
+
context's token is configured and never referenced (H2L-API-006)."""
|
|
539
|
+
uses_by: dict = {}
|
|
540
|
+
for c in self.sibling_candidates:
|
|
541
|
+
uses_by[(c[0], c[4].path)] = uses_by.get((c[0], c[4].path), 0) + 1
|
|
542
|
+
sib_index: dict = {}
|
|
543
|
+
for f, line, call, api, target, sym, args, unit in self.sibling_candidates:
|
|
544
|
+
if uses_by[(f, target.path)] < 2:
|
|
545
|
+
continue
|
|
546
|
+
dname = (target.def_ref or "").rsplit("/", 1)[-1]
|
|
547
|
+
mine = target.name.split("_")
|
|
548
|
+
key = (api.module, dname)
|
|
549
|
+
if key not in sib_index: # pattern with one part blanked -> containers
|
|
550
|
+
idx: dict = {}
|
|
551
|
+
for e in self.containers(api.module, (dname,)):
|
|
552
|
+
parts = e.name.split("_")
|
|
553
|
+
for i in range(len(parts)):
|
|
554
|
+
idx.setdefault((len(parts), i, tuple(parts[:i] + parts[i + 1:])), []).append((e, parts[i]))
|
|
555
|
+
sib_index[key] = idx
|
|
556
|
+
sibs = []
|
|
557
|
+
for i in range(len(mine)):
|
|
558
|
+
for e, tok in sib_index[key].get((len(mine), i, tuple(mine[:i] + mine[i + 1:])), []):
|
|
559
|
+
if e.path != target.path:
|
|
560
|
+
sibs.append((e, mine[i], tok))
|
|
561
|
+
if not sibs:
|
|
562
|
+
continue
|
|
563
|
+
ctx_text = " ".join(args[1:])
|
|
564
|
+
fn = self._function_at(unit, line)
|
|
565
|
+
tokens = set(re.split(r"[^A-Za-z0-9]+", ctx_text + " " + fn)) | \
|
|
566
|
+
{t for ident in re.findall(r"\w+", ctx_text + " " + fn) for t in ident.split("_")}
|
|
567
|
+
uses = uses_by[(f, target.path)] # copy-paste signature: the same handle is used again in this file
|
|
568
|
+
for e, my_tok, sib_tok in sibs:
|
|
569
|
+
if not (INSTANCE.match(my_tok) and INSTANCE.match(sib_tok)):
|
|
570
|
+
continue
|
|
571
|
+
if sib_tok in tokens and my_tok not in tokens and e.path not in self.referenced:
|
|
572
|
+
self.add("H2L-API-006", "warning", f, line, call,
|
|
573
|
+
f"{sym} uses instance '{my_tok}' (and is used {uses}x in this file) but this call's context "
|
|
574
|
+
f"refers to '{sib_tok}' "
|
|
575
|
+
f"({ctx_text[:80].strip() or fn}); {e.name} (the '{sib_tok}' sibling) is configured but never "
|
|
576
|
+
"referenced in the code — likely copy-paste of the wrong handle",
|
|
577
|
+
"heuristic: sibling handle mismatch (instance token)",
|
|
578
|
+
{"handle": target.path, "sibling": e.path, "contextToken": sib_tok, "siblingReferenced": False},
|
|
579
|
+
fix=f"use {self.symbol_of(e, api)}")
|
|
580
|
+
break
|
|
581
|
+
|
|
582
|
+
@staticmethod
|
|
583
|
+
def _function_at(unit: str, line: int) -> str:
|
|
584
|
+
head = "\n".join(unit.split("\n")[:line])
|
|
585
|
+
m = list(re.finditer(r"^\s*(?:static\s+)?[\w\s\*]+?\b(\w+)\s*\([^;{]*\)\s*\{", head, re.M))
|
|
586
|
+
return m[-1].group(1) if m else ""
|
|
587
|
+
|
|
588
|
+
def unreferenced(self) -> dict:
|
|
589
|
+
out = {}
|
|
590
|
+
dem = [e for e in self.containers("Dem", ("DemEventParameter",)) if e.path not in self.referenced
|
|
591
|
+
and any(pv.is_ref and pv.name == "DemDTCRef" for g in self.p.find(e.path) for pv in g.params)]
|
|
592
|
+
tx = []
|
|
593
|
+
for ip in self.containers("Com", ("ComIPdu",)):
|
|
594
|
+
if (self.val(ip, "ComIPduDirection") or "").strip() != "SEND":
|
|
595
|
+
continue
|
|
596
|
+
for g in self.p.find(ip.path):
|
|
597
|
+
for pv in g.params:
|
|
598
|
+
if pv.name == "ComIPduSignalRef" and pv.value and pv.value.strip() not in self.referenced:
|
|
599
|
+
tx.append(pv.value.strip())
|
|
600
|
+
nvm = [e for e in self.containers("NvM", ("NvMBlockDescriptor",)) if e.path not in self.referenced]
|
|
601
|
+
for k, items in (("demEventsWithDtc", [e.path for e in dem]), ("txSignals", sorted(set(tx))),
|
|
602
|
+
("nvmBlocks", [e.path for e in nvm])):
|
|
603
|
+
if items:
|
|
604
|
+
out[k] = {"count": len(items), "sample": items[:10]}
|
|
605
|
+
return out
|
|
383
606
|
|
|
384
607
|
def check_nvm_null(self, f, line, call, name, blk, arg):
|
|
385
608
|
if arg.replace(" ", "") not in NULLS:
|
|
@@ -395,7 +618,7 @@ class Auditor:
|
|
|
395
618
|
{"block": blk.path, "NvMRamBlockDataAddress": None, "NvMBlockUseSyncMechanism": sync})
|
|
396
619
|
|
|
397
620
|
|
|
398
|
-
def audit(project: Project,
|
|
621
|
+
def audit(project: Project, paths_arg: Optional[list] = None, headers_arg: Optional[list] = None) -> dict:
|
|
399
622
|
root = project.root
|
|
400
623
|
cfg = {}
|
|
401
624
|
try:
|
|
@@ -405,34 +628,109 @@ def audit(project: Project, paths: Optional[list] = None, headers: Optional[list
|
|
|
405
628
|
cfg = raw if isinstance(raw, dict) else ({"paths": raw} if isinstance(raw, list) else {})
|
|
406
629
|
except (OSError, ValueError):
|
|
407
630
|
pass
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
631
|
+
explicit = paths_arg or cfg.get("paths")
|
|
632
|
+
notes: list = []
|
|
633
|
+
tree = paths.walk(root, paths.is_c)
|
|
634
|
+
notes.extend(tree.notes(root))
|
|
635
|
+
if explicit:
|
|
636
|
+
scan = paths.expand(root, explicit, paths.is_c)
|
|
637
|
+
for p in paths.patterns(root, explicit):
|
|
638
|
+
if not p.expand(paths.is_c):
|
|
639
|
+
notes.append(f"audit path {p.raw!r} matched no C file under {root}")
|
|
640
|
+
sources = [f for f in scan if f.lower().endswith(".c")]
|
|
641
|
+
generated = []
|
|
642
|
+
else: # whole tree, minus generator output (recognised by the file, not the folder)
|
|
643
|
+
cfiles = [f for f in tree.files if f.lower().endswith(".c")]
|
|
644
|
+
generated = [f for f in cfiles if is_generated(f)]
|
|
645
|
+
gen = set(generated)
|
|
646
|
+
sources = [f for f in cfiles if f not in gen]
|
|
647
|
+
extra = paths.expand(root, headers_arg or cfg.get("headers") or [], paths.is_c)
|
|
648
|
+
hdrs = sorted({f for f in tree.files + extra if f.lower().endswith(".h")})
|
|
412
649
|
a = Auditor(project, parse_defines(hdrs))
|
|
650
|
+
try:
|
|
651
|
+
a.header_text = strip_comments("\n".join(open(h, encoding="utf-8", errors="replace").read() for h in hdrs[:2000]))
|
|
652
|
+
except OSError:
|
|
653
|
+
a.header_text = ""
|
|
654
|
+
for mac, (params, body, _f, _l) in a.defines.items():
|
|
655
|
+
if mac.startswith("Rte_Write_") and params is not None:
|
|
656
|
+
mm = re.search(r"Com_SendSignal\s*\(\s*(\w+)", body)
|
|
657
|
+
if mm:
|
|
658
|
+
t = a.by_symbol(APIS["Com_SendSignal"], mm.group(1))
|
|
659
|
+
if t is not None:
|
|
660
|
+
a.rte_written.setdefault(t.path, mac)
|
|
413
661
|
for f in sources:
|
|
414
662
|
try:
|
|
415
663
|
unit = strip_comments(open(f, encoding="utf-8", errors="replace").read())
|
|
416
664
|
except OSError:
|
|
417
665
|
continue
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
before = unit[ls:m.start()].rstrip()
|
|
423
|
-
prev = re.search(r"(\w+)\s*$", before)
|
|
424
|
-
if prev and prev.group(1) not in ("return", "void") and not before.endswith(")"):
|
|
425
|
-
continue # a declaration/prototype ("Std_ReturnType Com_SendSignal(")
|
|
426
|
-
args, _ = split_args(unit, m.end() - 1)
|
|
427
|
-
if args is None:
|
|
428
|
-
continue
|
|
429
|
-
a.audit_call(f, unit.count("\n", 0, m.start()) + 1, m.group(1), args, unit)
|
|
666
|
+
a.tu_defines = parse_defines([f])
|
|
667
|
+
for name, start, line, args in iter_calls(unit, CALL):
|
|
668
|
+
a.audit_call(f, line, name, args, unit)
|
|
669
|
+
a.check_siblings()
|
|
430
670
|
a.findings.sort(key=lambda d: (d["file"], d["line"], d["rule"]))
|
|
431
671
|
summary: dict = {}
|
|
432
672
|
for d in a.findings:
|
|
433
673
|
summary[d["severity"]] = summary.get(d["severity"], 0) + 1
|
|
434
|
-
|
|
435
|
-
|
|
674
|
+
res = {"scanned": {"sources": [project.rel(f) for f in sources], "headers": len(hdrs), "calls": a.calls,
|
|
675
|
+
"generatedSkipped": len(generated)},
|
|
676
|
+
"summary": summary, "findings": a.findings, "unchecked": a.unchecked,
|
|
677
|
+
"unreferenced": a.unreferenced()}
|
|
678
|
+
if notes:
|
|
679
|
+
res["notes"] = notes
|
|
680
|
+
if explicit and not sources:
|
|
681
|
+
res["error"] = (f"audit paths {list(explicit)} matched no .c file under {root}; nothing was audited. "
|
|
682
|
+
"Pass files, directories or globs relative to the project root, or omit paths.")
|
|
683
|
+
return res
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def can_info(project: Project, pdu_path: Optional[str]) -> list:
|
|
687
|
+
"""EcuC Pdu of a Com I-PDU -> CanIf Tx/Rx PDU(s) with CAN ID, controller and network.
|
|
688
|
+
|
|
689
|
+
Linked generically: the CanIf PDU's EcuC Pdu is the same Pdu, or the other end of a PduR routing path,
|
|
690
|
+
or stands for the same system I-PDU in the extract.
|
|
691
|
+
"""
|
|
692
|
+
if not pdu_path:
|
|
693
|
+
return []
|
|
694
|
+
from .rules import RuleContext
|
|
695
|
+
from .rules.system import channels, system_ipdus
|
|
696
|
+
ctx = RuleContext(project)
|
|
697
|
+
pdu = project.find(pdu_path)
|
|
698
|
+
if not pdu:
|
|
699
|
+
return []
|
|
700
|
+
pdu = pdu[0]
|
|
701
|
+
linked = {pdu.path}
|
|
702
|
+
for rp in ctx.containers("PduR/PduRRoutingPaths/PduRRoutingPath"):
|
|
703
|
+
ends = [ctx.target(ctx.value(rp, "PduRSrcPduRRef"))] + [ctx.target(pv) for pv in ctx.values(rp, "PduRDestPduRRef")]
|
|
704
|
+
pdus = {ctx.target(ctx.value(e, n)).path for e in ends if e is not None
|
|
705
|
+
for n in ("PduRSrcPduRef", "PduRDestPduRef") if ctx.target(ctx.value(e, n)) is not None}
|
|
706
|
+
if pdu.path in pdus:
|
|
707
|
+
linked |= pdus
|
|
708
|
+
sys_ip = set(system_ipdus(ctx, pdu))
|
|
709
|
+
out = []
|
|
710
|
+
for kind, ref, cid, typ in (("Tx", "CanIfTxPduRef", "CanIfTxPduCanId", "CanIfTxPduCanIdType"),
|
|
711
|
+
("Rx", "CanIfRxPduRef", "CanIfRxPduCanId", "CanIfRxPduCanIdType")):
|
|
712
|
+
for cp in ctx.containers(f"CanIf/CanIfInitCfg/CanIf{kind}PduCfg"):
|
|
713
|
+
tgt = ctx.target(ctx.value(cp, ref))
|
|
714
|
+
if tgt is None:
|
|
715
|
+
continue
|
|
716
|
+
if tgt.path not in linked and not (sys_ip and sys_ip & set(system_ipdus(ctx, tgt))):
|
|
717
|
+
continue
|
|
718
|
+
can_id = parse_int(ctx.text(cp, cid) or "")
|
|
719
|
+
hoh = None
|
|
720
|
+
if kind == "Tx":
|
|
721
|
+
buf = ctx.target(ctx.value(cp, "CanIfTxPduBufferRef"))
|
|
722
|
+
hoh = ctx.target(ctx.value(buf, "CanIfBufferHthRef")) if buf is not None else None
|
|
723
|
+
ctrl = ctx.target(ctx.value(hoh, "CanIfHthCanCtrlIdRef")) if hoh is not None else None
|
|
724
|
+
else:
|
|
725
|
+
hoh = ctx.target(ctx.value(cp, "CanIfRxPduHrhIdRef"))
|
|
726
|
+
ctrl = ctx.target(ctx.value(hoh, "CanIfHrhCanCtrlIdRef")) if hoh is not None else None
|
|
727
|
+
out.append({"canIfPdu": cp.path, "direction": kind, "canId": can_id,
|
|
728
|
+
"canIdHex": hex(can_id) if can_id is not None else None,
|
|
729
|
+
"canIdType": ctx.text(cp, typ), "at": f"{project.rel(cp.file)}:{cp.line}",
|
|
730
|
+
"controller": ctrl.path if ctrl is not None else None,
|
|
731
|
+
"canController": (ctx.text(ctrl, "CanIfCtrlCanCtrlRef") or None) if ctrl is not None else None,
|
|
732
|
+
"network": channels(ctx, tgt) or channels(ctx, pdu) or None})
|
|
733
|
+
return out
|
|
436
734
|
|
|
437
735
|
|
|
438
736
|
def signal_owners(project: Project, paths: list) -> dict:
|
|
@@ -445,8 +743,13 @@ def signal_owners(project: Project, paths: list) -> dict:
|
|
|
445
743
|
out[p] = None # not in the local checkout
|
|
446
744
|
continue
|
|
447
745
|
sig = g[0]
|
|
448
|
-
if
|
|
746
|
+
if (sig.def_ref or "").endswith("/ComIPdu"):
|
|
747
|
+
ipdus = [sig]
|
|
748
|
+
elif (sig.def_ref or "").endswith(("/ComSignal", "/ComGroupSignal", "/ComSignalGroup")):
|
|
749
|
+
ipdus = a.ipdus_of(sig)
|
|
750
|
+
else:
|
|
449
751
|
continue
|
|
450
752
|
out[p] = [{"ipdu": ip.path, "direction": (a.val(ip, "ComIPduDirection") or "").strip() or None,
|
|
451
|
-
"pdu": (a.val(ip, "ComPduIdRef") or "").strip() or None
|
|
753
|
+
"pdu": (a.val(ip, "ComPduIdRef") or "").strip() or None,
|
|
754
|
+
"can": can_info(project, (a.val(ip, "ComPduIdRef") or "").strip() or None)} for ip in ipdus]
|
|
452
755
|
return out
|