@h2loop/autosar 0.0.0-stage → 1.0.1
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/.hydron-plugin/plugin.json +3 -0
- package/README.md +53 -2
- package/agents/autosar-integrator.md +44 -0
- package/commands/autosar-trace.md +11 -0
- package/commands/autosar-validate.md +12 -0
- package/package.json +44 -4
- package/plugins/index.js +26 -0
- package/python/ecuc_engine/__init__.py +2 -0
- package/python/ecuc_engine/__main__.py +5 -0
- package/python/ecuc_engine/audit.py +475 -0
- package/python/ecuc_engine/cli.py +125 -0
- package/python/ecuc_engine/defs.py +330 -0
- package/python/ecuc_engine/diff.py +119 -0
- package/python/ecuc_engine/ecucjson.py +154 -0
- package/python/ecuc_engine/edit.py +429 -0
- package/python/ecuc_engine/model.py +333 -0
- package/python/ecuc_engine/paths.py +221 -0
- package/python/ecuc_engine/rules/__init__.py +82 -0
- package/python/ecuc_engine/rules/com.py +107 -0
- package/python/ecuc_engine/rules/handles.py +42 -0
- package/python/ecuc_engine/validate.py +263 -0
- package/python/ecuc_engine/values.py +146 -0
- package/python/ecuc_engine/xmltree.py +141 -0
- package/python/pyproject.toml +13 -0
- package/skills/autosar/SKILL.md +103 -0
- package/skills/autosar/references/config-classes.md +23 -0
- package/skills/autosar/references/ecuc-model.md +42 -0
- package/skills/autosar/references/project-layout.md +32 -0
- package/skills/autosar/references/symbolic-names.md +25 -0
- package/skills/autosar/references/workflow.md +29 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""COM I-PDU packing rules: signals inside the I-PDU, no overlapping signals."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from . import rule
|
|
7
|
+
from ..values import parse_int
|
|
8
|
+
|
|
9
|
+
TYPE_BITS = {"BOOLEAN": 1, "UINT8": 8, "SINT8": 8, "UINT16": 16, "SINT16": 16, "UINT32": 32, "SINT32": 32,
|
|
10
|
+
"UINT64": 64, "SINT64": 64, "FLOAT32": 32, "FLOAT64": 64}
|
|
11
|
+
CITE_BOUNDS = "ECUC_Com_00259 ComBitPosition (position within the I-PDU), ECUC_Com_00158 ComBitSize, ECUC_EcuC_00003 PduLength"
|
|
12
|
+
CITE_OVERLAP = "SWS_Com_CONSTR_00102 Signals Separated in I-PDU"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _size(ctx, s) -> Optional[int]:
|
|
16
|
+
bs = parse_int(ctx.text(s, "ComBitSize") or "")
|
|
17
|
+
if bs is not None:
|
|
18
|
+
return bs
|
|
19
|
+
typ = (ctx.text(s, "ComSignalType") or "").strip()
|
|
20
|
+
if typ in ("UINT8_N", "UINT8_DYN"):
|
|
21
|
+
n = parse_int(ctx.text(s, "ComSignalLength") or "")
|
|
22
|
+
return n * 8 if n is not None else None
|
|
23
|
+
return TYPE_BITS.get(typ)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def bits(pos: int, size: int, endian: str) -> list:
|
|
27
|
+
"""Occupied bit indices (byte*8 + bit). ComBitPosition is the LSB. BIG_ENDIAN (Motorola) walks
|
|
28
|
+
toward the MSB by going up within a byte, then to bit 0 of the previous byte."""
|
|
29
|
+
if endian == "BIG_ENDIAN":
|
|
30
|
+
out, b = [], pos
|
|
31
|
+
for _ in range(size):
|
|
32
|
+
out.append(b)
|
|
33
|
+
b = b + 1 if b % 8 != 7 else b - 15
|
|
34
|
+
return out
|
|
35
|
+
return list(range(pos, pos + size)) # LITTLE_ENDIAN and OPAQUE (bit0 of first byte, little-endian order)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _ipdus(ctx):
|
|
39
|
+
"""(ipdu, length_bytes|None, [(member, kind)]) for every ComIPdu."""
|
|
40
|
+
for ipdu in ctx.containers("Com/ComConfig/ComIPdu"):
|
|
41
|
+
pdu = ctx.target(ctx.value(ipdu, "ComPduIdRef"))
|
|
42
|
+
length = parse_int(ctx.text(pdu, "PduLength") or "") if pdu is not None else None
|
|
43
|
+
members = []
|
|
44
|
+
for ref in ctx.values(ipdu, "ComIPduSignalRef"):
|
|
45
|
+
s = ctx.target(ref)
|
|
46
|
+
if s is not None:
|
|
47
|
+
members.append(s)
|
|
48
|
+
for ref in ctx.values(ipdu, "ComIPduSignalGroupRef"):
|
|
49
|
+
grp = ctx.target(ref)
|
|
50
|
+
if grp is None:
|
|
51
|
+
continue
|
|
52
|
+
members.extend(c for g in ctx.p.find(grp.path) for c in g.children
|
|
53
|
+
if ctx.rel(c.def_ref).endswith("/ComGroupSignal"))
|
|
54
|
+
members.append(grp) # contributes only its update bit
|
|
55
|
+
yield ipdu, length, pdu, members
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _layout(ctx, s):
|
|
59
|
+
"""[(label, bitset, line)] a signal/group occupies: its value bits and its update bit."""
|
|
60
|
+
out = []
|
|
61
|
+
pos = parse_int(ctx.text(s, "ComBitPosition") or "")
|
|
62
|
+
size = _size(ctx, s)
|
|
63
|
+
if pos is not None and size:
|
|
64
|
+
endian = (ctx.text(s, "ComSignalEndianness") or "LITTLE_ENDIAN").strip()
|
|
65
|
+
out.append((s.name, bits(pos, size, endian), ctx.value(s, "ComBitPosition").node.line, pos, size, endian))
|
|
66
|
+
ub = parse_int(ctx.text(s, "ComUpdateBitPosition") or "")
|
|
67
|
+
if ub is not None:
|
|
68
|
+
out.append((f"{s.name}.updateBit", [ub], ctx.value(s, "ComUpdateBitPosition").node.line, ub, 1, "LITTLE_ENDIAN"))
|
|
69
|
+
return out
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@rule("H2L-COM-001", "error", CITE_BOUNDS, "signal bits lie inside its I-PDU (PduLength * 8)")
|
|
73
|
+
def signal_in_pdu(ctx):
|
|
74
|
+
for ipdu, length, pdu, members in _ipdus(ctx):
|
|
75
|
+
if length is None:
|
|
76
|
+
continue
|
|
77
|
+
limit = length * 8
|
|
78
|
+
for s in members:
|
|
79
|
+
for label, bs, line, pos, size, endian in _layout(ctx, s):
|
|
80
|
+
lo, hi = min(bs), max(bs)
|
|
81
|
+
if lo < 0 or hi >= limit:
|
|
82
|
+
yield s, line, (f"{label}: ComBitPosition={pos} ComBitSize={size} ({endian}) occupies bits {lo}..{hi}, "
|
|
83
|
+
f"outside I-PDU {ipdu.name} ({pdu.path} PduLength={length} bytes = bits 0..{limit - 1})"), \
|
|
84
|
+
(ctx.value(s, "ComBitPosition") or ctx.value(s, "ComUpdateBitPosition")).def_ref
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@rule("H2L-COM-002", "error", CITE_OVERLAP, "signals of one I-PDU do not overlap")
|
|
88
|
+
def no_overlap(ctx):
|
|
89
|
+
for ipdu, length, pdu, members in _ipdus(ctx):
|
|
90
|
+
owner: dict = {}
|
|
91
|
+
reported = set()
|
|
92
|
+
for s in members:
|
|
93
|
+
for label, bs, line, pos, size, endian in _layout(ctx, s):
|
|
94
|
+
for b in bs:
|
|
95
|
+
prev = owner.get(b)
|
|
96
|
+
if prev is None:
|
|
97
|
+
owner[b] = (label, s)
|
|
98
|
+
continue
|
|
99
|
+
if prev[0] == label:
|
|
100
|
+
continue
|
|
101
|
+
key = tuple(sorted((prev[0], label)))
|
|
102
|
+
if key in reported:
|
|
103
|
+
continue
|
|
104
|
+
reported.add(key)
|
|
105
|
+
yield s, line, (f"{label} overlaps {prev[0]} at bit {b} in I-PDU {ipdu.name} "
|
|
106
|
+
f"({label}: pos {pos} size {size} {endian})"), \
|
|
107
|
+
(ctx.value(s, "ComBitPosition") or ctx.value(s, "ComUpdateBitPosition")).def_ref
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Handle-ID uniqueness (TPS_ECUConfiguration 3.4.1.1 "Handle ID concept")."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
from . import rule
|
|
7
|
+
from ..values import parse_int
|
|
8
|
+
|
|
9
|
+
# Handle-ID params by name when no defs say so; with defs, any SYMBOLIC-NAME-VALUE integer counts.
|
|
10
|
+
NAME = re.compile(r"(HandleId|^CanIfTxPduId|^CanIfRxPduId)$")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@rule("H2L-HID-001", "error",
|
|
14
|
+
"TPS_ECUConfiguration 3.4.1.1 Handle ID concept; ECUC_Com_00165/00175, ECUC_PduR_00322, ECUC_CanIf_00591/00597",
|
|
15
|
+
"handle IDs are unique per module and handle-ID parameter")
|
|
16
|
+
def unique_handle_ids(ctx):
|
|
17
|
+
seen: dict = {} # (module path, rel def) -> {value: [(elem, pv)]}
|
|
18
|
+
for path, group in ctx.p.by_path.items():
|
|
19
|
+
e = group[0]
|
|
20
|
+
if e.kind != "container":
|
|
21
|
+
continue
|
|
22
|
+
mod = ctx.module_of(e)
|
|
23
|
+
for g in group:
|
|
24
|
+
for pv in g.params:
|
|
25
|
+
if pv.is_ref or pv.value is None:
|
|
26
|
+
continue
|
|
27
|
+
d = ctx.defs.get(pv.def_ref)
|
|
28
|
+
is_handle = (d.kind == "INTEGER" and d.symbolic_name) if d is not None else False
|
|
29
|
+
if not is_handle and not NAME.search(pv.name):
|
|
30
|
+
continue
|
|
31
|
+
v = parse_int(pv.value)
|
|
32
|
+
if v is None:
|
|
33
|
+
continue
|
|
34
|
+
key = (mod.path, ctx.rel(pv.def_ref))
|
|
35
|
+
seen.setdefault(key, {}).setdefault(v, []).append((g, pv))
|
|
36
|
+
for (mod, rel), by_val in seen.items():
|
|
37
|
+
for v, hits in by_val.items():
|
|
38
|
+
if len(hits) < 2:
|
|
39
|
+
continue
|
|
40
|
+
names = ", ".join(sorted({h[0].name for h in hits}))
|
|
41
|
+
for g, pv in hits:
|
|
42
|
+
yield g, pv.node.line, f"{pv.name}={v} is used by {len(hits)} containers in {mod} ({names}); handle IDs must be unique", pv.def_ref
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""Mechanical ECUC checks, derived from the param defs only (no hand-written module rules).
|
|
2
|
+
|
|
3
|
+
Diagnostics are LSP-like: {file, line, path, rule, severity, message, defRef}.
|
|
4
|
+
Rules:
|
|
5
|
+
ECUC-NO-DEF module's definition is not among the loaded param defs (module skipped)
|
|
6
|
+
ECUC-UNKNOWN-DEF container / parameter / reference whose DEFINITION-REF is not in the module def
|
|
7
|
+
ECUC-WRONG-PARENT container's def is not a child of its parent's def
|
|
8
|
+
ECUC-DEST-MISMATCH DEFINITION-REF DEST attribute differs from the def element type
|
|
9
|
+
ECUC-VALUE-TYPE NUMERICAL vs TEXTUAL value element does not match the def kind
|
|
10
|
+
ECUC-MULT-CONTAINER sub-container count outside the def multiplicity (incl. missing mandatory)
|
|
11
|
+
ECUC-CHOICE choice container does not hold exactly one choice
|
|
12
|
+
ECUC-MULT-PARAM parameter / reference count outside the def multiplicity
|
|
13
|
+
ECUC-MISSING-PARAM mandatory parameter / reference absent
|
|
14
|
+
ECUC-TYPE / ECUC-RANGE / ECUC-ENUM / ECUC-STRING / ECUC-EMPTY-VALUE value checks
|
|
15
|
+
ECUC-REF-DANGLING reference target does not exist
|
|
16
|
+
ECUC-REF-DEST reference target is of the wrong definition / metaclass
|
|
17
|
+
ECUC-REF-UNRESOLVED foreign reference target not in any loaded file (warning)
|
|
18
|
+
ECUC-DUP-NAME two containers with the same AUTOSAR path
|
|
19
|
+
ECUC-OBSOLETE value for an obsolete def (warning)
|
|
20
|
+
H2L-FILTER-NOMATCH a files/module filter matched nothing that was loaded (error, never a silent "clean")
|
|
21
|
+
H2L-DISCOVERY discovery notes: hidden dir included, symlink cycle skipped, walk cap (info)
|
|
22
|
+
Hand-written semantic rules (ecuc_engine/rules, ids H2L-*) run afterwards, with or without defs.
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from typing import Optional
|
|
27
|
+
|
|
28
|
+
from . import defs as D
|
|
29
|
+
from . import paths
|
|
30
|
+
from .model import Project
|
|
31
|
+
from .values import check_value, expected_value_tag
|
|
32
|
+
|
|
33
|
+
GENERIC_CONTAINER_DESTS = {"ECUC-CONTAINER-DEF"}
|
|
34
|
+
SEV_ORDER = {"error": 0, "warning": 1, "info": 2, "hint": 3}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Diags:
|
|
38
|
+
def __init__(self, project: Project):
|
|
39
|
+
self.p = project
|
|
40
|
+
self.items: list = []
|
|
41
|
+
|
|
42
|
+
def add(self, file, line, path, rule, severity, message, def_ref=None):
|
|
43
|
+
self.items.append({"file": self.p.rel(file), "line": line, "path": path, "rule": rule,
|
|
44
|
+
"severity": severity, "message": message, "defRef": def_ref})
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _same_def(defs: D.DefIndex, a: Optional[str], b: Optional[str]) -> bool:
|
|
48
|
+
if not a or not b:
|
|
49
|
+
return False
|
|
50
|
+
return a == b or defs.rel(a) == defs.rel(b)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _resolve_def(defs: D.DefIndex, ref: Optional[str]) -> Optional[D.DefNode]:
|
|
54
|
+
if not ref:
|
|
55
|
+
return None
|
|
56
|
+
d = defs.get(ref)
|
|
57
|
+
if d is not None:
|
|
58
|
+
return d
|
|
59
|
+
# vendor-relocated path (/Vendor/Com/... vs /AUTOSAR/EcucDefs/Com/...): match by module-relative path
|
|
60
|
+
rel = defs.rel(ref)
|
|
61
|
+
for m in defs.modules.values():
|
|
62
|
+
if rel.split("/", 1)[0] == m.name:
|
|
63
|
+
return defs.get(m.path + rel[len(m.name):])
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def validate(project: Project, files: Optional[list] = None, module: Optional[str] = None) -> dict:
|
|
68
|
+
defs = project.defs
|
|
69
|
+
dg = Diags(project)
|
|
70
|
+
warned_modules = set()
|
|
71
|
+
visited = set()
|
|
72
|
+
|
|
73
|
+
def fmult(lo, up):
|
|
74
|
+
return f"{lo}..{'*' if up is None else up}"
|
|
75
|
+
|
|
76
|
+
def check_elem_group(path: str):
|
|
77
|
+
if path in visited:
|
|
78
|
+
return
|
|
79
|
+
visited.add(path)
|
|
80
|
+
group = project.find(path)
|
|
81
|
+
e0 = group[0]
|
|
82
|
+
if e0.kind == "container" and len(group) > 1:
|
|
83
|
+
for e in group[1:]:
|
|
84
|
+
dg.add(e.file, e.line, path, "ECUC-DUP-NAME", "error",
|
|
85
|
+
f"duplicate container path {path} (also at {project.rel(e0.file)}:{e0.line})", e.def_ref)
|
|
86
|
+
d = _resolve_def(defs, e0.def_ref)
|
|
87
|
+
if d is None:
|
|
88
|
+
if e0.kind == "module":
|
|
89
|
+
key = e0.def_ref or path
|
|
90
|
+
if key not in warned_modules:
|
|
91
|
+
warned_modules.add(key)
|
|
92
|
+
dg.add(e0.file, e0.line, path, "ECUC-NO-DEF", "warning",
|
|
93
|
+
f"no param def loaded for {e0.def_ref}; module not validated", e0.def_ref)
|
|
94
|
+
else:
|
|
95
|
+
dg.add(e0.file, e0.line, path, "ECUC-UNKNOWN-DEF", "error",
|
|
96
|
+
f"container definition {e0.def_ref} does not exist in the loaded param defs", e0.def_ref)
|
|
97
|
+
return
|
|
98
|
+
if e0.def_dest and e0.def_dest != d.tag and not (e0.def_dest in GENERIC_CONTAINER_DESTS and d.is_container):
|
|
99
|
+
dg.add(e0.file, e0.line, path, "ECUC-DEST-MISMATCH", "warning",
|
|
100
|
+
f"DEFINITION-REF DEST={e0.def_dest} but the def is {d.tag}", d.path)
|
|
101
|
+
if e0.kind == "container" and e0.parent is not None:
|
|
102
|
+
pd = _resolve_def(defs, e0.parent.def_ref)
|
|
103
|
+
if pd is not None and d.parent != pd.path:
|
|
104
|
+
dg.add(e0.file, e0.line, path, "ECUC-WRONG-PARENT", "error",
|
|
105
|
+
f"{d.name} is not a sub-container of {pd.name} (allowed: "
|
|
106
|
+
f"{', '.join(defs.by_path[c].name for c in pd.containers) or 'none'})", d.path)
|
|
107
|
+
if d.obsolete:
|
|
108
|
+
dg.add(e0.file, e0.line, path, "ECUC-OBSOLETE", "warning", f"{d.name} is obsolete", d.path)
|
|
109
|
+
|
|
110
|
+
# children (merged across split fragments)
|
|
111
|
+
children = [c for e in group for c in e.children]
|
|
112
|
+
params = [(e, pv) for e in group for pv in e.params]
|
|
113
|
+
counts: dict = {}
|
|
114
|
+
seen_child_paths = set()
|
|
115
|
+
for c in children:
|
|
116
|
+
if c.path in seen_child_paths:
|
|
117
|
+
continue
|
|
118
|
+
seen_child_paths.add(c.path)
|
|
119
|
+
cd = _resolve_def(defs, c.def_ref)
|
|
120
|
+
if cd is not None:
|
|
121
|
+
counts[cd.path] = counts.get(cd.path, 0) + 1
|
|
122
|
+
if d.tag == D.CHOICE_TAG:
|
|
123
|
+
n = sum(counts.get(c, 0) for c in d.containers)
|
|
124
|
+
if n != 1:
|
|
125
|
+
dg.add(e0.file, e0.line, path, "ECUC-CHOICE", "error",
|
|
126
|
+
f"choice container {d.name} holds {n} choices, needs exactly 1 of "
|
|
127
|
+
f"{', '.join(defs.by_path[c].name for c in d.containers)}", d.path)
|
|
128
|
+
else:
|
|
129
|
+
for cpath in d.containers:
|
|
130
|
+
cd = defs.by_path[cpath]
|
|
131
|
+
n = counts.get(cpath, 0)
|
|
132
|
+
if n < cd.lower or (cd.upper is not None and n > cd.upper):
|
|
133
|
+
dg.add(e0.file, e0.line, path, "ECUC-MULT-CONTAINER", "error",
|
|
134
|
+
f"{d.name} '{e0.name}': {cd.name} has {n} instance(s), def requires {fmult(cd.lower, cd.upper)}",
|
|
135
|
+
cd.path)
|
|
136
|
+
pcounts: dict = {}
|
|
137
|
+
for e, pv in params:
|
|
138
|
+
pd = _resolve_def(defs, pv.def_ref)
|
|
139
|
+
line = pv.node.line
|
|
140
|
+
if pd is None or pd.parent != d.path:
|
|
141
|
+
dg.add(e.file, line, path, "ECUC-UNKNOWN-DEF", "error",
|
|
142
|
+
f"parameter/reference {pv.def_ref} is not defined for {d.name}", pv.def_ref)
|
|
143
|
+
continue
|
|
144
|
+
pcounts[pd.path] = pcounts.get(pd.path, 0) + 1
|
|
145
|
+
if pd.obsolete:
|
|
146
|
+
dg.add(e.file, line, path, "ECUC-OBSOLETE", "warning", f"{pd.name} is obsolete", pd.path)
|
|
147
|
+
if pv.def_dest and pv.def_dest != pd.tag:
|
|
148
|
+
dg.add(e.file, line, path, "ECUC-DEST-MISMATCH", "warning",
|
|
149
|
+
f"{pd.name}: DEFINITION-REF DEST={pv.def_dest} but the def is {pd.tag}", pd.path)
|
|
150
|
+
exp = expected_value_tag(pd)
|
|
151
|
+
if exp and pv.tag != exp:
|
|
152
|
+
dg.add(e.file, line, path, "ECUC-VALUE-TYPE", "warning",
|
|
153
|
+
f"{pd.name} is a {pd.kind} def; value element should be {exp}, found {pv.tag}", pd.path)
|
|
154
|
+
if D.is_ref_kind(pd.kind or ""):
|
|
155
|
+
check_ref(e, pv, pd, path, line)
|
|
156
|
+
elif pd.kind != "ADD-INFO" and not pv.is_auto:
|
|
157
|
+
for rule, msg in check_value(pd, pv.value):
|
|
158
|
+
sev = "warning" if rule == "ECUC-EMPTY-VALUE" else "error"
|
|
159
|
+
dg.add(e.file, (pv.value_node or pv.node).line, path, rule, sev, f"{d.name} '{e0.name}': {msg}", pd.path)
|
|
160
|
+
for ppath in d.params + d.refs:
|
|
161
|
+
pd = defs.by_path[ppath]
|
|
162
|
+
n = pcounts.get(ppath, 0)
|
|
163
|
+
if n == 0 and pd.lower > 0:
|
|
164
|
+
dg.add(e0.file, e0.line, path, "ECUC-MISSING-PARAM", "error",
|
|
165
|
+
f"{d.name} '{e0.name}': mandatory {pd.name} ({pd.kind}, {pd.mult}) is missing", pd.path)
|
|
166
|
+
elif n < pd.lower or (pd.upper is not None and n > pd.upper):
|
|
167
|
+
dg.add(e0.file, e0.line, path, "ECUC-MULT-PARAM", "error",
|
|
168
|
+
f"{d.name} '{e0.name}': {pd.name} has {n} value(s), def requires {pd.mult}", pd.path)
|
|
169
|
+
for c in children:
|
|
170
|
+
check_elem_group(c.path)
|
|
171
|
+
|
|
172
|
+
def check_ref(e, pv, pd, path, line):
|
|
173
|
+
tgt = (pv.value or "").strip()
|
|
174
|
+
if pd.kind in ("INSTANCE-REFERENCE", "URI-REFERENCE"):
|
|
175
|
+
return
|
|
176
|
+
if not tgt:
|
|
177
|
+
dg.add(e.file, line, path, "ECUC-EMPTY-VALUE", "error", f"{pd.name}: empty reference", pd.path)
|
|
178
|
+
return
|
|
179
|
+
if pd.kind == "FOREIGN-REFERENCE":
|
|
180
|
+
hit = project.identifiables.get(tgt)
|
|
181
|
+
if hit is None:
|
|
182
|
+
if project.input_loaded:
|
|
183
|
+
dg.add(e.file, line, path, "ECUC-REF-DANGLING", "error",
|
|
184
|
+
f"{pd.name} -> {tgt}: target not found in any loaded ARXML", pd.path)
|
|
185
|
+
else:
|
|
186
|
+
dg.add(e.file, line, path, "ECUC-REF-UNRESOLVED", "warning",
|
|
187
|
+
f"{pd.name} -> {tgt}: target not in loaded files (add the System/SWC ARXML to "
|
|
188
|
+
f"'input' in .hydron/autosar.json)", pd.path)
|
|
189
|
+
return
|
|
190
|
+
# DESTINATION-TYPE is often an abstract metaclass (RTE-EVENT for TIMING-EVENT), which needs the
|
|
191
|
+
# metamodel to check; the VALUE-REF DEST attribute must name the target's concrete type exactly.
|
|
192
|
+
if pv.dest and pv.dest != hit[0]:
|
|
193
|
+
dg.add(e.file, line, path, "ECUC-REF-DEST", "error",
|
|
194
|
+
f"{pd.name} -> {tgt}: VALUE-REF DEST={pv.dest} but the target is a {hit[0]}", pd.path)
|
|
195
|
+
return
|
|
196
|
+
targets = project.find(tgt)
|
|
197
|
+
if not targets:
|
|
198
|
+
dg.add(e.file, line, path, "ECUC-REF-DANGLING", "error",
|
|
199
|
+
f"{pd.name} -> {tgt}: no ECUC container with that path", pd.path)
|
|
200
|
+
return
|
|
201
|
+
t = targets[0]
|
|
202
|
+
if pd.dest_refs and not any(_same_def(defs, t.def_ref, dr) for dr in pd.dest_refs):
|
|
203
|
+
dg.add(e.file, line, path, "ECUC-REF-DEST", "error",
|
|
204
|
+
f"{pd.name} -> {tgt} is a {t.def_ref.rsplit('/', 1)[-1] if t.def_ref else '?'}, def requires "
|
|
205
|
+
f"{' or '.join(r.rsplit('/', 1)[-1] for r in pd.dest_refs)}", pd.path)
|
|
206
|
+
|
|
207
|
+
for m in project.modules:
|
|
208
|
+
if module and m.name != module and (m.def_ref or "").rsplit("/", 1)[-1] != module:
|
|
209
|
+
continue
|
|
210
|
+
check_elem_group(m.path)
|
|
211
|
+
|
|
212
|
+
from .rules import run_rules
|
|
213
|
+
dg.items.extend(run_rules(project))
|
|
214
|
+
|
|
215
|
+
items = dg.items
|
|
216
|
+
loaded = sorted(project.files)
|
|
217
|
+
by_file: dict = {}
|
|
218
|
+
for m in project.modules:
|
|
219
|
+
by_file.setdefault(project.rel(m.file), set()).add(m.name)
|
|
220
|
+
sample = ", ".join(project.rel(f) for f in loaded[:8]) + (f", … (+{len(loaded) - 8})" if len(loaded) > 8 else "")
|
|
221
|
+
scope: dict = {"root": project.root, "configFiles": len(loaded), "paramDefFiles": len(project.sources.defs),
|
|
222
|
+
"inputFiles": len(project.sources.input)}
|
|
223
|
+
nomatch = []
|
|
224
|
+
if files:
|
|
225
|
+
matched, empty = paths.select(project.root, files, loaded)
|
|
226
|
+
for raw in empty:
|
|
227
|
+
nomatch.append(_nomatch(project, f"files filter {raw!r} matched none of the {len(loaded)} loaded config "
|
|
228
|
+
f"file(s) [{sample or 'none'}]; NOTHING was validated for it. Pass a file, "
|
|
229
|
+
f"directory or glob relative to {project.root}, or omit 'files'."))
|
|
230
|
+
want = {project.rel(f) for f in matched}
|
|
231
|
+
items = [i for i in items if i["file"] in want]
|
|
232
|
+
scope["filter"] = {"files": list(files), "matched": sorted(want)[:200], "matchedCount": len(want)}
|
|
233
|
+
if module:
|
|
234
|
+
names = {m.name for m in project.modules} | {(m.def_ref or "").rsplit("/", 1)[-1] for m in project.modules}
|
|
235
|
+
if module not in names:
|
|
236
|
+
nomatch.append(_nomatch(project, f"module filter {module!r} matches no loaded module "
|
|
237
|
+
f"({', '.join(sorted(names - {''})) or 'none loaded'}); NOTHING was validated."))
|
|
238
|
+
scope["module"] = module
|
|
239
|
+
info = [{"file": "", "line": 0, "path": "", "rule": "H2L-DISCOVERY", "severity": "info", "message": n, "defRef": None}
|
|
240
|
+
for n in project.sources.notes]
|
|
241
|
+
items = nomatch + items + info
|
|
242
|
+
shown = sorted(by_file.items())
|
|
243
|
+
scope["files"] = [{"file": f, "modules": sorted(ms)} for f, ms in shown[:100]]
|
|
244
|
+
if len(shown) > 100:
|
|
245
|
+
scope["filesTruncated"] = len(shown) - 100
|
|
246
|
+
items.sort(key=lambda i: (i["file"], i["line"], SEV_ORDER.get(i["severity"], 9), i["rule"]))
|
|
247
|
+
summary: dict = {}
|
|
248
|
+
for i in items:
|
|
249
|
+
summary[i["severity"]] = summary.get(i["severity"], 0) + 1
|
|
250
|
+
return {
|
|
251
|
+
"validated": bool(defs),
|
|
252
|
+
"rules": "semantic rule packs ran (H2L-*)",
|
|
253
|
+
"defsLoaded": len(defs.modules),
|
|
254
|
+
"modules": sorted({m.name for m in project.modules}),
|
|
255
|
+
"summary": summary,
|
|
256
|
+
"scope": scope,
|
|
257
|
+
"diagnostics": items,
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _nomatch(project: Project, msg: str) -> dict:
|
|
262
|
+
return {"file": "", "line": 0, "path": project.root, "rule": "H2L-FILTER-NOMATCH", "severity": "error",
|
|
263
|
+
"message": msg, "defRef": None}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Typed value checks and ECUC-JSON value rendering, driven by the param def."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import math
|
|
5
|
+
import re
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from .defs import DefNode
|
|
9
|
+
|
|
10
|
+
C_IDENT = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
11
|
+
BOOL_TRUE = {"true", "1"}
|
|
12
|
+
BOOL_FALSE = {"false", "0"}
|
|
13
|
+
JS_SAFE = 2 ** 53 - 1
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def parse_int(s: str) -> Optional[int]:
|
|
17
|
+
s = s.strip()
|
|
18
|
+
try:
|
|
19
|
+
return int(s, 0)
|
|
20
|
+
except ValueError:
|
|
21
|
+
try:
|
|
22
|
+
return int(s) # leading zeros, e.g. "007"
|
|
23
|
+
except ValueError:
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def parse_float(s: str) -> Optional[float]:
|
|
28
|
+
t = s.strip()
|
|
29
|
+
if t.upper() in ("INF", "+INF"):
|
|
30
|
+
return math.inf
|
|
31
|
+
if t.upper() == "-INF":
|
|
32
|
+
return -math.inf
|
|
33
|
+
try:
|
|
34
|
+
return float(t)
|
|
35
|
+
except ValueError:
|
|
36
|
+
iv = parse_int(t)
|
|
37
|
+
return float(iv) if iv is not None else None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _bound(s: Optional[str], f):
|
|
41
|
+
if s is None or s == "":
|
|
42
|
+
return None
|
|
43
|
+
return f(s)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def check_value(d: DefNode, raw: Optional[str]) -> list:
|
|
47
|
+
"""Return [(rule, message)] problems with ``raw`` against ``d``. Empty list = OK."""
|
|
48
|
+
if raw is None or raw == "":
|
|
49
|
+
return [("ECUC-EMPTY-VALUE", f"{d.name}: empty value")]
|
|
50
|
+
k = d.kind
|
|
51
|
+
out = []
|
|
52
|
+
if k == "INTEGER":
|
|
53
|
+
v = parse_int(raw)
|
|
54
|
+
if v is None:
|
|
55
|
+
return [("ECUC-TYPE", f"{d.name}: '{raw}' is not an integer")]
|
|
56
|
+
lo, hi = _bound(d.min, parse_int), _bound(d.max, parse_int)
|
|
57
|
+
if (lo is not None and v < lo) or (hi is not None and v > hi):
|
|
58
|
+
out.append(("ECUC-RANGE", f"{d.name}={raw} outside [{d.min}..{d.max}]"))
|
|
59
|
+
elif k == "FLOAT":
|
|
60
|
+
v = parse_float(raw)
|
|
61
|
+
if v is None or math.isnan(v):
|
|
62
|
+
return [("ECUC-TYPE", f"{d.name}: '{raw}' is not a float")]
|
|
63
|
+
lo, hi = _bound(d.min, parse_float), _bound(d.max, parse_float)
|
|
64
|
+
bad = (lo is not None and (v < lo or (d.min_open and v == lo))) or \
|
|
65
|
+
(hi is not None and (v > hi or (d.max_open and v == hi)))
|
|
66
|
+
if bad:
|
|
67
|
+
out.append(("ECUC-RANGE", f"{d.name}={raw} outside {'(' if d.min_open else '['}{d.min}..{d.max}{')' if d.max_open else ']'}"))
|
|
68
|
+
elif k == "BOOLEAN":
|
|
69
|
+
if raw.strip().lower() not in BOOL_TRUE | BOOL_FALSE:
|
|
70
|
+
out.append(("ECUC-TYPE", f"{d.name}: '{raw}' is not a boolean (true/false/1/0)"))
|
|
71
|
+
elif k == "ENUMERATION":
|
|
72
|
+
if d.literals and raw.strip() not in d.literals:
|
|
73
|
+
lits = ", ".join(d.literals[:20]) + (" …" if len(d.literals) > 20 else "")
|
|
74
|
+
out.append(("ECUC-ENUM", f"{d.name}: '{raw}' is not a literal of the def ({lits})"))
|
|
75
|
+
elif k in ("STRING", "FUNCTION-NAME", "LINKER-SYMBOL", "MULTILINE-STRING"):
|
|
76
|
+
if d.min_len is not None and len(raw) < d.min_len:
|
|
77
|
+
out.append(("ECUC-STRING", f"{d.name}: length {len(raw)} < min {d.min_len}"))
|
|
78
|
+
if d.max_len is not None and len(raw) > d.max_len:
|
|
79
|
+
out.append(("ECUC-STRING", f"{d.name}: length {len(raw)} > max {d.max_len}"))
|
|
80
|
+
if k in ("FUNCTION-NAME", "LINKER-SYMBOL") and not C_IDENT.match(raw.strip()):
|
|
81
|
+
out.append(("ECUC-STRING", f"{d.name}: '{raw}' is not a valid C identifier"))
|
|
82
|
+
if d.regex:
|
|
83
|
+
try:
|
|
84
|
+
if not re.fullmatch(d.regex, raw):
|
|
85
|
+
out.append(("ECUC-STRING", f"{d.name}: '{raw}' does not match /{d.regex}/"))
|
|
86
|
+
except re.error:
|
|
87
|
+
pass
|
|
88
|
+
return out
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def expected_value_tag(d: DefNode) -> Optional[str]:
|
|
92
|
+
k = d.kind or ""
|
|
93
|
+
if k in ("INTEGER", "FLOAT", "BOOLEAN"):
|
|
94
|
+
return "ECUC-NUMERICAL-PARAM-VALUE"
|
|
95
|
+
if k in ("ENUMERATION", "STRING", "FUNCTION-NAME", "LINKER-SYMBOL", "MULTILINE-STRING"):
|
|
96
|
+
return "ECUC-TEXTUAL-PARAM-VALUE"
|
|
97
|
+
if k == "ADD-INFO":
|
|
98
|
+
return "ECUC-ADD-INFO-PARAM-VALUE"
|
|
99
|
+
if k == "INSTANCE-REFERENCE":
|
|
100
|
+
return "ECUC-INSTANCE-REFERENCE-VALUE"
|
|
101
|
+
if k.endswith("REFERENCE"):
|
|
102
|
+
return "ECUC-REFERENCE-VALUE"
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def to_json(d: Optional[DefNode], raw: Optional[str], is_ref: bool, numerical: bool):
|
|
107
|
+
"""Raw ARXML text -> compact JSON value (int/float/bool/str, refs as '->/path')."""
|
|
108
|
+
if raw is None:
|
|
109
|
+
return None
|
|
110
|
+
if is_ref:
|
|
111
|
+
return "->" + raw if raw.startswith("/") else raw
|
|
112
|
+
k = d.kind if d is not None else ("NUMBER" if numerical else None)
|
|
113
|
+
t = raw.strip()
|
|
114
|
+
if k == "BOOLEAN" or (k == "NUMBER" and t.lower() in ("true", "false")):
|
|
115
|
+
lo = t.lower()
|
|
116
|
+
return True if lo in BOOL_TRUE else False if lo in BOOL_FALSE else raw
|
|
117
|
+
if k in ("INTEGER", "NUMBER"):
|
|
118
|
+
v = parse_int(t)
|
|
119
|
+
if v is not None:
|
|
120
|
+
return v if abs(v) <= JS_SAFE and t == str(v) else t # keep hex / huge as text
|
|
121
|
+
if k == "INTEGER":
|
|
122
|
+
return raw
|
|
123
|
+
if k in ("FLOAT", "NUMBER"):
|
|
124
|
+
v = parse_float(t)
|
|
125
|
+
if v is not None and math.isfinite(v):
|
|
126
|
+
return v
|
|
127
|
+
return raw
|
|
128
|
+
return raw
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def from_json(d: Optional[DefNode], v) -> str:
|
|
132
|
+
"""JSON value from an edit op -> ARXML text."""
|
|
133
|
+
if isinstance(v, bool):
|
|
134
|
+
return "true" if v else "false"
|
|
135
|
+
if isinstance(v, (int, float)):
|
|
136
|
+
if isinstance(v, float) and v.is_integer() and d is not None and d.kind == "FLOAT":
|
|
137
|
+
return repr(v)
|
|
138
|
+
return str(v)
|
|
139
|
+
if v is None:
|
|
140
|
+
raise ValueError("null is not a value; use op 'remove' to delete a parameter")
|
|
141
|
+
s = str(v)
|
|
142
|
+
if s.startswith("->"):
|
|
143
|
+
s = s[2:]
|
|
144
|
+
if d is not None and d.kind == "BOOLEAN" and s.lower() in ("true", "false"):
|
|
145
|
+
s = s.lower()
|
|
146
|
+
return s
|