@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.
- 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 +328 -48
- package/python/ecuc_engine/cli.py +114 -5
- 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/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 +4 -1
- 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 +15 -2
- package/python/ecuc_engine/verify_rte.py +445 -0
- package/python/pyproject.toml +1 -1
- package/skills/autosar/SKILL.md +47 -1
- package/skills/autosar/references/codegen.md +30 -0
- package/skills/autosar/references/project-layout.md +7 -1
- 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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
stack
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
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)
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
352
|
-
|
|
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 =
|
|
448
|
+
typ, _ptr, is_array, alen, dline = d
|
|
449
|
+
self.last_decl_line = dline
|
|
356
450
|
if ref:
|
|
357
|
-
return
|
|
451
|
+
return typ, (alen if is_array else None)
|
|
358
452
|
if elem or (bare and is_array):
|
|
359
|
-
return
|
|
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 —
|
|
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
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
|
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
|
|
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
|