@anik-2/autosar 0.0.0-stage → 0.1.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 +55 -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 +21 -0
- package/python/ecuc_engine/__init__.py +2 -0
- package/python/ecuc_engine/__main__.py +5 -0
- package/python/ecuc_engine/cli.py +111 -0
- package/python/ecuc_engine/defs.py +330 -0
- package/python/ecuc_engine/diff.py +111 -0
- package/python/ecuc_engine/ecucjson.py +154 -0
- package/python/ecuc_engine/edit.py +429 -0
- package/python/ecuc_engine/model.py +330 -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 +227 -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 +70 -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 +29 -0
- package/skills/autosar/references/symbolic-names.md +25 -0
- package/skills/autosar/references/workflow.md +29 -0
|
@@ -0,0 +1,227 @@
|
|
|
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
|
+
Hand-written semantic rules (ecuc_engine/rules, ids H2L-*) run afterwards, with or without defs.
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from typing import Optional
|
|
25
|
+
|
|
26
|
+
from . import defs as D
|
|
27
|
+
from .model import Project
|
|
28
|
+
from .values import check_value, expected_value_tag
|
|
29
|
+
|
|
30
|
+
GENERIC_CONTAINER_DESTS = {"ECUC-CONTAINER-DEF"}
|
|
31
|
+
SEV_ORDER = {"error": 0, "warning": 1, "info": 2, "hint": 3}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Diags:
|
|
35
|
+
def __init__(self, project: Project):
|
|
36
|
+
self.p = project
|
|
37
|
+
self.items: list = []
|
|
38
|
+
|
|
39
|
+
def add(self, file, line, path, rule, severity, message, def_ref=None):
|
|
40
|
+
self.items.append({"file": self.p.rel(file), "line": line, "path": path, "rule": rule,
|
|
41
|
+
"severity": severity, "message": message, "defRef": def_ref})
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _same_def(defs: D.DefIndex, a: Optional[str], b: Optional[str]) -> bool:
|
|
45
|
+
if not a or not b:
|
|
46
|
+
return False
|
|
47
|
+
return a == b or defs.rel(a) == defs.rel(b)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _resolve_def(defs: D.DefIndex, ref: Optional[str]) -> Optional[D.DefNode]:
|
|
51
|
+
if not ref:
|
|
52
|
+
return None
|
|
53
|
+
d = defs.get(ref)
|
|
54
|
+
if d is not None:
|
|
55
|
+
return d
|
|
56
|
+
# vendor-relocated path (/Vendor/Com/... vs /AUTOSAR/EcucDefs/Com/...): match by module-relative path
|
|
57
|
+
rel = defs.rel(ref)
|
|
58
|
+
for m in defs.modules.values():
|
|
59
|
+
if rel.split("/", 1)[0] == m.name:
|
|
60
|
+
return defs.get(m.path + rel[len(m.name):])
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def validate(project: Project, files: Optional[list] = None, module: Optional[str] = None) -> dict:
|
|
65
|
+
defs = project.defs
|
|
66
|
+
dg = Diags(project)
|
|
67
|
+
warned_modules = set()
|
|
68
|
+
visited = set()
|
|
69
|
+
|
|
70
|
+
def fmult(lo, up):
|
|
71
|
+
return f"{lo}..{'*' if up is None else up}"
|
|
72
|
+
|
|
73
|
+
def check_elem_group(path: str):
|
|
74
|
+
if path in visited:
|
|
75
|
+
return
|
|
76
|
+
visited.add(path)
|
|
77
|
+
group = project.find(path)
|
|
78
|
+
e0 = group[0]
|
|
79
|
+
if e0.kind == "container" and len(group) > 1:
|
|
80
|
+
for e in group[1:]:
|
|
81
|
+
dg.add(e.file, e.line, path, "ECUC-DUP-NAME", "error",
|
|
82
|
+
f"duplicate container path {path} (also at {project.rel(e0.file)}:{e0.line})", e.def_ref)
|
|
83
|
+
d = _resolve_def(defs, e0.def_ref)
|
|
84
|
+
if d is None:
|
|
85
|
+
if e0.kind == "module":
|
|
86
|
+
key = e0.def_ref or path
|
|
87
|
+
if key not in warned_modules:
|
|
88
|
+
warned_modules.add(key)
|
|
89
|
+
dg.add(e0.file, e0.line, path, "ECUC-NO-DEF", "warning",
|
|
90
|
+
f"no param def loaded for {e0.def_ref}; module not validated", e0.def_ref)
|
|
91
|
+
else:
|
|
92
|
+
dg.add(e0.file, e0.line, path, "ECUC-UNKNOWN-DEF", "error",
|
|
93
|
+
f"container definition {e0.def_ref} does not exist in the loaded param defs", e0.def_ref)
|
|
94
|
+
return
|
|
95
|
+
if e0.def_dest and e0.def_dest != d.tag and not (e0.def_dest in GENERIC_CONTAINER_DESTS and d.is_container):
|
|
96
|
+
dg.add(e0.file, e0.line, path, "ECUC-DEST-MISMATCH", "warning",
|
|
97
|
+
f"DEFINITION-REF DEST={e0.def_dest} but the def is {d.tag}", d.path)
|
|
98
|
+
if e0.kind == "container" and e0.parent is not None:
|
|
99
|
+
pd = _resolve_def(defs, e0.parent.def_ref)
|
|
100
|
+
if pd is not None and d.parent != pd.path:
|
|
101
|
+
dg.add(e0.file, e0.line, path, "ECUC-WRONG-PARENT", "error",
|
|
102
|
+
f"{d.name} is not a sub-container of {pd.name} (allowed: "
|
|
103
|
+
f"{', '.join(defs.by_path[c].name for c in pd.containers) or 'none'})", d.path)
|
|
104
|
+
if d.obsolete:
|
|
105
|
+
dg.add(e0.file, e0.line, path, "ECUC-OBSOLETE", "warning", f"{d.name} is obsolete", d.path)
|
|
106
|
+
|
|
107
|
+
# children (merged across split fragments)
|
|
108
|
+
children = [c for e in group for c in e.children]
|
|
109
|
+
params = [(e, pv) for e in group for pv in e.params]
|
|
110
|
+
counts: dict = {}
|
|
111
|
+
seen_child_paths = set()
|
|
112
|
+
for c in children:
|
|
113
|
+
if c.path in seen_child_paths:
|
|
114
|
+
continue
|
|
115
|
+
seen_child_paths.add(c.path)
|
|
116
|
+
cd = _resolve_def(defs, c.def_ref)
|
|
117
|
+
if cd is not None:
|
|
118
|
+
counts[cd.path] = counts.get(cd.path, 0) + 1
|
|
119
|
+
if d.tag == D.CHOICE_TAG:
|
|
120
|
+
n = sum(counts.get(c, 0) for c in d.containers)
|
|
121
|
+
if n != 1:
|
|
122
|
+
dg.add(e0.file, e0.line, path, "ECUC-CHOICE", "error",
|
|
123
|
+
f"choice container {d.name} holds {n} choices, needs exactly 1 of "
|
|
124
|
+
f"{', '.join(defs.by_path[c].name for c in d.containers)}", d.path)
|
|
125
|
+
else:
|
|
126
|
+
for cpath in d.containers:
|
|
127
|
+
cd = defs.by_path[cpath]
|
|
128
|
+
n = counts.get(cpath, 0)
|
|
129
|
+
if n < cd.lower or (cd.upper is not None and n > cd.upper):
|
|
130
|
+
dg.add(e0.file, e0.line, path, "ECUC-MULT-CONTAINER", "error",
|
|
131
|
+
f"{d.name} '{e0.name}': {cd.name} has {n} instance(s), def requires {fmult(cd.lower, cd.upper)}",
|
|
132
|
+
cd.path)
|
|
133
|
+
pcounts: dict = {}
|
|
134
|
+
for e, pv in params:
|
|
135
|
+
pd = _resolve_def(defs, pv.def_ref)
|
|
136
|
+
line = pv.node.line
|
|
137
|
+
if pd is None or pd.parent != d.path:
|
|
138
|
+
dg.add(e.file, line, path, "ECUC-UNKNOWN-DEF", "error",
|
|
139
|
+
f"parameter/reference {pv.def_ref} is not defined for {d.name}", pv.def_ref)
|
|
140
|
+
continue
|
|
141
|
+
pcounts[pd.path] = pcounts.get(pd.path, 0) + 1
|
|
142
|
+
if pd.obsolete:
|
|
143
|
+
dg.add(e.file, line, path, "ECUC-OBSOLETE", "warning", f"{pd.name} is obsolete", pd.path)
|
|
144
|
+
if pv.def_dest and pv.def_dest != pd.tag:
|
|
145
|
+
dg.add(e.file, line, path, "ECUC-DEST-MISMATCH", "warning",
|
|
146
|
+
f"{pd.name}: DEFINITION-REF DEST={pv.def_dest} but the def is {pd.tag}", pd.path)
|
|
147
|
+
exp = expected_value_tag(pd)
|
|
148
|
+
if exp and pv.tag != exp:
|
|
149
|
+
dg.add(e.file, line, path, "ECUC-VALUE-TYPE", "warning",
|
|
150
|
+
f"{pd.name} is a {pd.kind} def; value element should be {exp}, found {pv.tag}", pd.path)
|
|
151
|
+
if D.is_ref_kind(pd.kind or ""):
|
|
152
|
+
check_ref(e, pv, pd, path, line)
|
|
153
|
+
elif pd.kind != "ADD-INFO" and not pv.is_auto:
|
|
154
|
+
for rule, msg in check_value(pd, pv.value):
|
|
155
|
+
sev = "warning" if rule == "ECUC-EMPTY-VALUE" else "error"
|
|
156
|
+
dg.add(e.file, (pv.value_node or pv.node).line, path, rule, sev, f"{d.name} '{e0.name}': {msg}", pd.path)
|
|
157
|
+
for ppath in d.params + d.refs:
|
|
158
|
+
pd = defs.by_path[ppath]
|
|
159
|
+
n = pcounts.get(ppath, 0)
|
|
160
|
+
if n == 0 and pd.lower > 0:
|
|
161
|
+
dg.add(e0.file, e0.line, path, "ECUC-MISSING-PARAM", "error",
|
|
162
|
+
f"{d.name} '{e0.name}': mandatory {pd.name} ({pd.kind}, {pd.mult}) is missing", pd.path)
|
|
163
|
+
elif n < pd.lower or (pd.upper is not None and n > pd.upper):
|
|
164
|
+
dg.add(e0.file, e0.line, path, "ECUC-MULT-PARAM", "error",
|
|
165
|
+
f"{d.name} '{e0.name}': {pd.name} has {n} value(s), def requires {pd.mult}", pd.path)
|
|
166
|
+
for c in children:
|
|
167
|
+
check_elem_group(c.path)
|
|
168
|
+
|
|
169
|
+
def check_ref(e, pv, pd, path, line):
|
|
170
|
+
tgt = (pv.value or "").strip()
|
|
171
|
+
if pd.kind in ("INSTANCE-REFERENCE", "URI-REFERENCE"):
|
|
172
|
+
return
|
|
173
|
+
if not tgt:
|
|
174
|
+
dg.add(e.file, line, path, "ECUC-EMPTY-VALUE", "error", f"{pd.name}: empty reference", pd.path)
|
|
175
|
+
return
|
|
176
|
+
if pd.kind == "FOREIGN-REFERENCE":
|
|
177
|
+
hit = project.identifiables.get(tgt)
|
|
178
|
+
if hit is None:
|
|
179
|
+
if project.input_loaded:
|
|
180
|
+
dg.add(e.file, line, path, "ECUC-REF-DANGLING", "error",
|
|
181
|
+
f"{pd.name} -> {tgt}: target not found in any loaded ARXML", pd.path)
|
|
182
|
+
else:
|
|
183
|
+
dg.add(e.file, line, path, "ECUC-REF-UNRESOLVED", "warning",
|
|
184
|
+
f"{pd.name} -> {tgt}: target not in loaded files (add the System/SWC ARXML to "
|
|
185
|
+
f"'input' in .hydron/autosar.json)", pd.path)
|
|
186
|
+
return
|
|
187
|
+
# DESTINATION-TYPE is often an abstract metaclass (RTE-EVENT for TIMING-EVENT), which needs the
|
|
188
|
+
# metamodel to check; the VALUE-REF DEST attribute must name the target's concrete type exactly.
|
|
189
|
+
if pv.dest and pv.dest != hit[0]:
|
|
190
|
+
dg.add(e.file, line, path, "ECUC-REF-DEST", "error",
|
|
191
|
+
f"{pd.name} -> {tgt}: VALUE-REF DEST={pv.dest} but the target is a {hit[0]}", pd.path)
|
|
192
|
+
return
|
|
193
|
+
targets = project.find(tgt)
|
|
194
|
+
if not targets:
|
|
195
|
+
dg.add(e.file, line, path, "ECUC-REF-DANGLING", "error",
|
|
196
|
+
f"{pd.name} -> {tgt}: no ECUC container with that path", pd.path)
|
|
197
|
+
return
|
|
198
|
+
t = targets[0]
|
|
199
|
+
if pd.dest_refs and not any(_same_def(defs, t.def_ref, dr) for dr in pd.dest_refs):
|
|
200
|
+
dg.add(e.file, line, path, "ECUC-REF-DEST", "error",
|
|
201
|
+
f"{pd.name} -> {tgt} is a {t.def_ref.rsplit('/', 1)[-1] if t.def_ref else '?'}, def requires "
|
|
202
|
+
f"{' or '.join(r.rsplit('/', 1)[-1] for r in pd.dest_refs)}", pd.path)
|
|
203
|
+
|
|
204
|
+
for m in project.modules:
|
|
205
|
+
if module and m.name != module and (m.def_ref or "").rsplit("/", 1)[-1] != module:
|
|
206
|
+
continue
|
|
207
|
+
check_elem_group(m.path)
|
|
208
|
+
|
|
209
|
+
from .rules import run_rules
|
|
210
|
+
dg.items.extend(run_rules(project))
|
|
211
|
+
|
|
212
|
+
items = dg.items
|
|
213
|
+
if files:
|
|
214
|
+
want = {project.rel(f) for f in files} | set(files)
|
|
215
|
+
items = [i for i in items if i["file"] in want]
|
|
216
|
+
items.sort(key=lambda i: (i["file"], i["line"], SEV_ORDER.get(i["severity"], 9), i["rule"]))
|
|
217
|
+
summary: dict = {}
|
|
218
|
+
for i in items:
|
|
219
|
+
summary[i["severity"]] = summary.get(i["severity"], 0) + 1
|
|
220
|
+
return {
|
|
221
|
+
"validated": bool(defs),
|
|
222
|
+
"rules": "semantic rule packs ran (H2L-*)",
|
|
223
|
+
"defsLoaded": len(defs.modules),
|
|
224
|
+
"modules": sorted({m.name for m in project.modules}),
|
|
225
|
+
"summary": summary,
|
|
226
|
+
"diagnostics": items,
|
|
227
|
+
}
|
|
@@ -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
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Byte-offset-preserving XML tree for ARXML.
|
|
2
|
+
|
|
3
|
+
The engine never re-serialises a whole file. It parses with expat, records the byte span of every
|
|
4
|
+
element (start tag, content, end tag) and the line it starts on, and later patches only the spans
|
|
5
|
+
an edit touches. Comments, whitespace, attribute order, UUIDs and everything else outside the
|
|
6
|
+
patched spans stay byte-identical.
|
|
7
|
+
|
|
8
|
+
Namespace-agnostic: a prefixed tag (``ar:ECUC-CONTAINER-VALUE``) is stored without its prefix, so
|
|
9
|
+
any AUTOSAR schema namespace (r4.0 or a later one) parses the same way.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import xml.parsers.expat as expat
|
|
14
|
+
from typing import Iterator, Optional
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Node:
|
|
18
|
+
__slots__ = ("tag", "attrs", "start", "stag_end", "end", "cstart", "cend", "line", "children", "parent", "_text")
|
|
19
|
+
|
|
20
|
+
def __init__(self, tag: str, attrs: dict, start: int, stag_end: int, line: int, parent: "Optional[Node]"):
|
|
21
|
+
self.tag = tag
|
|
22
|
+
self.attrs = attrs
|
|
23
|
+
self.start = start # byte offset of '<'
|
|
24
|
+
self.stag_end = stag_end # byte offset just after the start tag's '>'
|
|
25
|
+
self.end = stag_end # byte offset just after the element (set on close)
|
|
26
|
+
self.cstart: Optional[int] = None # content span (None for <X/>)
|
|
27
|
+
self.cend: Optional[int] = None
|
|
28
|
+
self.line = line
|
|
29
|
+
self.children: list[Node] = []
|
|
30
|
+
self.parent = parent
|
|
31
|
+
self._text: list[str] = []
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def text(self) -> str:
|
|
35
|
+
return "".join(self._text)
|
|
36
|
+
|
|
37
|
+
def child(self, tag: str) -> "Optional[Node]":
|
|
38
|
+
for c in self.children:
|
|
39
|
+
if c.tag == tag:
|
|
40
|
+
return c
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
def child_text(self, tag: str) -> Optional[str]:
|
|
44
|
+
c = self.child(tag)
|
|
45
|
+
return c.text.strip() if c is not None else None
|
|
46
|
+
|
|
47
|
+
def iter(self) -> Iterator["Node"]:
|
|
48
|
+
stack = [self]
|
|
49
|
+
while stack:
|
|
50
|
+
n = stack.pop()
|
|
51
|
+
yield n
|
|
52
|
+
stack.extend(reversed(n.children))
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def short_name(self) -> Optional[str]:
|
|
56
|
+
return self.child_text("SHORT-NAME")
|
|
57
|
+
|
|
58
|
+
def __repr__(self) -> str: # pragma: no cover
|
|
59
|
+
return f"<Node {self.tag} {self.short_name or ''} @{self.line}>"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _local(name: str) -> str:
|
|
63
|
+
return name.rsplit(":", 1)[-1]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _stag_end(data: bytes, start: int) -> int:
|
|
67
|
+
"""Index just past the '>' closing the start tag at ``start`` (quote-aware)."""
|
|
68
|
+
i = start + 1
|
|
69
|
+
n = len(data)
|
|
70
|
+
quote = 0
|
|
71
|
+
while i < n:
|
|
72
|
+
b = data[i]
|
|
73
|
+
if quote:
|
|
74
|
+
if b == quote:
|
|
75
|
+
quote = 0
|
|
76
|
+
elif b in (0x22, 0x27): # " '
|
|
77
|
+
quote = b
|
|
78
|
+
elif b == 0x3E: # >
|
|
79
|
+
return i + 1
|
|
80
|
+
i += 1
|
|
81
|
+
raise ValueError(f"unterminated start tag at byte {start}")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def parse(data: bytes) -> Node:
|
|
85
|
+
"""Parse ``data`` and return the document element with byte spans filled in."""
|
|
86
|
+
p = expat.ParserCreate()
|
|
87
|
+
p.buffer_text = True
|
|
88
|
+
stack: list[Node] = []
|
|
89
|
+
root: list[Node] = []
|
|
90
|
+
|
|
91
|
+
def start(name, attrs):
|
|
92
|
+
s = p.CurrentByteIndex
|
|
93
|
+
se = _stag_end(data, s)
|
|
94
|
+
node = Node(_local(name), {_local(k): v for k, v in attrs.items()}, s, se, p.CurrentLineNumber,
|
|
95
|
+
stack[-1] if stack else None)
|
|
96
|
+
if stack:
|
|
97
|
+
stack[-1].children.append(node)
|
|
98
|
+
else:
|
|
99
|
+
root.append(node)
|
|
100
|
+
stack.append(node)
|
|
101
|
+
|
|
102
|
+
def end(name):
|
|
103
|
+
node = stack.pop()
|
|
104
|
+
if data[node.stag_end - 2] == 0x2F: # '/' of '/>'
|
|
105
|
+
node.end = node.stag_end
|
|
106
|
+
else:
|
|
107
|
+
idx = p.CurrentByteIndex
|
|
108
|
+
node.cstart = node.stag_end
|
|
109
|
+
node.cend = idx
|
|
110
|
+
node.end = data.index(b">", idx) + 1
|
|
111
|
+
|
|
112
|
+
def chars(t):
|
|
113
|
+
if stack:
|
|
114
|
+
stack[-1]._text.append(t)
|
|
115
|
+
|
|
116
|
+
p.StartElementHandler = start
|
|
117
|
+
p.EndElementHandler = end
|
|
118
|
+
p.CharacterDataHandler = chars
|
|
119
|
+
p.Parse(data, True)
|
|
120
|
+
if not root:
|
|
121
|
+
raise ValueError("no root element")
|
|
122
|
+
return root[0]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def escape(s: str) -> str:
|
|
126
|
+
return s.replace("&", "&").replace("<", "<").replace(">", ">")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def escape_attr(s: str) -> str:
|
|
130
|
+
return escape(s).replace('"', """)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def line_indent(data: bytes, pos: int) -> Optional[bytes]:
|
|
134
|
+
"""Whitespace between the previous newline and ``pos`` if it is all whitespace, else None."""
|
|
135
|
+
nl = data.rfind(b"\n", 0, pos)
|
|
136
|
+
seg = data[nl + 1:pos]
|
|
137
|
+
return seg if seg.strip() == b"" else None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def line_of(data: bytes, pos: int) -> int:
|
|
141
|
+
return data.count(b"\n", 0, pos) + 1
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ecuc-engine"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "Load, read, edit in place, validate and diff AUTOSAR ECUC ARXML (Hydron AUTOSAR pack)"
|
|
5
|
+
requires-python = ">=3.10"
|
|
6
|
+
# The core is stdlib-only; autosar-data adds a strict schema check of every edited file.
|
|
7
|
+
dependencies = ["autosar-data==0.17.0"]
|
|
8
|
+
|
|
9
|
+
[project.optional-dependencies]
|
|
10
|
+
test = ["pytest>=8"]
|
|
11
|
+
|
|
12
|
+
[tool.pytest.ini_options]
|
|
13
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: autosar
|
|
3
|
+
description: >-
|
|
4
|
+
AUTOSAR Classic integration expertise — ECUC configuration (ARXML), param defs, BSW module
|
|
5
|
+
config, code generation and build, and config-to-code tracing. Load it at the START of any task
|
|
6
|
+
that touches .arxml files, ECUC values/param defs, DEFINITION-REFs, a BSW module's configuration
|
|
7
|
+
(Com, PduR, CanIf, EcuC, Os, Rte, NvM, Dem, Dcm, …), generated Output/ code, or the vendor
|
|
8
|
+
configurator / SIP. It carries the tool map (ecuc_read/ecuc_edit/ecuc_validate/arxml_diff,
|
|
9
|
+
autosar_trace/autosar_config_lookup, autosar_generate/autosar_build), the edit → validate →
|
|
10
|
+
generate → build → trace loop, and the ECUC model rules needed to edit correctly.
|
|
11
|
+
---
|
|
12
|
+
# AUTOSAR skill — Classic Platform integration layer
|
|
13
|
+
|
|
14
|
+
You are working on an AUTOSAR Classic ECU project. The configuration is ECUC ARXML; generated C
|
|
15
|
+
in `Output/` is derived from it; in-house code in `Source/` calls the generated symbols.
|
|
16
|
+
|
|
17
|
+
## Hard rules
|
|
18
|
+
0. **Configured values are answered with `ecuc_read`** (it accepts a SHORT-NAME or partial path) or
|
|
19
|
+
`autosar_config_lookup` — not grep/read on ARXML. A value's AUTOSAR path is its `@path`; `@def` is its
|
|
20
|
+
DEFINITION-REF. Never give one when the other is asked.
|
|
21
|
+
1. **Never hand-write or hand-edit ARXML** (no `write`/`edit`/`sed` on `.arxml`; the pack blocks them). Every change goes
|
|
22
|
+
through `ecuc_edit`, which picks the value element, DEFINITION-REF/DEST and VALUE-REF DEST from
|
|
23
|
+
the param defs and patches only the changed bytes. Reading raw ARXML for context is fine; prefer `ecuc_read`.
|
|
24
|
+
2. **Validate after every edit**: `ecuc_validate` (scoped with `files`/`module` if large). Fix
|
|
25
|
+
every error you introduced before moving on; report pre-existing ones separately.
|
|
26
|
+
3. **Cite** the AUTOSAR path, the DEFINITION-REF and file:line for every element you discuss or change.
|
|
27
|
+
4. **Generated code is an output.** Never edit `Output/` (or other generated files) to fix a
|
|
28
|
+
problem — change the configuration and regenerate.
|
|
29
|
+
5. Values come from the param defs (ranges, literals, multiplicity) and the SWS; when the defs do
|
|
30
|
+
not settle a question (cross-module consistency, handle IDs, timing), say so and cite the SWS
|
|
31
|
+
requirement rather than guessing.
|
|
32
|
+
|
|
33
|
+
## Tool map
|
|
34
|
+
| Need | Tool |
|
|
35
|
+
|---|---|
|
|
36
|
+
| What modules/files/defs exist | `ecuc_read` (no path) |
|
|
37
|
+
| Values of a module/container | `ecuc_read path=… depth=…` |
|
|
38
|
+
| What may be set/added here (types, ranges, literals, multiplicity) | `ecuc_read path=… schema=true`, or a def path `/AUTOSAR/EcucDefs/<Mod>/…` |
|
|
39
|
+
| Change config | `ecuc_edit ops=[…]` (`dry_run` first for large edits) |
|
|
40
|
+
| Check config | `ecuc_validate` |
|
|
41
|
+
| Review a change / PR / two versions | `arxml_diff a b` |
|
|
42
|
+
| Which C symbols does this config produce, who calls them / where does this symbol come from | `autosar_trace query=…` |
|
|
43
|
+
| Refs in/out, children of an element in the ingested repo | `autosar_config_lookup path=…` |
|
|
44
|
+
| Regenerate code | `autosar_generate` (configured command, asks permission) |
|
|
45
|
+
| Compile/link | `autosar_build` |
|
|
46
|
+
| Spec questions (SWS/TPS requirement text) | the project's ingested docs search tools |
|
|
47
|
+
|
|
48
|
+
## The loop
|
|
49
|
+
`ecuc_read (schema) → ecuc_edit → ecuc_validate → autosar_generate → arxml_diff / inspect Output → autosar_build → autosar_trace (impact on Source/)`.
|
|
50
|
+
Details and failure handling: `references/workflow.md`.
|
|
51
|
+
|
|
52
|
+
## References (load when relevant)
|
|
53
|
+
- `references/ecuc-model.md` — module/container/param/ref values vs defs, DEFINITION-REF, ECUC JSON, multiplicity, choice containers, refs vs foreign refs.
|
|
54
|
+
- `references/symbolic-names.md` — the symbolic-name rule (TPS_ECUC_02108) that links config to generated `#define`s, handle IDs.
|
|
55
|
+
- `references/config-classes.md` — pre-compile / link-time / post-build config classes, variants, post-build loadable/selectable.
|
|
56
|
+
- `references/project-layout.md` — Config / Input / Output / Source / Build / SIP and `.hydron/autosar.json`.
|
|
57
|
+
- `references/workflow.md` — the edit → validate → generate → build → trace loop, diagnostics rules, and what to do when a step fails.
|
|
58
|
+
|
|
59
|
+
## Reading diagnostics
|
|
60
|
+
`file:line SEVERITY RULE path — message [def DEFINITION-REF]`. Rules: ECUC-MISSING-PARAM,
|
|
61
|
+
ECUC-MULT-CONTAINER/-PARAM, ECUC-CHOICE, ECUC-RANGE, ECUC-ENUM, ECUC-TYPE, ECUC-STRING,
|
|
62
|
+
ECUC-REF-DANGLING, ECUC-REF-DEST, ECUC-REF-UNRESOLVED (foreign target not loaded: add System/SWC
|
|
63
|
+
ARXML to `input`), ECUC-UNKNOWN-DEF (often a release mismatch between config and param defs),
|
|
64
|
+
ECUC-DUP-NAME, ECUC-NO-DEF (module defs not loaded → module not validated), ECUC-VALUE-TYPE,
|
|
65
|
+
ECUC-DEST-MISMATCH, ECUC-OBSOLETE. These are mechanical checks from the defs.
|
|
66
|
+
Semantic rule packs (run even without defs; each message carries its spec citation):
|
|
67
|
+
H2L-COM-001 signal/update bit outside its I-PDU (ComBitPosition/ComBitSize vs EcuC PduLength*8),
|
|
68
|
+
H2L-COM-002 overlapping signals in one I-PDU (Motorola numbering for BIG_ENDIAN; ComBitPosition = LSB),
|
|
69
|
+
H2L-HID-001 duplicate handle IDs per module. Other SWS cross-module rules are not checked yet — reason
|
|
70
|
+
about them explicitly.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Configuration classes and post-build
|
|
2
|
+
|
|
3
|
+
Every param/ref def lists, per implementation variant, when its value is fixed
|
|
4
|
+
(`VALUE-CONFIG-CLASSES` in recent releases):
|
|
5
|
+
|
|
6
|
+
| Class | Fixed at | Generated as | Changing it needs |
|
|
7
|
+
|---|---|---|---|
|
|
8
|
+
| PRE-COMPILE | compile time | `#define`s in `<Mod>_Cfg.h` | regenerate + full rebuild |
|
|
9
|
+
| LINK | link time | `const` data in `<Mod>_Lcfg.c` | regenerate + relink of that object |
|
|
10
|
+
| POST-BUILD | after build (flash/load) | `const` tables in `<Mod>_PBcfg.c`, passed to `<Mod>_Init(ConfigPtr)` | regenerate post-build data only (if the project uses post-build loadable) |
|
|
11
|
+
|
|
12
|
+
The module's `IMPLEMENTATION-CONFIG-VARIANT` (VARIANT-PRE-COMPILE / VARIANT-LINK-TIME /
|
|
13
|
+
VARIANT-POST-BUILD) selects which class applies; it must be one of the module def's
|
|
14
|
+
`SUPPORTED-CONFIG-VARIANTS`.
|
|
15
|
+
|
|
16
|
+
**Post-build selectable** (several configuration sets in one image, chosen at init via
|
|
17
|
+
`EcuM`/`<Mod>_Init` config pointer) and **post-build loadable** (configuration flashed separately)
|
|
18
|
+
only allow changing values whose class is POST-BUILD for the selected variant, and multiplicity
|
|
19
|
+
only where `MULTIPLICITY-CONFIG-CLASSES` allows it. `POST-BUILD-VARIANT-VALUE=true` means the value
|
|
20
|
+
may differ between post-build variants (predefined variants / variation points).
|
|
21
|
+
|
|
22
|
+
When proposing a change, state its class: a pre-compile change forces a full rebuild and a new
|
|
23
|
+
software version; a post-build change may ship as a data update.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# ECUC model basics
|
|
2
|
+
|
|
3
|
+
**Definitions vs values.** Param defs (`ECUC-MODULE-DEF` trees: the AUTOSAR StMD
|
|
4
|
+
`/AUTOSAR/EcucDefs/<Mod>` and vendor VSMD/BSWMD refinements) describe what may be configured.
|
|
5
|
+
Values (`ECUC-MODULE-CONFIGURATION-VALUES`) are the configuration. Every value element points to
|
|
6
|
+
its def through `DEFINITION-REF` (an AUTOSAR path); the `DEST` attribute names the def's metaclass.
|
|
7
|
+
|
|
8
|
+
**Hierarchy.**
|
|
9
|
+
- Module configuration → `CONTAINERS` → `ECUC-CONTAINER-VALUE` → `SUB-CONTAINERS` …
|
|
10
|
+
- A container holds `PARAMETER-VALUES` (`ECUC-NUMERICAL-PARAM-VALUE` for integer/float/boolean,
|
|
11
|
+
`ECUC-TEXTUAL-PARAM-VALUE` for enum/string/function-name/linker-symbol) and `REFERENCE-VALUES`
|
|
12
|
+
(`ECUC-REFERENCE-VALUE` with `VALUE-REF DEST=…`, `ECUC-INSTANCE-REFERENCE-VALUE`).
|
|
13
|
+
- A value's AUTOSAR path is the chain of SHORT-NAMEs: `/<Pkg>/<Module>/<Container>/…`. The def
|
|
14
|
+
path follows the *def* names: `/AUTOSAR/EcucDefs/Com/ComConfig/ComSignal`.
|
|
15
|
+
|
|
16
|
+
**ECUC JSON** (what `ecuc_read` returns and `ecuc_edit add` accepts): containers keyed
|
|
17
|
+
`"<DefName>:<ShortName>"` (just `"<Name>"` when equal), params by def short-name, multi-valued
|
|
18
|
+
params as arrays, refs as `"->/path"`, `@path`/`@def`/`@at` metadata.
|
|
19
|
+
|
|
20
|
+
**Multiplicity.** `LOWER/UPPER-MULTIPLICITY` (or `UPPER-MULTIPLICITY-INFINITE`) on every container,
|
|
21
|
+
param and ref def. Lower ≥ 1 = mandatory. A **choice container** (`ECUC-CHOICE-CONTAINER-DEF`)
|
|
22
|
+
must hold exactly one of its `CHOICES`.
|
|
23
|
+
|
|
24
|
+
**Parameter kinds.** INTEGER (uint64-safe, MIN/MAX), FLOAT (MIN/MAX with OPEN/CLOSED interval,
|
|
25
|
+
INF), BOOLEAN (`true`/`false`/`1`/`0`), ENUMERATION (literals), STRING / FUNCTION-NAME /
|
|
26
|
+
LINKER-SYMBOL (length, regex, C identifiers), ADD-INFO, MULTILINE-STRING.
|
|
27
|
+
|
|
28
|
+
**References.**
|
|
29
|
+
- ECUC reference (`ECUC-REFERENCE-DEF`, choice/symbolic-name refs): target is another container
|
|
30
|
+
value whose DEFINITION-REF matches a `DESTINATION-REF` (e.g. `ComPduIdRef → EcuC/…/Pdu`).
|
|
31
|
+
- Foreign reference (`ECUC-FOREIGN-REFERENCE-DEF`): target is a System/SWC element of the metaclass
|
|
32
|
+
in `DESTINATION-TYPE` (e.g. `I-SIGNAL-TO-I-PDU-MAPPING`); `VALUE-REF DEST` names the target's
|
|
33
|
+
concrete metaclass. Resolving it needs the System extract in `input`.
|
|
34
|
+
- Instance references point into a component instance context; the engine reads but does not edit them.
|
|
35
|
+
|
|
36
|
+
**Split files.** One module's configuration may be split across several files with the same
|
|
37
|
+
module path; tools merge them for reading and edit each element in the file it lives in.
|
|
38
|
+
|
|
39
|
+
**Releases.** The def metamodel drifts across releases (e.g. `VALUE-CONFIG-CLASSES` vs older
|
|
40
|
+
`ECUC-IMPLEMENTATION-CONFIGURATION-CLASS`; params added/removed). Always validate against the
|
|
41
|
+
param defs of the release the project's SIP targets; an ECUC-UNKNOWN-DEF burst usually means
|
|
42
|
+
mismatched defs, not broken config.
|