@h2loop/autosar 1.0.1 → 1.2.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.
Files changed (53) hide show
  1. package/README.md +11 -1
  2. package/agents/autosar-integrator.md +45 -2
  3. package/commands/autosar-trace.md +2 -2
  4. package/package.json +4 -1
  5. package/plugins/index.js +55 -23
  6. package/python/ecuc_engine/__init__.py +1 -1
  7. package/python/ecuc_engine/add_signal.py +351 -0
  8. package/python/ecuc_engine/arxml_edit.py +387 -0
  9. package/python/ecuc_engine/audit.py +352 -50
  10. package/python/ecuc_engine/cli.py +125 -5
  11. package/python/ecuc_engine/codegen/__init__.py +233 -0
  12. package/python/ecuc_engine/codegen/binding.py +305 -0
  13. package/python/ecuc_engine/codegen/bindings/sip_standin.json +378 -0
  14. package/python/ecuc_engine/codegen/cdiff.py +395 -0
  15. package/python/ecuc_engine/codegen/cwriter.py +150 -0
  16. package/python/ecuc_engine/codegen/emit.py +463 -0
  17. package/python/ecuc_engine/codegen/handles.py +176 -0
  18. package/python/ecuc_engine/codegen/mcal/tc4d.json +60 -0
  19. package/python/ecuc_engine/codegen/model.py +264 -0
  20. package/python/ecuc_engine/codegen/modules/__init__.py +7 -0
  21. package/python/ecuc_engine/codegen/modules/com.py +145 -0
  22. package/python/ecuc_engine/codegen/modules/comstack.py +14 -0
  23. package/python/ecuc_engine/codegen/modules/dem.py +22 -0
  24. package/python/ecuc_engine/codegen/modules/mcal.py +170 -0
  25. package/python/ecuc_engine/codegen/modules/memstack.py +24 -0
  26. package/python/ecuc_engine/codegen/registry.py +208 -0
  27. package/python/ecuc_engine/codegen/schm_memmap.py +159 -0
  28. package/python/ecuc_engine/context.py +481 -0
  29. package/python/ecuc_engine/csrc.py +189 -0
  30. package/python/ecuc_engine/defs.py +30 -0
  31. package/python/ecuc_engine/ecucjson.py +75 -22
  32. package/python/ecuc_engine/find.py +164 -0
  33. package/python/ecuc_engine/gen.py +105 -0
  34. package/python/ecuc_engine/history.py +146 -0
  35. package/python/ecuc_engine/model.py +4 -1
  36. package/python/ecuc_engine/probe.py +229 -0
  37. package/python/ecuc_engine/rte_api.py +935 -0
  38. package/python/ecuc_engine/rules/__init__.py +5 -3
  39. package/python/ecuc_engine/rules/mem.py +84 -0
  40. package/python/ecuc_engine/rules/nvm.py +131 -0
  41. package/python/ecuc_engine/rules/pdur.py +87 -0
  42. package/python/ecuc_engine/rules/system.py +162 -0
  43. package/python/ecuc_engine/specids.py +163 -0
  44. package/python/ecuc_engine/sysmodel.py +152 -0
  45. package/python/ecuc_engine/validate.py +15 -2
  46. package/python/ecuc_engine/values.py +26 -0
  47. package/python/ecuc_engine/verify_rte.py +445 -0
  48. package/python/pyproject.toml +1 -1
  49. package/skills/autosar/SKILL.md +78 -6
  50. package/skills/autosar/references/codegen.md +58 -0
  51. package/skills/autosar/references/ecuc-model.md +4 -1
  52. package/skills/autosar/references/project-layout.md +10 -1
  53. package/skills/autosar/references/runnable-code.md +50 -0
@@ -22,7 +22,7 @@ from dataclasses import dataclass
22
22
  from typing import Optional
23
23
 
24
24
  from .model import Elem, Project
25
- from .values import parse_int
25
+ from .values import hex_str, parse_int
26
26
 
27
27
  from . import paths
28
28
 
@@ -69,6 +69,7 @@ CALL = re.compile(r"\b(Com_(?:Send|Receive)Signal(?:Group)?|NvM_(?:ReadBlock|Wri
69
69
  r"Dem_(?:SetEventStatus|ReportErrorStatus)|PduR_\w*Transmit|Rte_(?:Write|Read)_\w+)\s*\(")
70
70
  NUM = re.compile(r"^\(?\s*(?:\(\s*\w+\s*\)\s*)?(0[xX][0-9a-fA-F]+|\d+)[uUlL]*\s*\)?$")
71
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
72
73
  NULLS = {"NULL_PTR", "NULL", "0", "((void*)0)", "(void*)0"}
73
74
  TYPE_BITS = {"boolean": 8, "uint8": 8, "sint8": 8, "uint16": 16, "sint16": 16, "uint32": 32, "sint32": 32,
74
75
  "uint64": 64, "sint64": 64, "float32": 32, "float64": 64, "char": 8, "int8_t": 8, "uint8_t": 8,
@@ -134,6 +135,23 @@ def split_args(src: str, open_idx: int):
134
135
  return None, i
135
136
 
136
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
+
137
155
  def parse_defines(files: list) -> dict:
138
156
  """NAME -> (params|None, body, file, line) for every #define in the given headers."""
139
157
  out: dict = {}
@@ -167,21 +185,32 @@ class Auditor:
167
185
  self.unchecked: list = []
168
186
  self.calls = 0
169
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 = ""
170
195
 
171
196
  # ---------- ECUC lookups ----------
172
197
  def module(self, name: str) -> list:
173
198
  return [m for m in self.p.modules if (m.def_ref or "").rsplit("/", 1)[-1] == name or m.name == name]
174
199
 
175
200
  def containers(self, module: str, defnames: tuple) -> list:
176
- out = []
177
- for m in self.module(module):
178
- stack = list(m.children)
179
- while stack:
180
- e = stack.pop()
181
- stack.extend(e.children)
182
- if (e.def_ref or "").rsplit("/", 1)[-1] in defnames:
183
- out.append(e)
184
- return out
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]
185
214
 
186
215
  def val(self, e: Elem, name: str) -> Optional[str]:
187
216
  for g in self.p.find(e.path):
@@ -208,32 +237,55 @@ class Auditor:
208
237
  mod, dname, short = m.groups()
209
238
  if mod != api.module:
210
239
  return None
211
- for e in self.containers(api.module, (dname,)):
212
- if e.name == short:
213
- return e
214
- return None
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)
215
244
 
216
245
  def symbol_of(self, e: Elem, api: Api) -> str:
217
246
  return f"{api.module}Conf_{(e.def_ref or '').rsplit('/', 1)[-1]}_{e.name}"
218
247
 
219
248
  def ipdus_of(self, sig: Elem) -> list:
220
249
  """I-PDUs that reference this signal (or, for a group signal, its signal group)."""
221
- targets = {sig.path}
222
- if (sig.def_ref or "").endswith("/ComGroupSignal") and sig.parent is not None:
223
- targets = {sig.parent.path}
224
- out = []
225
- for ip in self.containers("Com", ("ComIPdu",)):
226
- for g in self.p.find(ip.path):
227
- if any(pv.is_ref and pv.name in ("ComIPduSignalRef", "ComIPduSignalGroupRef") and (pv.value or "").strip() in targets
228
- for pv in g.params):
229
- out.append(ip)
230
- break
231
- return out
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, []))
232
262
 
233
263
  # ---------- findings ----------
264
+ def located(self, evidence):
265
+ """Evidence plus, computed (never left to the reader): <k>At = file:line of each configured element named
266
+ by AUTOSAR path, <Param>At = file:line of that parameter's VALUE element, and <k>Hex for integers."""
267
+ if not isinstance(evidence, dict):
268
+ return evidence
269
+ from .ecucjson import value_at
270
+ out = dict(evidence)
271
+ elems = []
272
+ for k, v in evidence.items():
273
+ if isinstance(v, str) and v.startswith("/"):
274
+ g = self.p.find(v)
275
+ if g:
276
+ out[f"{k}At"] = ", ".join(f"{self.p.rel(e.file)}:{e.line}" for e in g)
277
+ elems.extend(g)
278
+ for k, v in evidence.items():
279
+ if isinstance(v, int) and not isinstance(v, bool) and not k.lower().endswith("line"):
280
+ out[f"{k}Hex"] = hex_str(v)
281
+ sites = [value_at(self.p, e, pv) for e in elems for pv in e.params if pv.name == k]
282
+ if sites and f"{k}At" not in out:
283
+ out[f"{k}At"] = sites[0] if len(sites) == 1 else sites
284
+ return out
285
+
234
286
  def add(self, rule, sev, f, line, call, msg, cite, evidence, fix=None):
235
287
  d = {"rule": rule, "severity": sev, "file": self.p.rel(f), "line": line, "call": call,
236
- "message": msg, "cite": cite, "evidence": evidence}
288
+ "message": msg, "cite": cite, "evidence": self.located(evidence)}
237
289
  if fix:
238
290
  d["fix"] = fix
239
291
  self.findings.append(d)
@@ -248,7 +300,7 @@ class Auditor:
248
300
  while e.startswith("(") and e.endswith(")") and split_args("x" + e, 1)[1] == len(e) + 1:
249
301
  e = e[1:-1].strip()
250
302
  if re.fullmatch(r"\w+", e):
251
- d = self.defines.get(e)
303
+ d = self.tu_defines.get(e) or self.defines.get(e)
252
304
  num = None
253
305
  if d and d[0] is None:
254
306
  mm = NUM.match(d[1])
@@ -282,10 +334,29 @@ class Auditor:
282
334
  "survives handle re-assignment", f"{cite_sym}; TPS_ECUConfiguration 3.4.1.1",
283
335
  {"path": target.path, api.handle_param: h[1]}, fix=f"replace {args[0]} with {sym}")
284
336
  elif h[0] == "sym":
337
+ m = SYM.match(h[1])
338
+ if m and m.group(1) != api.module and self.module(m.group(1)):
339
+ other = next((e for e in self.containers(m.group(1), (m.group(2),)) if e.name == m.group(3)), None)
340
+ self.add("H2L-API-009", "error", f, line, call,
341
+ f"{h[1]} is a {m.group(1)} handle ({other.path if other else m.group(2)}) but {name} takes a "
342
+ f"{api.module} {'/'.join(api.defs)} handle ({api.handle_param}); the numeric value addresses an "
343
+ f"unrelated {api.module} PDU/object", f"{api.cite}; TPS_ECUC_02108 symbolic names are per module",
344
+ {"symbol": h[1], "belongsTo": other.path if other else None, "expects": f"{api.module}Conf_{api.defs[0]}_<name>"})
345
+ return
285
346
  target = self.by_symbol(api, h[1])
347
+ if target is None and m and m.group(1) == api.module:
348
+ d = self.tu_defines.get(h[1]) or self.defines.get(h[1])
349
+ self.add("H2L-API-002", "error", f, line, call,
350
+ f"{h[1]} names no configured {m.group(2)} '{m.group(3)}' in {api.module}"
351
+ + (f"; the generated define ({self.p.rel(d[2])}:{d[3]} = {h[2]}) is stale — regenerate and fix "
352
+ "the configuration or the call" if d else ""),
353
+ f"{api.cite}; TPS_ECUC_02108 symbolic name values",
354
+ {"symbol": h[1], "define": h[2], "defineAt": f"{self.p.rel(d[2])}:{d[3]}" if d else None})
355
+ return
286
356
  if target is None and h[2] is not None:
287
357
  hits = self.by_handle(api, h[2])
288
358
  target = hits[0] if hits else None
359
+ self.check_alias(api, h[1], h[2], target)
289
360
  if target is None:
290
361
  if SYM.match(h[1]) or h[2] is not None:
291
362
  self.add("H2L-API-002", "error", f, line, call,
@@ -298,12 +369,23 @@ class Auditor:
298
369
  else:
299
370
  self.unchecked.append({"file": self.p.rel(f), "line": line, "call": call, "reason": "handle is an expression"})
300
371
  return
372
+ self.referenced.add(target.path)
373
+ if h[0] == "sym":
374
+ self.sibling_candidates.append((f, line, call, api, target, h[1], args, unit))
375
+ if api.kind == "send" and via is None and target.path in self.rte_written:
376
+ mac = self.rte_written[target.path]
377
+ self.add("H2L-API-010", "warning", f, line, call,
378
+ f"{target.name} is written directly through Com here, but it is also the RTE-mapped signal of "
379
+ f"{mac} (generated RTE): two writers for one signal, bypassing the RTE",
380
+ "SWS_Rte inter-ECU sender-receiver communication (one sender per data element); TPS_SystemTemplate "
381
+ "SenderReceiverToSignalMapping", {"signal": target.path, "rteApi": mac})
301
382
  if api.kind in ("send", "receive"):
302
383
  self.check_direction(f, line, call, api, target)
303
384
  if api.data is not None and len(args) > api.data:
304
385
  self.check_width(f, line, call, target, args[api.data], unit)
305
386
  if api.kind == "nvm" and api.data is not None and len(args) > api.data:
306
387
  self.check_nvm_null(f, line, call, name, target, args[api.data])
388
+ self.check_nvm_ram(f, line, call, name, target, args[api.data])
307
389
 
308
390
  def audit_rte(self, f, line, name, args, unit):
309
391
  d = self.defines.get(name)
@@ -334,29 +416,63 @@ class Auditor:
334
416
  f"ECUC_Com_00493 ComIPduDirection; {api.cite}",
335
417
  {"signal": sig.path, "ipdu": ip.path, "ComIPduDirection": d})
336
418
 
419
+ def struct_member(self, type_name: str, member: str, unit: str) -> Optional[tuple]:
420
+ key = (type_name, member)
421
+ if key in self._struct_cache:
422
+ return self._struct_cache[key]
423
+ res = None
424
+ for text in (unit, self.header_text):
425
+ m = re.search(rf"typedef\s+struct\s*\w*\s*\{{([^{{}}]*)\}}\s*{re.escape(type_name)}\s*;", text) or \
426
+ re.search(rf"struct\s+{re.escape(type_name)}\s*\{{([^{{}}]*)\}}", text)
427
+ if m:
428
+ mm = re.search(rf"\b(\w+)\s+{re.escape(member)}\s*(\[\s*(\w*)\s*\])?\s*;", m.group(1))
429
+ if mm:
430
+ res = (mm.group(1), mm.group(3) if mm.group(2) else None)
431
+ break
432
+ self._struct_cache[key] = res
433
+ return res
434
+
435
+ def decl(self, var: str, unit: str):
436
+ m = re.search(rf"\b(?:(?:static|volatile|const|extern)\s+)*(\w+)\s*(\*?)\s*(?:const\s+)?{re.escape(var)}"
437
+ rf"\s*(\[\s*(\w*)\s*\])?\s*[;=,)]", unit)
438
+ if not m or m.group(1) in ("return", "else", "case", "goto", "sizeof", "struct", "union", "enum"):
439
+ return None
440
+ return m.group(1), bool(m.group(2)), bool(m.group(3)), m.group(4), unit.count("\n", 0, m.start()) + 1
441
+
337
442
  def c_type(self, expr: str, unit: str):
338
443
  """Type of the object a data-pointer argument points to -> (type, array_len|None), or None if unknown."""
444
+ self.last_decl_line = None
339
445
  e = expr.strip()
340
446
  while e.startswith("(") and e.endswith(")") and split_args("x" + e, 1)[1] == len(e) + 1:
341
447
  e = e[1:-1].strip()
342
448
  lit = re.fullmatch(r"&\s*\(\s*(\w+)\s*\)\s*\{.*\}", e, re.S) # &(uint8){ x }
343
449
  if lit:
344
450
  return lit.group(1), None
451
+ mem = re.fullmatch(r"&\s*\(?\s*(\w+)\s*(\.|->)\s*(\w+)\s*\)?", e) # &var.member / &ptr->member
452
+ if mem:
453
+ d = self.decl(mem.group(1), unit)
454
+ if d is None or (mem.group(2) == "->") != d[1]:
455
+ return None
456
+ t = self.struct_member(d[0], mem.group(3), unit)
457
+ if t is None:
458
+ return None
459
+ self.last_decl_line = d[4]
460
+ return t
345
461
  ref = re.fullmatch(r"&\s*(\w+)", e) # &var
346
462
  elem = re.fullmatch(r"&\s*(\w+)\s*\[[^\]]*\]", e) # &buf[i]
347
463
  bare = re.fullmatch(r"(\w+)", e) # buf (array decays) or a pointer variable
348
464
  var = (ref or elem or bare).group(1) if (ref or elem or bare) else None
349
465
  if var is None:
350
466
  return None
351
- decl = re.search(rf"\b(?:(?:static|volatile|const|extern)\s+)*(\w+)\s+(?:const\s+)?{re.escape(var)}"
352
- rf"\s*(\[\s*(\w*)\s*\])?\s*[;=,)]", unit)
353
- if not decl or decl.group(1) in ("return", "else", "case", "goto", "sizeof", "struct", "union", "enum"):
467
+ d = self.decl(var, unit)
468
+ if d is None or d[1]:
354
469
  return None
355
- is_array = bool(decl.group(2))
470
+ typ, _ptr, is_array, alen, dline = d
471
+ self.last_decl_line = dline
356
472
  if ref:
357
- return decl.group(1), (decl.group(3) if is_array else None)
473
+ return typ, (alen if is_array else None)
358
474
  if elem or (bare and is_array):
359
- return decl.group(1), None
475
+ return typ, None
360
476
  return None # bare non-array variable: a pointer whose pointee is not visible here
361
477
 
362
478
  def check_width(self, f, line, call, sig, arg, unit):
@@ -379,9 +495,136 @@ class Auditor:
379
495
  self.add("H2L-API-004", "error", f, line, call,
380
496
  f"{arg} is a {ct[0]}{'[' + ct[1] + ']' if ct[1] else ''} ({cbits} bits) but {sig.name} is "
381
497
  f"ComSignalType={typ} ({expect // 8}-byte object, ComBitSize={bs}); Com accesses {expect // 8} "
382
- f"byte(s) at that address — wrong signal or wrong variable",
498
+ f"byte(s) at that address — " + ("Com writes past the object (overflow)" if "Receive" in call else
499
+ "wrong signal or wrong variable") + (f"; object declared at line {self.last_decl_line}" if self.last_decl_line else ""),
383
500
  "SWS_Com_00197/00198 SignalDataPtr points to an object of the ComSignalType; ECUC_Com_00127 ComSignalType",
384
- {"signal": sig.path, "ComSignalType": typ, "ComBitSize": bs, "cType": ct[0]})
501
+ {"signal": sig.path, "ComSignalType": typ, "ComBitSize": bs, "cType": ct[0],
502
+ "declLine": self.last_decl_line})
503
+
504
+ def check_alias(self, api, sym, value, target):
505
+ """A hand-written #define standing in for a generated handle (H2L-API-007), reported at the #define."""
506
+ d = self.tu_defines.get(sym) or self.defines.get(sym)
507
+ if d is None or sym in self._aliases:
508
+ return
509
+ self._aliases.add(sym)
510
+ dfile, dline = d[2], d[3]
511
+ try:
512
+ raw = open(dfile, encoding="utf-8", errors="replace").read().split("\n")[dline - 1]
513
+ except (OSError, IndexError):
514
+ raw = ""
515
+ named = re.search(r"\b(\w+Conf_\w+?_\w+)\b", raw.split("define", 1)[-1].split(sym, 1)[-1])
516
+ call = f"#define {sym} {d[1]}"
517
+ if named:
518
+ meant = self.by_symbol(api, named.group(1))
519
+ mv = parse_int(self.val(meant, api.handle_param) or "") if meant is not None else None
520
+ if meant is not None and mv != value:
521
+ self.add("H2L-API-007", "error", dfile, dline, call,
522
+ f"stale handle alias: {sym} = {value} is commented as {named.group(1)}, whose {api.handle_param} is "
523
+ f"{mv}; {value} addresses {target.path if target else 'no configured element'}",
524
+ "TPS_ECUC_02108 symbolic name values; TPS_ECUConfiguration 3.4.1.1",
525
+ {"alias": sym, "value": value, "meant": meant.path, api.handle_param: mv,
526
+ "actual": target.path if target else None}, fix=f"use {named.group(1)} directly")
527
+ return
528
+ self.add("H2L-API-007", "warning", dfile, dline, call,
529
+ f"hand-coded handle macro {sym} = {value} ({target.path if target else 'matches nothing'}); it breaks "
530
+ "silently when the generator re-assigns handles",
531
+ "TPS_ECUC_02108 symbolic name values", {"alias": sym, "value": value, "path": target.path if target else None},
532
+ fix=f"use {self.symbol_of(target, api)}" if target else None)
533
+
534
+ def check_nvm_ram(self, f, line, call, name, blk, arg):
535
+ """Data pointer is the permanent RAM block of a *different* NvM block (H2L-API-008)."""
536
+ m = re.fullmatch(r"\(?\s*&?\s*(\w+)\s*\)?", arg.strip())
537
+ if not m or arg.replace(" ", "") in NULLS:
538
+ return
539
+ sym = m.group(1)
540
+ own = (self.val(blk, "NvMRamBlockDataAddress") or "").strip()
541
+ if re.search(rf"\b{re.escape(sym)}\b", own):
542
+ return
543
+ for other in self.containers("NvM", ("NvMBlockDescriptor",)):
544
+ if other.path == blk.path:
545
+ continue
546
+ addr = (self.val(other, "NvMRamBlockDataAddress") or "").strip()
547
+ if addr and re.search(rf"(?:^|[^\w]){re.escape(sym)}\b", addr):
548
+ self.add("H2L-API-008", "error", f, line, call,
549
+ f"{name} on {blk.name} is given {arg}, the permanent RAM block of {other.name} "
550
+ f"(NvMRamBlockDataAddress={addr}); {blk.name}'s own RAM block is {own or '(none)'} — wrong data is "
551
+ f"stored/overwritten", "SWS_NvM_00454/00455 NvM_DstPtr/NvM_SrcPtr; ECUC_NvM_00482 NvMRamBlockDataAddress",
552
+ {"block": blk.path, "ownRam": own or None, "givenRamOf": other.path,
553
+ "NvMNvBlockLength": self.val(blk, "NvMNvBlockLength"),
554
+ "otherLength": self.val(other, "NvMNvBlockLength")},
555
+ fix=f"pass {own}" if own else None)
556
+ return
557
+
558
+ def check_siblings(self):
559
+ """Handle whose instance token (FL/FR/2/...) disagrees with the call's context, while the sibling with the
560
+ context's token is configured and never referenced (H2L-API-006)."""
561
+ uses_by: dict = {}
562
+ for c in self.sibling_candidates:
563
+ uses_by[(c[0], c[4].path)] = uses_by.get((c[0], c[4].path), 0) + 1
564
+ sib_index: dict = {}
565
+ for f, line, call, api, target, sym, args, unit in self.sibling_candidates:
566
+ if uses_by[(f, target.path)] < 2:
567
+ continue
568
+ dname = (target.def_ref or "").rsplit("/", 1)[-1]
569
+ mine = target.name.split("_")
570
+ key = (api.module, dname)
571
+ if key not in sib_index: # pattern with one part blanked -> containers
572
+ idx: dict = {}
573
+ for e in self.containers(api.module, (dname,)):
574
+ parts = e.name.split("_")
575
+ for i in range(len(parts)):
576
+ idx.setdefault((len(parts), i, tuple(parts[:i] + parts[i + 1:])), []).append((e, parts[i]))
577
+ sib_index[key] = idx
578
+ sibs = []
579
+ for i in range(len(mine)):
580
+ for e, tok in sib_index[key].get((len(mine), i, tuple(mine[:i] + mine[i + 1:])), []):
581
+ if e.path != target.path:
582
+ sibs.append((e, mine[i], tok))
583
+ if not sibs:
584
+ continue
585
+ ctx_text = " ".join(args[1:])
586
+ fn = self._function_at(unit, line)
587
+ tokens = set(re.split(r"[^A-Za-z0-9]+", ctx_text + " " + fn)) | \
588
+ {t for ident in re.findall(r"\w+", ctx_text + " " + fn) for t in ident.split("_")}
589
+ uses = uses_by[(f, target.path)] # copy-paste signature: the same handle is used again in this file
590
+ for e, my_tok, sib_tok in sibs:
591
+ if not (INSTANCE.match(my_tok) and INSTANCE.match(sib_tok)):
592
+ continue
593
+ if sib_tok in tokens and my_tok not in tokens and e.path not in self.referenced:
594
+ self.add("H2L-API-006", "warning", f, line, call,
595
+ f"{sym} uses instance '{my_tok}' (and is used {uses}x in this file) but this call's context "
596
+ f"refers to '{sib_tok}' "
597
+ f"({ctx_text[:80].strip() or fn}); {e.name} (the '{sib_tok}' sibling) is configured but never "
598
+ "referenced in the code — likely copy-paste of the wrong handle",
599
+ "heuristic: sibling handle mismatch (instance token)",
600
+ {"handle": target.path, "sibling": e.path, "contextToken": sib_tok, "siblingReferenced": False},
601
+ fix=f"use {self.symbol_of(e, api)}")
602
+ break
603
+
604
+ @staticmethod
605
+ def _function_at(unit: str, line: int) -> str:
606
+ head = "\n".join(unit.split("\n")[:line])
607
+ m = list(re.finditer(r"^\s*(?:static\s+)?[\w\s\*]+?\b(\w+)\s*\([^;{]*\)\s*\{", head, re.M))
608
+ return m[-1].group(1) if m else ""
609
+
610
+ def unreferenced(self) -> dict:
611
+ out = {}
612
+ dem = [e for e in self.containers("Dem", ("DemEventParameter",)) if e.path not in self.referenced
613
+ and any(pv.is_ref and pv.name == "DemDTCRef" for g in self.p.find(e.path) for pv in g.params)]
614
+ tx = []
615
+ for ip in self.containers("Com", ("ComIPdu",)):
616
+ if (self.val(ip, "ComIPduDirection") or "").strip() != "SEND":
617
+ continue
618
+ for g in self.p.find(ip.path):
619
+ for pv in g.params:
620
+ if pv.name == "ComIPduSignalRef" and pv.value and pv.value.strip() not in self.referenced:
621
+ tx.append(pv.value.strip())
622
+ nvm = [e for e in self.containers("NvM", ("NvMBlockDescriptor",)) if e.path not in self.referenced]
623
+ for k, items in (("demEventsWithDtc", [e.path for e in dem]), ("txSignals", sorted(set(tx))),
624
+ ("nvmBlocks", [e.path for e in nvm])):
625
+ if items:
626
+ out[k] = {"count": len(items), "sample": items[:10]}
627
+ return out
385
628
 
386
629
  def check_nvm_null(self, f, line, call, name, blk, arg):
387
630
  if arg.replace(" ", "") not in NULLS:
@@ -426,30 +669,34 @@ def audit(project: Project, paths_arg: Optional[list] = None, headers_arg: Optio
426
669
  extra = paths.expand(root, headers_arg or cfg.get("headers") or [], paths.is_c)
427
670
  hdrs = sorted({f for f in tree.files + extra if f.lower().endswith(".h")})
428
671
  a = Auditor(project, parse_defines(hdrs))
672
+ try:
673
+ a.header_text = strip_comments("\n".join(open(h, encoding="utf-8", errors="replace").read() for h in hdrs[:2000]))
674
+ except OSError:
675
+ a.header_text = ""
676
+ for mac, (params, body, _f, _l) in a.defines.items():
677
+ if mac.startswith("Rte_Write_") and params is not None:
678
+ mm = re.search(r"Com_SendSignal\s*\(\s*(\w+)", body)
679
+ if mm:
680
+ t = a.by_symbol(APIS["Com_SendSignal"], mm.group(1))
681
+ if t is not None:
682
+ a.rte_written.setdefault(t.path, mac)
429
683
  for f in sources:
430
684
  try:
431
685
  unit = strip_comments(open(f, encoding="utf-8", errors="replace").read())
432
686
  except OSError:
433
687
  continue
434
- for m in CALL.finditer(unit):
435
- ls = unit.rfind("\n", 0, m.start()) + 1
436
- if unit[ls:m.start()].lstrip().startswith("#"):
437
- continue # macro definition, not a call
438
- before = unit[ls:m.start()].rstrip()
439
- prev = re.search(r"(\w+)\s*$", before)
440
- if prev and prev.group(1) not in ("return", "void") and not before.endswith(")"):
441
- continue # a declaration/prototype ("Std_ReturnType Com_SendSignal(")
442
- args, _ = split_args(unit, m.end() - 1)
443
- if args is None:
444
- continue
445
- a.audit_call(f, unit.count("\n", 0, m.start()) + 1, m.group(1), args, unit)
688
+ a.tu_defines = parse_defines([f])
689
+ for name, start, line, args in iter_calls(unit, CALL):
690
+ a.audit_call(f, line, name, args, unit)
691
+ a.check_siblings()
446
692
  a.findings.sort(key=lambda d: (d["file"], d["line"], d["rule"]))
447
693
  summary: dict = {}
448
694
  for d in a.findings:
449
695
  summary[d["severity"]] = summary.get(d["severity"], 0) + 1
450
696
  res = {"scanned": {"sources": [project.rel(f) for f in sources], "headers": len(hdrs), "calls": a.calls,
451
697
  "generatedSkipped": len(generated)},
452
- "summary": summary, "findings": a.findings, "unchecked": a.unchecked}
698
+ "summary": summary, "findings": a.findings, "unchecked": a.unchecked,
699
+ "unreferenced": a.unreferenced()}
453
700
  if notes:
454
701
  res["notes"] = notes
455
702
  if explicit and not sources:
@@ -458,6 +705,56 @@ def audit(project: Project, paths_arg: Optional[list] = None, headers_arg: Optio
458
705
  return res
459
706
 
460
707
 
708
+ def can_info(project: Project, pdu_path: Optional[str]) -> list:
709
+ """EcuC Pdu of a Com I-PDU -> CanIf Tx/Rx PDU(s) with CAN ID, controller and network.
710
+
711
+ Linked generically: the CanIf PDU's EcuC Pdu is the same Pdu, or the other end of a PduR routing path,
712
+ or stands for the same system I-PDU in the extract.
713
+ """
714
+ if not pdu_path:
715
+ return []
716
+ from .rules import RuleContext
717
+ from .rules.system import channels, system_ipdus
718
+ ctx = RuleContext(project)
719
+ pdu = project.find(pdu_path)
720
+ if not pdu:
721
+ return []
722
+ pdu = pdu[0]
723
+ linked = {pdu.path}
724
+ for rp in ctx.containers("PduR/PduRRoutingPaths/PduRRoutingPath"):
725
+ ends = [ctx.target(ctx.value(rp, "PduRSrcPduRRef"))] + [ctx.target(pv) for pv in ctx.values(rp, "PduRDestPduRRef")]
726
+ pdus = {ctx.target(ctx.value(e, n)).path for e in ends if e is not None
727
+ for n in ("PduRSrcPduRef", "PduRDestPduRef") if ctx.target(ctx.value(e, n)) is not None}
728
+ if pdu.path in pdus:
729
+ linked |= pdus
730
+ sys_ip = set(system_ipdus(ctx, pdu))
731
+ out = []
732
+ for kind, ref, cid, typ in (("Tx", "CanIfTxPduRef", "CanIfTxPduCanId", "CanIfTxPduCanIdType"),
733
+ ("Rx", "CanIfRxPduRef", "CanIfRxPduCanId", "CanIfRxPduCanIdType")):
734
+ for cp in ctx.containers(f"CanIf/CanIfInitCfg/CanIf{kind}PduCfg"):
735
+ tgt = ctx.target(ctx.value(cp, ref))
736
+ if tgt is None:
737
+ continue
738
+ if tgt.path not in linked and not (sys_ip and sys_ip & set(system_ipdus(ctx, tgt))):
739
+ continue
740
+ can_id = parse_int(ctx.text(cp, cid) or "")
741
+ hoh = None
742
+ if kind == "Tx":
743
+ buf = ctx.target(ctx.value(cp, "CanIfTxPduBufferRef"))
744
+ hoh = ctx.target(ctx.value(buf, "CanIfBufferHthRef")) if buf is not None else None
745
+ ctrl = ctx.target(ctx.value(hoh, "CanIfHthCanCtrlIdRef")) if hoh is not None else None
746
+ else:
747
+ hoh = ctx.target(ctx.value(cp, "CanIfRxPduHrhIdRef"))
748
+ ctrl = ctx.target(ctx.value(hoh, "CanIfHrhCanCtrlIdRef")) if hoh is not None else None
749
+ out.append({"canIfPdu": cp.path, "direction": kind, "canId": can_id,
750
+ "canIdHex": hex(can_id) if can_id is not None else None,
751
+ "canIdType": ctx.text(cp, typ), "at": f"{project.rel(cp.file)}:{cp.line}",
752
+ "controller": ctrl.path if ctrl is not None else None,
753
+ "canController": (ctx.text(ctrl, "CanIfCtrlCanCtrlRef") or None) if ctrl is not None else None,
754
+ "network": channels(ctx, tgt) or channels(ctx, pdu) or None})
755
+ return out
756
+
757
+
461
758
  def signal_owners(project: Project, paths: list) -> dict:
462
759
  """AUTOSAR path of a ComSignal / ComGroupSignal -> [{ipdu, direction, pdu}] from the local ECUC."""
463
760
  a = Auditor(project, {})
@@ -468,8 +765,13 @@ def signal_owners(project: Project, paths: list) -> dict:
468
765
  out[p] = None # not in the local checkout
469
766
  continue
470
767
  sig = g[0]
471
- if not (sig.def_ref or "").endswith(("/ComSignal", "/ComGroupSignal", "/ComSignalGroup")):
768
+ if (sig.def_ref or "").endswith("/ComIPdu"):
769
+ ipdus = [sig]
770
+ elif (sig.def_ref or "").endswith(("/ComSignal", "/ComGroupSignal", "/ComSignalGroup")):
771
+ ipdus = a.ipdus_of(sig)
772
+ else:
472
773
  continue
473
774
  out[p] = [{"ipdu": ip.path, "direction": (a.val(ip, "ComIPduDirection") or "").strip() or None,
474
- "pdu": (a.val(ip, "ComPduIdRef") or "").strip() or None} for ip in a.ipdus_of(sig)]
775
+ "pdu": (a.val(ip, "ComPduIdRef") or "").strip() or None,
776
+ "can": can_info(project, (a.val(ip, "ComPduIdRef") or "").strip() or None)} for ip in ipdus]
475
777
  return out