@h2loop/autosar 1.0.1 → 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.
Files changed (47) hide show
  1. package/README.md +4 -0
  2. package/agents/autosar-integrator.md +34 -2
  3. package/package.json +4 -1
  4. package/plugins/index.js +53 -23
  5. package/python/ecuc_engine/__init__.py +1 -1
  6. package/python/ecuc_engine/add_signal.py +351 -0
  7. package/python/ecuc_engine/arxml_edit.py +387 -0
  8. package/python/ecuc_engine/audit.py +328 -48
  9. package/python/ecuc_engine/cli.py +114 -5
  10. package/python/ecuc_engine/codegen/__init__.py +185 -0
  11. package/python/ecuc_engine/codegen/binding.py +210 -0
  12. package/python/ecuc_engine/codegen/bindings/sip_standin.json +111 -0
  13. package/python/ecuc_engine/codegen/cdiff.py +395 -0
  14. package/python/ecuc_engine/codegen/cwriter.py +150 -0
  15. package/python/ecuc_engine/codegen/emit.py +395 -0
  16. package/python/ecuc_engine/codegen/handles.py +89 -0
  17. package/python/ecuc_engine/codegen/mcal/tc4d.json +60 -0
  18. package/python/ecuc_engine/codegen/model.py +259 -0
  19. package/python/ecuc_engine/codegen/modules/__init__.py +7 -0
  20. package/python/ecuc_engine/codegen/modules/comstack.py +14 -0
  21. package/python/ecuc_engine/codegen/modules/dem.py +22 -0
  22. package/python/ecuc_engine/codegen/modules/mcal.py +170 -0
  23. package/python/ecuc_engine/codegen/modules/memstack.py +24 -0
  24. package/python/ecuc_engine/codegen/registry.py +133 -0
  25. package/python/ecuc_engine/codegen/schm_memmap.py +159 -0
  26. package/python/ecuc_engine/context.py +481 -0
  27. package/python/ecuc_engine/csrc.py +189 -0
  28. package/python/ecuc_engine/defs.py +30 -0
  29. package/python/ecuc_engine/find.py +162 -0
  30. package/python/ecuc_engine/gen.py +105 -0
  31. package/python/ecuc_engine/history.py +146 -0
  32. package/python/ecuc_engine/model.py +4 -1
  33. package/python/ecuc_engine/probe.py +229 -0
  34. package/python/ecuc_engine/rte_api.py +935 -0
  35. package/python/ecuc_engine/rules/__init__.py +5 -3
  36. package/python/ecuc_engine/rules/mem.py +84 -0
  37. package/python/ecuc_engine/rules/nvm.py +131 -0
  38. package/python/ecuc_engine/rules/pdur.py +87 -0
  39. package/python/ecuc_engine/rules/system.py +162 -0
  40. package/python/ecuc_engine/sysmodel.py +152 -0
  41. package/python/ecuc_engine/validate.py +15 -2
  42. package/python/ecuc_engine/verify_rte.py +445 -0
  43. package/python/pyproject.toml +1 -1
  44. package/skills/autosar/SKILL.md +47 -1
  45. package/skills/autosar/references/codegen.md +30 -0
  46. package/skills/autosar/references/project-layout.md +7 -1
  47. package/skills/autosar/references/runnable-code.md +50 -0
@@ -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,27 +237,28 @@ 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 ----------
234
264
  def add(self, rule, sev, f, line, call, msg, cite, evidence, fix=None):
@@ -248,7 +278,7 @@ class Auditor:
248
278
  while e.startswith("(") and e.endswith(")") and split_args("x" + e, 1)[1] == len(e) + 1:
249
279
  e = e[1:-1].strip()
250
280
  if re.fullmatch(r"\w+", e):
251
- d = self.defines.get(e)
281
+ d = self.tu_defines.get(e) or self.defines.get(e)
252
282
  num = None
253
283
  if d and d[0] is None:
254
284
  mm = NUM.match(d[1])
@@ -282,10 +312,29 @@ class Auditor:
282
312
  "survives handle re-assignment", f"{cite_sym}; TPS_ECUConfiguration 3.4.1.1",
283
313
  {"path": target.path, api.handle_param: h[1]}, fix=f"replace {args[0]} with {sym}")
284
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
285
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
286
334
  if target is None and h[2] is not None:
287
335
  hits = self.by_handle(api, h[2])
288
336
  target = hits[0] if hits else None
337
+ self.check_alias(api, h[1], h[2], target)
289
338
  if target is None:
290
339
  if SYM.match(h[1]) or h[2] is not None:
291
340
  self.add("H2L-API-002", "error", f, line, call,
@@ -298,12 +347,23 @@ class Auditor:
298
347
  else:
299
348
  self.unchecked.append({"file": self.p.rel(f), "line": line, "call": call, "reason": "handle is an expression"})
300
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})
301
360
  if api.kind in ("send", "receive"):
302
361
  self.check_direction(f, line, call, api, target)
303
362
  if api.data is not None and len(args) > api.data:
304
363
  self.check_width(f, line, call, target, args[api.data], unit)
305
364
  if api.kind == "nvm" and api.data is not None and len(args) > api.data:
306
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])
307
367
 
308
368
  def audit_rte(self, f, line, name, args, unit):
309
369
  d = self.defines.get(name)
@@ -334,29 +394,63 @@ class Auditor:
334
394
  f"ECUC_Com_00493 ComIPduDirection; {api.cite}",
335
395
  {"signal": sig.path, "ipdu": ip.path, "ComIPduDirection": d})
336
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
+
337
420
  def c_type(self, expr: str, unit: str):
338
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
339
423
  e = expr.strip()
340
424
  while e.startswith("(") and e.endswith(")") and split_args("x" + e, 1)[1] == len(e) + 1:
341
425
  e = e[1:-1].strip()
342
426
  lit = re.fullmatch(r"&\s*\(\s*(\w+)\s*\)\s*\{.*\}", e, re.S) # &(uint8){ x }
343
427
  if lit:
344
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
345
439
  ref = re.fullmatch(r"&\s*(\w+)", e) # &var
346
440
  elem = re.fullmatch(r"&\s*(\w+)\s*\[[^\]]*\]", e) # &buf[i]
347
441
  bare = re.fullmatch(r"(\w+)", e) # buf (array decays) or a pointer variable
348
442
  var = (ref or elem or bare).group(1) if (ref or elem or bare) else None
349
443
  if var is None:
350
444
  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"):
445
+ d = self.decl(var, unit)
446
+ if d is None or d[1]:
354
447
  return None
355
- is_array = bool(decl.group(2))
448
+ typ, _ptr, is_array, alen, dline = d
449
+ self.last_decl_line = dline
356
450
  if ref:
357
- return decl.group(1), (decl.group(3) if is_array else None)
451
+ return typ, (alen if is_array else None)
358
452
  if elem or (bare and is_array):
359
- return decl.group(1), None
453
+ return typ, None
360
454
  return None # bare non-array variable: a pointer whose pointee is not visible here
361
455
 
362
456
  def check_width(self, f, line, call, sig, arg, unit):
@@ -379,9 +473,136 @@ class Auditor:
379
473
  self.add("H2L-API-004", "error", f, line, call,
380
474
  f"{arg} is a {ct[0]}{'[' + ct[1] + ']' if ct[1] else ''} ({cbits} bits) but {sig.name} is "
381
475
  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",
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 ""),
383
478
  "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]})
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
385
606
 
386
607
  def check_nvm_null(self, f, line, call, name, blk, arg):
387
608
  if arg.replace(" ", "") not in NULLS:
@@ -426,30 +647,34 @@ def audit(project: Project, paths_arg: Optional[list] = None, headers_arg: Optio
426
647
  extra = paths.expand(root, headers_arg or cfg.get("headers") or [], paths.is_c)
427
648
  hdrs = sorted({f for f in tree.files + extra if f.lower().endswith(".h")})
428
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)
429
661
  for f in sources:
430
662
  try:
431
663
  unit = strip_comments(open(f, encoding="utf-8", errors="replace").read())
432
664
  except OSError:
433
665
  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)
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()
446
670
  a.findings.sort(key=lambda d: (d["file"], d["line"], d["rule"]))
447
671
  summary: dict = {}
448
672
  for d in a.findings:
449
673
  summary[d["severity"]] = summary.get(d["severity"], 0) + 1
450
674
  res = {"scanned": {"sources": [project.rel(f) for f in sources], "headers": len(hdrs), "calls": a.calls,
451
675
  "generatedSkipped": len(generated)},
452
- "summary": summary, "findings": a.findings, "unchecked": a.unchecked}
676
+ "summary": summary, "findings": a.findings, "unchecked": a.unchecked,
677
+ "unreferenced": a.unreferenced()}
453
678
  if notes:
454
679
  res["notes"] = notes
455
680
  if explicit and not sources:
@@ -458,6 +683,56 @@ def audit(project: Project, paths_arg: Optional[list] = None, headers_arg: Optio
458
683
  return res
459
684
 
460
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
734
+
735
+
461
736
  def signal_owners(project: Project, paths: list) -> dict:
462
737
  """AUTOSAR path of a ComSignal / ComGroupSignal -> [{ipdu, direction, pdu}] from the local ECUC."""
463
738
  a = Auditor(project, {})
@@ -468,8 +743,13 @@ def signal_owners(project: Project, paths: list) -> dict:
468
743
  out[p] = None # not in the local checkout
469
744
  continue
470
745
  sig = g[0]
471
- if not (sig.def_ref or "").endswith(("/ComSignal", "/ComGroupSignal", "/ComSignalGroup")):
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:
472
751
  continue
473
752
  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)]
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]
475
755
  return out