@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.
- package/README.md +11 -1
- package/agents/autosar-integrator.md +45 -2
- package/commands/autosar-trace.md +2 -2
- package/package.json +4 -1
- package/plugins/index.js +55 -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 +352 -50
- package/python/ecuc_engine/cli.py +125 -5
- package/python/ecuc_engine/codegen/__init__.py +233 -0
- package/python/ecuc_engine/codegen/binding.py +305 -0
- package/python/ecuc_engine/codegen/bindings/sip_standin.json +378 -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 +463 -0
- package/python/ecuc_engine/codegen/handles.py +176 -0
- package/python/ecuc_engine/codegen/mcal/tc4d.json +60 -0
- package/python/ecuc_engine/codegen/model.py +264 -0
- package/python/ecuc_engine/codegen/modules/__init__.py +7 -0
- package/python/ecuc_engine/codegen/modules/com.py +145 -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 +208 -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/ecucjson.py +75 -22
- package/python/ecuc_engine/find.py +164 -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/specids.py +163 -0
- package/python/ecuc_engine/sysmodel.py +152 -0
- package/python/ecuc_engine/validate.py +15 -2
- package/python/ecuc_engine/values.py +26 -0
- package/python/ecuc_engine/verify_rte.py +445 -0
- package/python/pyproject.toml +1 -1
- package/skills/autosar/SKILL.md +78 -6
- package/skills/autosar/references/codegen.md +58 -0
- package/skills/autosar/references/ecuc-model.md +4 -1
- package/skills/autosar/references/project-layout.md +10 -1
- 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
|
-
|
|
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,32 +237,55 @@ 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 ----------
|
|
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
|
-
|
|
352
|
-
|
|
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 =
|
|
470
|
+
typ, _ptr, is_array, alen, dline = d
|
|
471
|
+
self.last_decl_line = dline
|
|
356
472
|
if ref:
|
|
357
|
-
return
|
|
473
|
+
return typ, (alen if is_array else None)
|
|
358
474
|
if elem or (bare and is_array):
|
|
359
|
-
return
|
|
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 —
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
|
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
|