@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
@@ -74,9 +74,11 @@ class RuleContext:
74
74
 
75
75
 
76
76
  def run_rules(project: Project) -> Iterable[dict]:
77
- from . import com, handles # noqa: F401 (registers the packs)
77
+ from . import com, handles, mem, nvm, pdur, system # noqa: F401 (registers the packs)
78
78
  ctx = RuleContext(project)
79
79
  for r in RULES:
80
- for e, line, msg, def_ref in r.fn(ctx):
80
+ for item in r.fn(ctx):
81
+ e, line, msg, def_ref = item[:4]
82
+ sev = item[4] if len(item) > 4 else r.severity # a rule may downgrade one finding (e.g. NOT CHECKED -> info)
81
83
  yield {"file": project.rel(e.file), "line": line, "path": e.path, "rule": r.id,
82
- "severity": r.severity, "message": f"{msg} [{r.cite}]", "defRef": def_ref}
84
+ "severity": sev, "message": f"{msg} [{r.cite}]", "defRef": def_ref}
@@ -0,0 +1,84 @@
1
+ """H2L-MEM-001: do the Fee/Ea blocks fit the memory they are configured to use?
2
+
3
+ Followed generically: each Fee/Ea block -> its address-area reference (MemAcc address area) -> sub address
4
+ areas -> sector batch (number of sectors x erase sector size). Per-block cost = blockSize + block overhead,
5
+ rounded up to the virtual page size (FeeBlockOverhead/FeeVirtualPageSize from the Fee config when present),
6
+ times the number of NV instances NvM keeps (NvMNvBlockNum of the referencing NvM block). Fee/Ea need spare
7
+ space for sector switching / garbage collection, so the check compares against half the area (warning).
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from . import rule
12
+ from ..values import parse_int
13
+
14
+ CITE = ("ECUC_Fee_00148 FeeBlockSize, FeeBlockOverhead/FeeVirtualPageSize; ECUC_NvM_00479 NvMNvBlockNum; MemAcc "
15
+ "address area / Mem sector batch sizes (SWS_MemAcc, SWS_Mem)")
16
+
17
+
18
+ def _up(n: int, page: int) -> int:
19
+ return n if page <= 1 else ((n + page - 1) // page) * page
20
+
21
+
22
+ def _first(ctx, rel):
23
+ c = ctx.containers(rel)
24
+ return c[0] if c else None
25
+
26
+
27
+ @rule("H2L-MEM-001", "warning", CITE, "Fee/Ea blocks fit their configured memory area")
28
+ def capacity(ctx):
29
+ for mod, blkdef, size_p in (("Fee", "Fee/FeeBlockConfiguration", "FeeBlockSize"),
30
+ ("Ea", "Ea/EaBlockConfiguration", "EaBlockSize")):
31
+ blocks = ctx.containers(blkdef)
32
+ if not blocks:
33
+ continue
34
+ gen = _first(ctx, f"{mod}/{mod}General")
35
+ pub = _first(ctx, f"{mod}/{mod}PublishedInformation")
36
+ page = parse_int(ctx.text(gen, f"{mod}VirtualPageSize") or "") if gen is not None else None
37
+ over = parse_int(ctx.text(pub, f"{mod}BlockOverhead") or "") if pub is not None else None
38
+ instances: dict = {}
39
+ for nb in ctx.containers("NvM/NvMBlockDescriptor"):
40
+ stack = [nb]
41
+ n = parse_int(ctx.text(nb, "NvMNvBlockNum") or "") or 1
42
+ while stack:
43
+ e = stack.pop()
44
+ for g in ctx.p.find(e.path):
45
+ stack.extend(g.children)
46
+ for pv in g.params:
47
+ if pv.is_ref and pv.value:
48
+ instances[pv.value.strip()] = max(instances.get(pv.value.strip(), 1), n)
49
+ per_area: dict = {}
50
+ for b in blocks:
51
+ size = parse_int(ctx.text(b, size_p) or "")
52
+ if size is None:
53
+ continue
54
+ area = None
55
+ for g in ctx.p.find(b.path):
56
+ for pv in g.params:
57
+ t = ctx.target(pv) if pv.is_ref else None
58
+ if t is not None and "AddressArea" in (t.def_ref or ""):
59
+ area = t
60
+ cost = _up(size + (over or 0), page or 1) * instances.get(b.path, 1)
61
+ key = area.path if area is not None else None
62
+ per_area.setdefault(key, [0, 0, area])
63
+ per_area[key][0] += cost
64
+ per_area[key][1] += 1
65
+ for key, (need, count, area) in per_area.items():
66
+ if area is None:
67
+ continue
68
+ cap, parts = 0, []
69
+ for sub in ctx.p.find(area.path)[0].children:
70
+ ns = parse_int(ctx.text(sub, f"MemAccNumberOfSectors") or "")
71
+ batch = ctx.target(ctx.value(sub, "MemAccSectorBatchRef"))
72
+ ss = parse_int(ctx.text(batch, "MemEraseSectorSize") or "") if batch is not None else None
73
+ if ns and ss:
74
+ cap += ns * ss
75
+ parts.append(f"{sub.name}: {ns} x {ss} B")
76
+ if not cap:
77
+ continue
78
+ usable = cap // 2
79
+ if need > usable:
80
+ anchor = ctx.value(blocks[0], size_p)
81
+ yield blocks[0], anchor.node.line if anchor else blocks[0].line, (
82
+ f"{count} {mod} block(s) on {area.name} need {need} B (size + overhead {over or 0} B, rounded to "
83
+ f"{page or 1} B pages, x NV instances) but the area holds {cap} B ({', '.join(parts)}); with space "
84
+ f"for sector switching only ~{usable} B are usable"), anchor.def_ref if anchor else None
@@ -0,0 +1,131 @@
1
+ """NvM block length vs the C object behind its permanent RAM block, and the Fee/Ea block that stores it.
2
+
3
+ NvMRamBlockDataAddress names a C object; its size comes from a compile-only sizeof probe built with the
4
+ project's own compiler/flags (ecuc_engine/probe.py). NvMNvBlockLength is the NV *data* length
5
+ (ECUC_NvM_00479: "NV block data length in bytes"; CRC is managed by NvM on top). The Fee/Ea block is found by
6
+ following the block's references to a FeeBlockConfiguration / EaBlockConfiguration.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import re
12
+
13
+ from . import rule
14
+ from .. import paths, probe
15
+ from ..values import parse_int
16
+
17
+ CITE = ("ECUC_NvM_00479 NvMNvBlockLength (NV block data length), ECUC_NvM_00482 NvMRamBlockDataAddress, "
18
+ "ECUC_Fee_00148 FeeBlockSize / ECUC_Ea_00128 EaBlockSize")
19
+ SYMBOL = re.compile(r"^[&(\s]*(?:\(\s*\w+\s*\*?\s*\)\s*)?&?\s*([A-Za-z_]\w*)")
20
+
21
+
22
+ def _storage_blocks(ctx, blk):
23
+ """Fee/Ea block configurations referenced (at any depth) from an NvM block descriptor."""
24
+ out = []
25
+ stack = [blk]
26
+ while stack:
27
+ e = stack.pop()
28
+ for g in ctx.p.find(e.path):
29
+ stack.extend(g.children)
30
+ for pv in g.params:
31
+ if pv.is_ref and pv.value:
32
+ t = ctx.target(pv)
33
+ if t is not None and (t.def_ref or "").endswith(("/FeeBlockConfiguration", "/EaBlockConfiguration")):
34
+ out.append(t)
35
+ return out
36
+
37
+
38
+ def blocks_with_ram(ctx):
39
+ for blk in ctx.containers("NvM/NvMBlockDescriptor"):
40
+ addr = (ctx.text(blk, "NvMRamBlockDataAddress") or "").strip()
41
+ m = SYMBOL.match(addr)
42
+ if m:
43
+ yield blk, addr, m.group(1)
44
+
45
+
46
+ def probe_sizes(ctx):
47
+ """{block path: (size|None, Decl|None)} and notes; cached on the context."""
48
+ if getattr(ctx, "_nvm_sizes", None) is not None:
49
+ return ctx._nvm_sizes
50
+ root = ctx.p.root
51
+ tree = paths.walk(root, paths.is_c).files
52
+ from ..audit import is_generated
53
+ own = [f for f in tree if not is_generated(f)]
54
+ headers = [f for f in own if f.lower().endswith(".h")] + [f for f in tree if f.lower().endswith(".h") and f not in own]
55
+ cfiles = [f for f in own if f.lower().endswith(".c")]
56
+ decls, where = {}, {}
57
+ for blk, _addr, sym in blocks_with_ram(ctx):
58
+ d = probe.find_decl(sym, cfiles, headers)
59
+ where[blk.path] = d
60
+ if d is not None:
61
+ if not d.header:
62
+ d.header = probe.type_header(d.type, headers)
63
+ if d.header is None and d.type not in ("uint8", "uint16", "uint32", "sint8", "sint16", "sint32", "boolean",
64
+ "char", "int", "short", "long", "float", "double"):
65
+ continue
66
+ decls[blk.path] = d
67
+ ctx._nvm_probed = set(decls)
68
+ found, notes, tc = probe.sizes(root, probe.load_cfg(root), decls) if decls else ({}, [], None)
69
+ ctx._nvm_sizes = ({k: (found.get(k), where.get(k)) for k in where}, notes, tc)
70
+ return ctx._nvm_sizes
71
+
72
+
73
+ @rule("H2L-NVM-001", "error", CITE, "NvM block length / Fee-Ea block size match the RAM block's C object size")
74
+ def nvm_length_vs_c(ctx):
75
+ """Every NvM block descriptor gets an outcome: a size verdict (error on mismatch), or a NOT CHECKED info that
76
+ says why. A summary info is always emitted, so the rule never yields nothing when NvM is configured."""
77
+ blocks = ctx.containers("NvM/NvMBlockDescriptor")
78
+ if not blocks:
79
+ return
80
+ first = blocks[0]
81
+ with_ram = list(blocks_with_ram(ctx))
82
+ if not with_ram:
83
+ yield first, first.line, (f"NOT CHECKED: none of the {len(blocks)} NvM block(s) has an NvMRamBlockDataAddress "
84
+ "naming a C object"), None, "info"
85
+ return
86
+ table, notes, tc = probe_sizes(ctx)
87
+ for n in notes:
88
+ yield first, first.line, f"NOT CHECKED: {n}", None, "info"
89
+ checked = mism = 0
90
+ unchecked = []
91
+ for blk, addr, sym in with_ram:
92
+ size, decl = table.get(blk.path, (None, None))
93
+ if size is None:
94
+ why = ("not declared in any C source" if decl is None else
95
+ f"{decl.type} has no header defining it" if blk.path not in getattr(ctx, "_nvm_probed", {blk.path})
96
+ else "compiler did not report its size")
97
+ unchecked.append(blk.name)
98
+ yield blk, blk.line, f"NOT CHECKED: {blk.name}: sizeof({sym}) undetermined ({why})", None, "info"
99
+ continue
100
+ checked += 1
101
+ where = f"{os.path.relpath(decl.file, ctx.p.root)}:{decl.line}" if decl else "?"
102
+ ln = parse_int(ctx.text(blk, "NvMNvBlockLength") or "")
103
+ pv = ctx.value(blk, "NvMNvBlockLength")
104
+ crc = (ctx.text(blk, "NvMBlockUseCrc") or "").lower() == "true"
105
+ tail = f" (RAM block {addr}: {decl.type if decl else '?'} at {where}, sizeof = {size} via {tc.origin if tc else '?'}" \
106
+ f"{'; CRC ' + (ctx.text(blk, 'NvMBlockCrcType') or '') + ' is added by NvM on top' if crc else ''})"
107
+ if ln is not None and ln != size:
108
+ mism += 1
109
+ yield blk, pv.node.line, f"{blk.name}: NvMNvBlockLength={ln} but sizeof({sym}) = {size}" + tail, pv.def_ref
110
+ for st in _storage_blocks(ctx, blk):
111
+ sname = "FeeBlockSize" if (st.def_ref or "").endswith("/FeeBlockConfiguration") else "EaBlockSize"
112
+ bs = parse_int(ctx.text(st, sname) or "")
113
+ spv = ctx.value(st, sname)
114
+ if bs is not None and bs < size:
115
+ mism += 1
116
+ yield st, spv.node.line, (f"{st.name}: {sname}={bs} cannot hold the {size}-byte RAM block of {blk.name}"
117
+ + tail), spv.def_ref
118
+ yield first, first.line, (f"sizeof check: {checked} of {len(with_ram)} RAM block(s) sized via "
119
+ f"{tc.origin if tc else 'no probe'}, {mism} mismatch(es), {len(unchecked)} NOT CHECKED"), None, "info"
120
+
121
+
122
+ @rule("H2L-NVM-000", "info", "NvM RAM block coverage", "RAM blocks whose C object could not be found")
123
+ def nvm_unresolved(ctx):
124
+ if not ctx.containers("NvM/NvMBlockDescriptor"):
125
+ return
126
+ table, _notes, _tc = probe_sizes(ctx)
127
+ miss = [p for p, (s, d) in table.items() if d is None]
128
+ if miss:
129
+ first = ctx.containers("NvM/NvMBlockDescriptor")[0]
130
+ yield first, first.line, (f"{len(miss)} NvM RAM block symbol(s) not declared in any C source "
131
+ f"(e.g. {', '.join(miss[:3])}); H2L-NVM-001 skipped for them"), None
@@ -0,0 +1,87 @@
1
+ """PduR routing vs the System / ECU extract.
2
+
3
+ A routing path forwards one I-PDU: its source and destination EcuC Pdus must stand for the same system
4
+ I-PDU (the same IPdu triggered on two channels, or an I-PDU with the same I-SIGNAL content), or for a
5
+ source/target pair of an extract GATEWAY I-PDU-MAPPING when the extract has one.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from . import rule
10
+ from .system import channels, foreign_targets, system_ipdus
11
+
12
+ CITE = ("SWS_PduR routing path (PduRSrcPdu/PduRDestPdu); System Template Gateway.iPduMappings / IPdu triggered on "
13
+ "source and target channels")
14
+
15
+
16
+ def _pdu(ctx, container, ref_name):
17
+ return ctx.target(ctx.value(container, ref_name)) if container is not None else None
18
+
19
+
20
+ def _gateway_map(ctx) -> dict:
21
+ """source PDU-TRIGGERING -> {target PDU-TRIGGERING} from extract I-PDU-MAPPINGs (if any)."""
22
+ out: dict = {}
23
+ for m in ctx.p.system.tags("I-PDU-MAPPING"):
24
+ src = m.values.get("SOURCE-I-PDU-REF", [])
25
+ tgt = m.values.get("TARGET-I-PDU-REF", [])
26
+ for s in src:
27
+ out.setdefault(s, set()).update(tgt)
28
+ return out
29
+
30
+
31
+ def _paths(ctx):
32
+ for rp in ctx.containers("PduR/PduRRoutingPaths/PduRRoutingPath"):
33
+ src = ctx.target(ctx.value(rp, "PduRSrcPduRRef"))
34
+ dests = [ctx.target(pv) for pv in ctx.values(rp, "PduRDestPduRRef")]
35
+ yield rp, src, [d for d in dests if d is not None]
36
+
37
+
38
+ @rule("H2L-PDUR-001", "error", CITE, "a routing path's destination carries the same I-PDU as its source")
39
+ def route_vs_extract(ctx):
40
+ if not ctx.p.system.by_path:
41
+ return
42
+ gw = _gateway_map(ctx)
43
+ sysi = ctx.p.system
44
+ for rp, src, dests in _paths(ctx):
45
+ spdu = _pdu(ctx, src, "PduRSrcPduRef")
46
+ s_ip = system_ipdus(ctx, spdu)
47
+ if not s_ip:
48
+ continue
49
+ s_sig = {x for ip in s_ip for x in sysi.signals_of_ipdu(ip)}
50
+ for d in dests:
51
+ dpdu = _pdu(ctx, d, "PduRDestPduRef")
52
+ d_ip = system_ipdus(ctx, dpdu)
53
+ if not d_ip or set(d_ip) & set(s_ip):
54
+ continue
55
+ d_sig = {x for ip in d_ip for x in sysi.signals_of_ipdu(ip)}
56
+ if s_sig and s_sig == d_sig:
57
+ continue
58
+ src_trig = [t.path for _pv, t in foreign_targets(ctx, spdu)]
59
+ if any(t in gw for t in src_trig):
60
+ allowed = set().union(*(gw[t] for t in src_trig if t in gw))
61
+ dst_trig = {t.path for _pv, t in foreign_targets(ctx, dpdu)}
62
+ if allowed & dst_trig:
63
+ continue
64
+ pv = ctx.value(rp, "PduRDestPduRRef")
65
+ others = [o.name for o, s2, ds in _paths(ctx) if o.path != rp.path and any(x.path == d.path for x in ds)]
66
+ yield rp, pv.node.line, (
67
+ f"{rp.name}: source {spdu.name if spdu else src.name} carries system I-PDU {', '.join(s_ip)} "
68
+ f"(channel {', '.join(channels(ctx, spdu)) or '?'}) but destination {d.name} -> "
69
+ f"{dpdu.name if dpdu else '?'} carries {', '.join(d_ip)} (channel {', '.join(channels(ctx, dpdu)) or '?'}), "
70
+ f"with different I-SIGNALs ({len(s_sig)} vs {len(d_sig)}, {len(s_sig & d_sig)} shared)"
71
+ + (f"; that destination is also the target of {', '.join(others)}" if others else "")), pv.def_ref
72
+
73
+
74
+ @rule("H2L-PDUR-002", "warning", "SWS_PduR routing path (one source per destination PDU)",
75
+ "a destination PDU is fed by more than one routing path")
76
+ def fan_in(ctx):
77
+ feeds: dict = {}
78
+ for rp, src, dests in _paths(ctx):
79
+ for d in dests:
80
+ feeds.setdefault(d.path, []).append((rp, src))
81
+ for dpath, rps in feeds.items():
82
+ srcs = {s.path for _r, s in rps if s is not None}
83
+ if len(srcs) > 1:
84
+ for rp, _s in rps:
85
+ pv = ctx.value(rp, "PduRDestPduRRef")
86
+ yield rp, pv.node.line, (f"{rp.name}: destination {dpath} is also fed by "
87
+ f"{', '.join(r.name for r, _ in rps if r.path != rp.path)} (different sources)"), pv.def_ref
@@ -0,0 +1,162 @@
1
+ """ECU configuration vs the System / ECU extract (the ECUC must agree with the extract it was derived from).
2
+
3
+ Links are discovered from the configuration's own foreign references and the extract's metaclasses:
4
+ ComSignal --(any foreign ref: I-SIGNAL-TO-I-PDU-MAPPING | I-SIGNAL | SYSTEM-SIGNAL)--> mapping;
5
+ EcuC Pdu --(any foreign ref: PDU-TRIGGERING | *FRAME-TRIGGERING | *I-PDU)--> system I-PDU.
6
+ No element names are assumed.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from typing import Optional
11
+
12
+ from . import rule
13
+ from ..values import parse_int
14
+
15
+ ENDIAN = {"MOST-SIGNIFICANT-BYTE-LAST": "LITTLE_ENDIAN", "MOST-SIGNIFICANT-BYTE-FIRST": "BIG_ENDIAN", "OPAQUE": "OPAQUE"}
16
+ CITE_SIG = ("System Template ISignalToIPduMapping.startPosition/packingByteOrder, ISignal.length vs "
17
+ "ECUC_Com_00259 ComBitPosition, ECUC_Com_00158 ComBitSize, ECUC_Com_00157 ComSignalEndianness")
18
+ CITE_PDU = "System Template IPdu.length vs ECUC_EcuC_00003 PduLength"
19
+
20
+
21
+ def foreign_targets(ctx, e):
22
+ """(param value, system Ident) for every reference of an ECUC element that lands in the system index."""
23
+ out = []
24
+ for g in ctx.p.find(e.path):
25
+ for pv in g.params:
26
+ if pv.is_ref and pv.value:
27
+ t = ctx.p.system.get(pv.value)
28
+ if t is not None:
29
+ out.append((pv, t))
30
+ return out
31
+
32
+
33
+ def system_ipdus(ctx, pdu) -> list:
34
+ """EcuC Pdu container -> system I-PDU paths it stands for."""
35
+ if pdu is None:
36
+ return []
37
+ out = []
38
+ for _pv, t in foreign_targets(ctx, pdu):
39
+ if t.tag.endswith("TRIGGERING"):
40
+ out.extend(ctx.p.system.ipdus_of_triggering(t.path))
41
+ elif t.tag.endswith("PDU"):
42
+ out.append(t.path)
43
+ return sorted(set(out))
44
+
45
+
46
+ def channels(ctx, pdu) -> list:
47
+ if pdu is None:
48
+ return []
49
+ return sorted({ctx.p.system.channel_of(t.path) for _pv, t in foreign_targets(ctx, pdu)
50
+ if t.tag.endswith("TRIGGERING") and ctx.p.system.channel_of(t.path)})
51
+
52
+
53
+ def owning_ipdus(ctx):
54
+ """ComSignal path -> list of ComIPdu Elems that reference it."""
55
+ own: dict = {}
56
+ for ip in ctx.containers("Com/ComConfig/ComIPdu"):
57
+ for pv in ctx.values(ip, "ComIPduSignalRef"):
58
+ if pv.value:
59
+ own.setdefault(pv.value.strip(), []).append(ip)
60
+ return own
61
+
62
+
63
+ def mappings_for(ctx, sig, sys_ipdus: list) -> list:
64
+ sysi = ctx.p.system
65
+ out = []
66
+ for _pv, t in foreign_targets(ctx, sig):
67
+ if t.tag == "I-SIGNAL-TO-I-PDU-MAPPING":
68
+ out.append(t)
69
+ elif t.tag in ("I-SIGNAL", "SYSTEM-SIGNAL"):
70
+ isigs = [t.path] if t.tag == "I-SIGNAL" else [i.path for i in sysi.refs_to(t.path) if i.tag == "I-SIGNAL"]
71
+ for isig in isigs:
72
+ for m in sysi.refs_to(isig):
73
+ if m.tag == "I-SIGNAL-TO-I-PDU-MAPPING" and (not sys_ipdus or m.parent in sys_ipdus):
74
+ out.append(m)
75
+ return out
76
+
77
+
78
+ def _motorola_msb(lsb: int, size: int) -> int:
79
+ b = lsb
80
+ for _ in range(size - 1):
81
+ b = b + 1 if b % 8 != 7 else b - 15
82
+ return b
83
+
84
+
85
+ def _line(m) -> str:
86
+ return f"{m.file.rsplit('/', 1)[-1]}:{m.line}"
87
+
88
+
89
+ @rule("H2L-SYS-001", "error", CITE_SIG, "ComSignal layout and I-PDU agree with the system extract mapping")
90
+ def signal_vs_extract(ctx):
91
+ if not ctx.p.system.by_path:
92
+ return
93
+ own = owning_ipdus(ctx)
94
+ for sig in ctx.containers("Com/ComConfig/ComSignal") + ctx.containers("Com/ComConfig/ComSignalGroup/ComGroupSignal"):
95
+ ipdus = own.get(sig.path, [])
96
+ sys_ipdus = sorted({x for ip in ipdus for x in system_ipdus(ctx, ctx.target(ctx.value(ip, "ComPduIdRef")))})
97
+ maps = mappings_for(ctx, sig, sys_ipdus)
98
+ if not maps:
99
+ continue # no link: counted by the info rule below
100
+ m = maps[0]
101
+ isig = ctx.p.system.get(m.one("I-SIGNAL-REF"))
102
+ pos, size = parse_int(ctx.text(sig, "ComBitPosition") or ""), parse_int(ctx.text(sig, "ComBitSize") or "")
103
+ if size is None:
104
+ n = parse_int(ctx.text(sig, "ComSignalLength") or "")
105
+ size = n * 8 if n is not None else None
106
+ start = parse_int(m.one("START-POSITION") or "")
107
+ length = parse_int(isig.one("LENGTH") or "") if isig is not None else None
108
+ order = m.one("PACKING-BYTE-ORDER")
109
+ endian = (ctx.text(sig, "ComSignalEndianness") or "").strip()
110
+ anchor = ctx.value(sig, "ComBitPosition") or ctx.value(sig, "ComBitSize")
111
+ line = anchor.node.line if anchor is not None else sig.line
112
+ ev = f"extract {m.path} ({_line(m)}): START-POSITION={start}, PACKING-BYTE-ORDER={order}, " \
113
+ f"I-SIGNAL LENGTH={length}"
114
+ if pos is not None and start is not None and size:
115
+ ok = pos == start
116
+ if order == "MOST-SIGNIFICANT-BYTE-FIRST":
117
+ ok = ok or start == _motorola_msb(pos, size) or pos == _motorola_msb(start, size)
118
+ if not ok:
119
+ yield sig, line, f"{sig.name}: ComBitPosition={pos} but the system extract maps it at START-POSITION={start}; {ev}", \
120
+ ctx.value(sig, "ComBitPosition").def_ref
121
+ if size is not None and length is not None and size != length:
122
+ yield sig, line, f"{sig.name}: ComBitSize={size} but I-SIGNAL LENGTH={length} bits; {ev}", \
123
+ (ctx.value(sig, "ComBitSize") or ctx.value(sig, "ComSignalLength")).def_ref
124
+ if order in ENDIAN and endian and ENDIAN[order] != endian:
125
+ yield sig, line, f"{sig.name}: ComSignalEndianness={endian} but PACKING-BYTE-ORDER={order} ({ENDIAN[order]}); {ev}", \
126
+ ctx.value(sig, "ComSignalEndianness").def_ref
127
+ if sys_ipdus and m.parent not in sys_ipdus:
128
+ yield sig, sig.line, (f"{sig.name} is in ComIPdu {', '.join(i.name for i in ipdus)} (system I-PDU "
129
+ f"{', '.join(sys_ipdus)}) but the extract maps its I-SIGNAL into {m.parent} ({_line(m)})"), sig.def_ref
130
+
131
+
132
+ @rule("H2L-SYS-002", "error", CITE_PDU, "EcuC PduLength agrees with the system I-PDU LENGTH")
133
+ def pdu_length_vs_extract(ctx):
134
+ if not ctx.p.system.by_path:
135
+ return
136
+ for pdu in ctx.containers("EcuC/EcucConfigSet/EcucPduCollection/Pdu"):
137
+ length = parse_int(ctx.text(pdu, "PduLength") or "")
138
+ if length is None:
139
+ continue
140
+ for ip in system_ipdus(ctx, pdu):
141
+ si = ctx.p.system.get(ip)
142
+ sl = parse_int(si.one("LENGTH") or "") if si is not None else None
143
+ if sl is not None and sl != length:
144
+ pv = ctx.value(pdu, "PduLength")
145
+ yield pdu, pv.node.line, (f"{pdu.name}: PduLength={length} but system I-PDU {ip} has LENGTH={sl} "
146
+ f"({_line(si)})"), pv.def_ref
147
+
148
+
149
+ @rule("H2L-SYS-000", "info", "ECU extract link coverage", "how much of the config could be linked to the extract")
150
+ def coverage(ctx):
151
+ first: Optional[object] = next((m for m in ctx.p.modules if (m.def_ref or "").endswith("/Com")), None)
152
+ if first is None:
153
+ return
154
+ if not ctx.p.system.by_path:
155
+ yield first, first.line, ("no System/ECU extract loaded (no non-ECUC .arxml found or configured as 'input'); "
156
+ "H2L-SYS/H2L-PDUR cross-checks were skipped"), None
157
+ return
158
+ sigs = ctx.containers("Com/ComConfig/ComSignal")
159
+ unlinked = [s for s in sigs if not mappings_for(ctx, s, [])]
160
+ if unlinked:
161
+ yield first, first.line, (f"{len(unlinked)} of {len(sigs)} ComSignal(s) have no reference into the loaded "
162
+ f"extract (e.g. {', '.join(s.path for s in unlinked[:3])}); H2L-SYS-001 skipped for them"), None
@@ -0,0 +1,152 @@
1
+ """Index of the non-ECUC ARXML (System / ECU extract, SWCs, BSWMD) for cross-checks.
2
+
3
+ One expat pass per file. Every identifiable (element with a SHORT-NAME) becomes an Ident with its tag,
4
+ file, line, the identifiable children, and the leaf values *of its own* (descendants that are not
5
+ inside a nested identifiable): e.g. an I-SIGNAL-TO-I-PDU-MAPPING gets {"I-SIGNAL-REF": [...],
6
+ "START-POSITION": ["16"], "PACKING-BYTE-ORDER": [...]}. References keep their DEST type.
7
+ Nothing is keyed by project-specific names; rules navigate by AUTOSAR metaclass and reference.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import xml.parsers.expat as expat
12
+ from dataclasses import dataclass, field
13
+ from typing import Optional
14
+
15
+
16
+ @dataclass
17
+ class Ident:
18
+ path: str
19
+ tag: str
20
+ file: str
21
+ line: int
22
+ values: dict = field(default_factory=dict) # leaf tag -> [text]
23
+ dests: dict = field(default_factory=dict) # ref tag -> [DEST]
24
+ children: list = field(default_factory=list) # child identifiable paths
25
+ parent: Optional[str] = None
26
+
27
+ def one(self, tag: str) -> Optional[str]:
28
+ v = self.values.get(tag)
29
+ return v[0] if v else None
30
+
31
+
32
+ class SystemIndex:
33
+ def __init__(self):
34
+ self.by_path: dict = {}
35
+ self.by_tag: dict = {}
36
+ self._refs_to: Optional[dict] = None
37
+
38
+ def add_file(self, path: str):
39
+ try:
40
+ with open(path, "rb") as fh:
41
+ data = fh.read()
42
+ except OSError:
43
+ return
44
+ p = expat.ParserCreate()
45
+ p.buffer_text = True
46
+ stack: list = [] # [tag, Ident|None, text parts, attrs, line]
47
+ idents: list = [] # current chain of Idents
48
+
49
+ def start(name, attrs):
50
+ stack.append([name.rsplit(":", 1)[-1], None, [], attrs, p.CurrentLineNumber])
51
+
52
+ def chars(t):
53
+ if stack:
54
+ stack[-1][2].append(t)
55
+
56
+ def end(name):
57
+ tag, ident, text, attrs, line = stack.pop()
58
+ if tag == "SHORT-NAME" and stack:
59
+ owner = stack[-1]
60
+ if owner[1] is None:
61
+ parent = idents[-1] if idents else None
62
+ sn = "".join(text).strip()
63
+ ip = f"{parent.path if parent else ''}/{sn}"
64
+ owner[1] = Ident(ip, owner[0], path, owner[4], parent=parent.path if parent else None)
65
+ if parent is not None:
66
+ parent.children.append(ip)
67
+ idents.append(owner[1])
68
+ self.by_path.setdefault(ip, owner[1])
69
+ self.by_tag.setdefault(owner[0], []).append(owner[1])
70
+ return
71
+ if ident is not None:
72
+ idents.pop()
73
+ return
74
+ if idents: # a leaf value of the innermost enclosing identifiable
75
+ txt = "".join(text).strip()
76
+ if txt:
77
+ cur = idents[-1]
78
+ cur.values.setdefault(tag, []).append(txt)
79
+ if "DEST" in attrs:
80
+ cur.dests.setdefault(tag, []).append(attrs["DEST"])
81
+
82
+ p.StartElementHandler = start
83
+ p.EndElementHandler = end
84
+ p.CharacterDataHandler = chars
85
+ try:
86
+ p.Parse(data, True)
87
+ except expat.ExpatError:
88
+ pass
89
+ self._refs_to = None
90
+
91
+ def get(self, path: Optional[str]) -> Optional[Ident]:
92
+ return self.by_path.get(path.strip()) if path else None
93
+
94
+ def tags(self, tag: str) -> list:
95
+ return self.by_tag.get(tag, [])
96
+
97
+ def refs_to(self, path: str) -> list:
98
+ """Idents that reference ``path`` (any *-REF leaf)."""
99
+ if self._refs_to is None:
100
+ idx: dict = {}
101
+ for i in self.by_path.values():
102
+ for t, vals in i.values.items():
103
+ if t.endswith("-REF") or t.endswith("-TREF"):
104
+ for v in vals:
105
+ idx.setdefault(v, []).append(i)
106
+ self._refs_to = idx
107
+ return self._refs_to.get(path, [])
108
+
109
+ def foreign(self) -> dict:
110
+ return {p: (i.tag, i.line, i.file) for p, i in self.by_path.items()}
111
+
112
+ # ---- navigation helpers (by metaclass, not by name) ----
113
+ def ipdus_of_triggering(self, path: Optional[str]) -> list:
114
+ """A PDU-TRIGGERING or FRAME-TRIGGERING (any bus) -> system I-PDU paths it carries."""
115
+ t = self.get(path)
116
+ if t is None:
117
+ return []
118
+ out = []
119
+ for ref in t.values.get("I-PDU-REF", []):
120
+ out.append(ref)
121
+ for pt in t.values.get("PDU-TRIGGERING-REF", []):
122
+ out.extend(self.ipdus_of_triggering(pt))
123
+ if not out:
124
+ fr = self.get(t.one("FRAME-REF"))
125
+ if fr is not None:
126
+ for m in fr.children:
127
+ mi = self.get(m)
128
+ if mi is not None:
129
+ out.extend(mi.values.get("PDU-REF", []))
130
+ seen, res = set(), []
131
+ for x in out:
132
+ if x not in seen:
133
+ seen.add(x)
134
+ res.append(x)
135
+ return res
136
+
137
+ def channel_of(self, path: Optional[str]) -> Optional[str]:
138
+ """Physical channel (the identifiable holding the triggering) of a PDU/FRAME-TRIGGERING."""
139
+ t = self.get(path)
140
+ return t.parent if t is not None else None
141
+
142
+ def signals_of_ipdu(self, ipdu: str) -> list:
143
+ """I-SIGNAL paths mapped into a system I-PDU (via its I-SIGNAL-TO-I-PDU-MAPPING children)."""
144
+ i = self.get(ipdu)
145
+ if i is None:
146
+ return []
147
+ out = []
148
+ for c in i.children:
149
+ ci = self.get(c)
150
+ if ci is not None and ci.values.get("I-SIGNAL-REF"):
151
+ out.extend(ci.values["I-SIGNAL-REF"])
152
+ return sorted(set(out))
@@ -64,7 +64,8 @@ def _resolve_def(defs: D.DefIndex, ref: Optional[str]) -> Optional[D.DefNode]:
64
64
  return None
65
65
 
66
66
 
67
- def validate(project: Project, files: Optional[list] = None, module: Optional[str] = None) -> dict:
67
+ def validate(project: Project, files: Optional[list] = None, module: Optional[str] = None,
68
+ check_generated: Optional[bool] = None, code: bool = True) -> dict:
68
69
  defs = project.defs
69
70
  dg = Diags(project)
70
71
  warned_modules = set()
@@ -211,6 +212,18 @@ def validate(project: Project, files: Optional[list] = None, module: Optional[st
211
212
 
212
213
  from .rules import run_rules
213
214
  dg.items.extend(run_rules(project))
215
+ from .probe import load_cfg
216
+ cfg = load_cfg(project.root)
217
+ if code: # configuration completeness seen from the C code: handles used but not configured
218
+ from .audit import audit
219
+ for f in audit(project).get("findings", []):
220
+ if f["rule"] == "H2L-API-002":
221
+ dg.items.append({"file": f["file"], "line": f["line"], "path": "", "rule": f["rule"],
222
+ "severity": f["severity"], "message": f"{f['call']}: {f['message']} [{f['cite']}]",
223
+ "defRef": None})
224
+ from . import gen
225
+ if check_generated if check_generated is not None else gen.default_on(project, cfg):
226
+ dg.items.extend(gen.check(project, cfg))
214
227
 
215
228
  items = dg.items
216
229
  loaded = sorted(project.files)
@@ -228,7 +241,7 @@ def validate(project: Project, files: Optional[list] = None, module: Optional[st
228
241
  f"file(s) [{sample or 'none'}]; NOTHING was validated for it. Pass a file, "
229
242
  f"directory or glob relative to {project.root}, or omit 'files'."))
230
243
  want = {project.rel(f) for f in matched}
231
- items = [i for i in items if i["file"] in want]
244
+ items = [i for i in items if i["file"] in want or not i["file"].lower().endswith(".arxml")]
232
245
  scope["filter"] = {"files": list(files), "matched": sorted(want)[:200], "matchedCount": len(want)}
233
246
  if module:
234
247
  names = {m.name for m in project.modules} | {(m.def_ref or "").rsplit("/", 1)[-1] for m in project.modules}