@anik-2/autosar 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.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 +43 -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 +101 -0
- package/python/ecuc_engine/defs.py +330 -0
- package/python/ecuc_engine/diff.py +111 -0
- package/python/ecuc_engine/ecucjson.py +118 -0
- package/python/ecuc_engine/edit.py +399 -0
- package/python/ecuc_engine/model.py +330 -0
- package/python/ecuc_engine/validate.py +222 -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 +63 -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,330 @@
|
|
|
1
|
+
"""Project discovery + the ECUC value model.
|
|
2
|
+
|
|
3
|
+
Files are classified by content, not by name or folder: a file holding ECUC-MODULE-DEF is a
|
|
4
|
+
param-def source, one holding ECUC-MODULE-CONFIGURATION-VALUES is configuration, any other .arxml
|
|
5
|
+
is "input" (System extract, SWCs, BSWMD) and only indexed for foreign-reference resolution.
|
|
6
|
+
`.hydron/autosar.json` can pin each set with globs: {"paramDefs": [...], "config": [...], "input": [...]}.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import glob
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
import xml.etree.ElementTree as ET
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
from . import defs as D
|
|
19
|
+
from .xmltree import Node, parse
|
|
20
|
+
|
|
21
|
+
SKIP_DIRS = {".git", "node_modules", ".hydron", ".venv", "__pycache__", ".cache", ".idea", ".vscode"}
|
|
22
|
+
PROJECT_CONFIG = os.path.join(".hydron", "autosar.json")
|
|
23
|
+
|
|
24
|
+
MODULE_VALUES = "ECUC-MODULE-CONFIGURATION-VALUES"
|
|
25
|
+
CONTAINER_VALUE = "ECUC-CONTAINER-VALUE"
|
|
26
|
+
PARAM_VALUE_TAGS = {"ECUC-NUMERICAL-PARAM-VALUE", "ECUC-TEXTUAL-PARAM-VALUE", "ECUC-ADD-INFO-PARAM-VALUE"}
|
|
27
|
+
REF_VALUE_TAGS = {"ECUC-REFERENCE-VALUE", "ECUC-INSTANCE-REFERENCE-VALUE"}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class ParamVal:
|
|
32
|
+
node: Node
|
|
33
|
+
def_ref: Optional[str]
|
|
34
|
+
def_dest: Optional[str]
|
|
35
|
+
tag: str
|
|
36
|
+
value: Optional[str] # VALUE text / VALUE-REF text / None
|
|
37
|
+
value_node: Optional[Node]
|
|
38
|
+
dest: Optional[str] = None # VALUE-REF DEST
|
|
39
|
+
is_auto: bool = False
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def name(self) -> str:
|
|
43
|
+
return (self.def_ref or "?").rsplit("/", 1)[-1]
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def is_ref(self) -> bool:
|
|
47
|
+
return self.tag in REF_VALUE_TAGS
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class Elem:
|
|
52
|
+
"""A module configuration (ECUC-MODULE-CONFIGURATION-VALUES) or a container value."""
|
|
53
|
+
node: Node
|
|
54
|
+
file: str
|
|
55
|
+
path: str
|
|
56
|
+
name: str
|
|
57
|
+
kind: str # "module" | "container"
|
|
58
|
+
def_ref: Optional[str]
|
|
59
|
+
def_dest: Optional[str]
|
|
60
|
+
params: list = field(default_factory=list) # ParamVal (params and refs)
|
|
61
|
+
children: list = field(default_factory=list) # Elem
|
|
62
|
+
parent: Optional["Elem"] = None
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def line(self) -> int:
|
|
66
|
+
return self.node.line
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class FileModel:
|
|
71
|
+
path: str
|
|
72
|
+
data: bytes
|
|
73
|
+
root: Node
|
|
74
|
+
elems: list # top-level module Elems
|
|
75
|
+
identifiables: dict # path -> (tag, line)
|
|
76
|
+
newline: bytes = b"\n"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
_MARKERS = {
|
|
80
|
+
"defs": re.compile(rb"<(?:[\w.-]+:)?ECUC-MODULE-DEF[\s>]"),
|
|
81
|
+
"config": re.compile(rb"<(?:[\w.-]+:)?ECUC-MODULE-CONFIGURATION-VALUES[\s>]"),
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _scan_markers(path: str) -> set:
|
|
86
|
+
"""Which element kinds a file holds (element starts only; DEST="ECUC-MODULE-DEF" text does not count)."""
|
|
87
|
+
found = set()
|
|
88
|
+
tail = b""
|
|
89
|
+
try:
|
|
90
|
+
with open(path, "rb") as f:
|
|
91
|
+
while len(found) < len(_MARKERS):
|
|
92
|
+
chunk = f.read(1 << 20)
|
|
93
|
+
if not chunk:
|
|
94
|
+
break
|
|
95
|
+
buf = tail + chunk
|
|
96
|
+
for k, rx in _MARKERS.items():
|
|
97
|
+
if k not in found and rx.search(buf):
|
|
98
|
+
found.add(k)
|
|
99
|
+
tail = buf[-96:]
|
|
100
|
+
except OSError:
|
|
101
|
+
pass
|
|
102
|
+
return found
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _walk_arxml(root: str):
|
|
106
|
+
for dp, dns, fns in os.walk(root):
|
|
107
|
+
dns[:] = [d for d in dns if d not in SKIP_DIRS and not d.startswith(".")]
|
|
108
|
+
for fn in fns:
|
|
109
|
+
if fn.lower().endswith(".arxml"):
|
|
110
|
+
yield os.path.join(dp, fn)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _globs(root: str, pats) -> list:
|
|
114
|
+
out = []
|
|
115
|
+
for p in pats or []:
|
|
116
|
+
p = os.path.expanduser(p)
|
|
117
|
+
full = p if os.path.isabs(p) else os.path.join(root, p)
|
|
118
|
+
if os.path.isdir(full):
|
|
119
|
+
out.extend(_walk_arxml(full))
|
|
120
|
+
else:
|
|
121
|
+
out.extend(sorted(glob.glob(full, recursive=True)))
|
|
122
|
+
seen, res = set(), []
|
|
123
|
+
for f in out:
|
|
124
|
+
a = os.path.abspath(f)
|
|
125
|
+
if a not in seen and os.path.isfile(a):
|
|
126
|
+
seen.add(a)
|
|
127
|
+
res.append(a)
|
|
128
|
+
return res
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@dataclass
|
|
132
|
+
class Sources:
|
|
133
|
+
root: str
|
|
134
|
+
config: list
|
|
135
|
+
defs: list
|
|
136
|
+
input: list
|
|
137
|
+
warnings: list
|
|
138
|
+
origin: dict # which mechanism found each set
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def discover(root: str, config_files: Optional[list] = None, def_files: Optional[list] = None) -> Sources:
|
|
142
|
+
root = os.path.abspath(root)
|
|
143
|
+
cfg = {}
|
|
144
|
+
cfg_path = os.path.join(root, PROJECT_CONFIG)
|
|
145
|
+
warnings: list = []
|
|
146
|
+
if os.path.isfile(cfg_path):
|
|
147
|
+
try:
|
|
148
|
+
with open(cfg_path) as f:
|
|
149
|
+
cfg = json.load(f)
|
|
150
|
+
except Exception as e:
|
|
151
|
+
warnings.append(f"{PROJECT_CONFIG} is not valid JSON ({e}); ignoring it")
|
|
152
|
+
origin = {}
|
|
153
|
+
scanned = None
|
|
154
|
+
|
|
155
|
+
def scan():
|
|
156
|
+
nonlocal scanned
|
|
157
|
+
if scanned is None:
|
|
158
|
+
scanned = {f: _scan_markers(f) for f in _walk_arxml(root)}
|
|
159
|
+
return scanned
|
|
160
|
+
|
|
161
|
+
if def_files:
|
|
162
|
+
dfiles, origin["defs"] = _globs(root, def_files), "argument"
|
|
163
|
+
elif cfg.get("paramDefs"):
|
|
164
|
+
dfiles, origin["defs"] = _globs(root, cfg["paramDefs"]), PROJECT_CONFIG
|
|
165
|
+
else:
|
|
166
|
+
dfiles, origin["defs"] = [f for f, m in scan().items() if "defs" in m], "auto-discovered"
|
|
167
|
+
if not dfiles and os.environ.get("AUTOSAR_PARAMDEFS"):
|
|
168
|
+
dfiles = _globs(root, os.environ["AUTOSAR_PARAMDEFS"].split(os.pathsep))
|
|
169
|
+
origin["defs"] = "AUTOSAR_PARAMDEFS"
|
|
170
|
+
if config_files:
|
|
171
|
+
cfiles, origin["config"] = _globs(root, config_files), "argument"
|
|
172
|
+
elif cfg.get("config"):
|
|
173
|
+
cfiles, origin["config"] = _globs(root, cfg["config"]), PROJECT_CONFIG
|
|
174
|
+
else:
|
|
175
|
+
cfiles, origin["config"] = [f for f, m in scan().items() if "config" in m], "auto-discovered"
|
|
176
|
+
if cfg.get("input"):
|
|
177
|
+
ifiles, origin["input"] = _globs(root, cfg["input"]), PROJECT_CONFIG
|
|
178
|
+
else:
|
|
179
|
+
known = set(cfiles) | set(dfiles)
|
|
180
|
+
ifiles, origin["input"] = [f for f in scan() if f not in known], "auto-discovered"
|
|
181
|
+
if not dfiles:
|
|
182
|
+
warnings.append(
|
|
183
|
+
"No ECUC param definitions found (no ECUC-MODULE-DEF in the project, no paramDefs in "
|
|
184
|
+
f"{PROJECT_CONFIG}, AUTOSAR_PARAMDEFS unset). Values are readable but NOT validated, and edits "
|
|
185
|
+
"cannot add new elements. Point paramDefs at the AUTOSAR / vendor BSWMD param-def ARXML.")
|
|
186
|
+
return Sources(root, cfiles, dfiles, ifiles, warnings, origin)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _dest(n: Optional[Node]) -> Optional[str]:
|
|
190
|
+
return n.attrs.get("DEST") if n is not None else None
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def build_file_model(path: str, data: bytes) -> FileModel:
|
|
194
|
+
root = parse(data)
|
|
195
|
+
elems: list = []
|
|
196
|
+
idents: dict = {}
|
|
197
|
+
|
|
198
|
+
def visit(node: Node, prefix: str, parent: Optional[Elem]):
|
|
199
|
+
sn = node.short_name
|
|
200
|
+
path_here = prefix
|
|
201
|
+
if sn is not None:
|
|
202
|
+
path_here = f"{prefix}/{sn}"
|
|
203
|
+
idents.setdefault(path_here, (node.tag, node.line))
|
|
204
|
+
if node.tag in (MODULE_VALUES, CONTAINER_VALUE) and sn is not None:
|
|
205
|
+
dr = node.child("DEFINITION-REF")
|
|
206
|
+
e = Elem(node=node, file=path, path=path_here, name=sn,
|
|
207
|
+
kind="module" if node.tag == MODULE_VALUES else "container",
|
|
208
|
+
def_ref=dr.text.strip() if dr is not None else None, def_dest=_dest(dr), parent=parent)
|
|
209
|
+
for coll in node.children:
|
|
210
|
+
if coll.tag in ("PARAMETER-VALUES", "REFERENCE-VALUES"):
|
|
211
|
+
for pv in coll.children:
|
|
212
|
+
e.params.append(_param(pv))
|
|
213
|
+
if parent is not None:
|
|
214
|
+
parent.children.append(e)
|
|
215
|
+
else:
|
|
216
|
+
elems.append(e)
|
|
217
|
+
for coll in node.children:
|
|
218
|
+
if coll.tag in ("CONTAINERS", "SUB-CONTAINERS"):
|
|
219
|
+
for c in coll.children:
|
|
220
|
+
visit(c, path_here, e)
|
|
221
|
+
return
|
|
222
|
+
for c in node.children:
|
|
223
|
+
if c.tag in ("SHORT-NAME", "DESC", "ADMIN-DATA", "INTRODUCTION", "ANNOTATIONS"):
|
|
224
|
+
continue
|
|
225
|
+
visit(c, path_here, parent if sn is None else None)
|
|
226
|
+
|
|
227
|
+
visit(root, "", None)
|
|
228
|
+
nl = b"\r\n" if b"\r\n" in data[:4096] else b"\n"
|
|
229
|
+
return FileModel(path, data, root, elems, idents, nl)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _param(pv: Node) -> ParamVal:
|
|
233
|
+
dr = pv.child("DEFINITION-REF")
|
|
234
|
+
vn = pv.child("VALUE") if pv.tag in PARAM_VALUE_TAGS else pv.child("VALUE-REF") or pv.child("VALUE-IREF")
|
|
235
|
+
val = None
|
|
236
|
+
if vn is not None:
|
|
237
|
+
val = vn.text.strip() if vn.tag != "VALUE-IREF" else "(instance-ref)"
|
|
238
|
+
if pv.tag == "ECUC-ADD-INFO-PARAM-VALUE":
|
|
239
|
+
val = " ".join(vn.text.split())
|
|
240
|
+
auto = (pv.child_text("IS-AUTO-VALUE") or "").lower() == "true"
|
|
241
|
+
return ParamVal(node=pv, def_ref=dr.text.strip() if dr is not None else None, def_dest=_dest(dr), tag=pv.tag,
|
|
242
|
+
value=val, value_node=vn, dest=_dest(vn) if vn is not None and vn.tag == "VALUE-REF" else None,
|
|
243
|
+
is_auto=auto)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def index_identifiables(path: str) -> dict:
|
|
247
|
+
"""Fast path -> (tag, line=0) index of a non-config ARXML (System, SWC, BSWMD) for foreign refs."""
|
|
248
|
+
out: dict = {}
|
|
249
|
+
stack: list = [] # (short_name or None)
|
|
250
|
+
pending: list = []
|
|
251
|
+
try:
|
|
252
|
+
for ev, el in ET.iterparse(path, events=("start", "end")):
|
|
253
|
+
tag = el.tag.rsplit("}", 1)[-1]
|
|
254
|
+
if ev == "start":
|
|
255
|
+
stack.append(None)
|
|
256
|
+
pending.append(tag)
|
|
257
|
+
else:
|
|
258
|
+
if tag == "SHORT-NAME" and len(stack) >= 2:
|
|
259
|
+
stack[-2] = (el.text or "").strip()
|
|
260
|
+
p = "/" + "/".join(s for s in stack[:-1] if s)
|
|
261
|
+
out.setdefault(p, (pending[-2], 0))
|
|
262
|
+
stack.pop()
|
|
263
|
+
pending.pop()
|
|
264
|
+
el.clear()
|
|
265
|
+
except ET.ParseError:
|
|
266
|
+
pass
|
|
267
|
+
return out
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class Project:
|
|
271
|
+
def __init__(self, root: str, config_files=None, def_files=None, load_input: bool = True):
|
|
272
|
+
self.sources = discover(root, config_files, def_files)
|
|
273
|
+
self.root = self.sources.root
|
|
274
|
+
self.warnings = list(self.sources.warnings)
|
|
275
|
+
self.defs = D.load_defs(self.sources.defs) if self.sources.defs else D.DefIndex()
|
|
276
|
+
if self.sources.defs and not self.defs:
|
|
277
|
+
self.warnings.append("param-def files were found but contain no ECUC-MODULE-DEF")
|
|
278
|
+
self.files: dict[str, FileModel] = {}
|
|
279
|
+
for f in self.sources.config:
|
|
280
|
+
self._load(f)
|
|
281
|
+
self.foreign: dict = {}
|
|
282
|
+
self.input_loaded = False
|
|
283
|
+
if load_input:
|
|
284
|
+
for f in self.sources.input:
|
|
285
|
+
self.foreign.update(index_identifiables(f))
|
|
286
|
+
self.input_loaded = bool(self.sources.input)
|
|
287
|
+
self.reindex()
|
|
288
|
+
|
|
289
|
+
def _load(self, f: str, data: Optional[bytes] = None):
|
|
290
|
+
if data is None:
|
|
291
|
+
with open(f, "rb") as fh:
|
|
292
|
+
data = fh.read()
|
|
293
|
+
try:
|
|
294
|
+
self.files[f] = build_file_model(f, data)
|
|
295
|
+
except Exception as e:
|
|
296
|
+
self.warnings.append(f"{self.rel(f)}: XML parse error: {e}")
|
|
297
|
+
|
|
298
|
+
def reload(self, f: str, data: bytes):
|
|
299
|
+
self._load(f, data)
|
|
300
|
+
self.reindex()
|
|
301
|
+
|
|
302
|
+
def reindex(self):
|
|
303
|
+
self.by_path: dict[str, list] = {}
|
|
304
|
+
self.modules: list = []
|
|
305
|
+
self.identifiables: dict = dict(self.foreign)
|
|
306
|
+
for fm in self.files.values():
|
|
307
|
+
for p, v in fm.identifiables.items():
|
|
308
|
+
self.identifiables.setdefault(p, v + (fm.path,))
|
|
309
|
+
for m in fm.elems:
|
|
310
|
+
self.modules.append(m)
|
|
311
|
+
for e in _iter_elems(m):
|
|
312
|
+
self.by_path.setdefault(e.path, []).append(e)
|
|
313
|
+
|
|
314
|
+
def rel(self, f: str) -> str:
|
|
315
|
+
try:
|
|
316
|
+
r = os.path.relpath(f, self.root)
|
|
317
|
+
return f if r.startswith("..") else r
|
|
318
|
+
except ValueError:
|
|
319
|
+
return f
|
|
320
|
+
|
|
321
|
+
def find(self, path: str) -> list:
|
|
322
|
+
return self.by_path.get(path.rstrip("/"), [])
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _iter_elems(e: Elem):
|
|
326
|
+
stack = [e]
|
|
327
|
+
while stack:
|
|
328
|
+
x = stack.pop()
|
|
329
|
+
yield x
|
|
330
|
+
stack.extend(reversed(x.children))
|
|
@@ -0,0 +1,222 @@
|
|
|
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
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from typing import Optional
|
|
24
|
+
|
|
25
|
+
from . import defs as D
|
|
26
|
+
from .model import Project
|
|
27
|
+
from .values import check_value, expected_value_tag
|
|
28
|
+
|
|
29
|
+
GENERIC_CONTAINER_DESTS = {"ECUC-CONTAINER-DEF"}
|
|
30
|
+
SEV_ORDER = {"error": 0, "warning": 1, "info": 2, "hint": 3}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Diags:
|
|
34
|
+
def __init__(self, project: Project):
|
|
35
|
+
self.p = project
|
|
36
|
+
self.items: list = []
|
|
37
|
+
|
|
38
|
+
def add(self, file, line, path, rule, severity, message, def_ref=None):
|
|
39
|
+
self.items.append({"file": self.p.rel(file), "line": line, "path": path, "rule": rule,
|
|
40
|
+
"severity": severity, "message": message, "defRef": def_ref})
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _same_def(defs: D.DefIndex, a: Optional[str], b: Optional[str]) -> bool:
|
|
44
|
+
if not a or not b:
|
|
45
|
+
return False
|
|
46
|
+
return a == b or defs.rel(a) == defs.rel(b)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _resolve_def(defs: D.DefIndex, ref: Optional[str]) -> Optional[D.DefNode]:
|
|
50
|
+
if not ref:
|
|
51
|
+
return None
|
|
52
|
+
d = defs.get(ref)
|
|
53
|
+
if d is not None:
|
|
54
|
+
return d
|
|
55
|
+
# vendor-relocated path (/Vendor/Com/... vs /AUTOSAR/EcucDefs/Com/...): match by module-relative path
|
|
56
|
+
rel = defs.rel(ref)
|
|
57
|
+
for m in defs.modules.values():
|
|
58
|
+
if rel.split("/", 1)[0] == m.name:
|
|
59
|
+
return defs.get(m.path + rel[len(m.name):])
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def validate(project: Project, files: Optional[list] = None, module: Optional[str] = None) -> dict:
|
|
64
|
+
defs = project.defs
|
|
65
|
+
dg = Diags(project)
|
|
66
|
+
warned_modules = set()
|
|
67
|
+
visited = set()
|
|
68
|
+
|
|
69
|
+
def fmult(lo, up):
|
|
70
|
+
return f"{lo}..{'*' if up is None else up}"
|
|
71
|
+
|
|
72
|
+
def check_elem_group(path: str):
|
|
73
|
+
if path in visited:
|
|
74
|
+
return
|
|
75
|
+
visited.add(path)
|
|
76
|
+
group = project.find(path)
|
|
77
|
+
e0 = group[0]
|
|
78
|
+
if e0.kind == "container" and len(group) > 1:
|
|
79
|
+
for e in group[1:]:
|
|
80
|
+
dg.add(e.file, e.line, path, "ECUC-DUP-NAME", "error",
|
|
81
|
+
f"duplicate container path {path} (also at {project.rel(e0.file)}:{e0.line})", e.def_ref)
|
|
82
|
+
d = _resolve_def(defs, e0.def_ref)
|
|
83
|
+
if d is None:
|
|
84
|
+
if e0.kind == "module":
|
|
85
|
+
key = e0.def_ref or path
|
|
86
|
+
if key not in warned_modules:
|
|
87
|
+
warned_modules.add(key)
|
|
88
|
+
dg.add(e0.file, e0.line, path, "ECUC-NO-DEF", "warning",
|
|
89
|
+
f"no param def loaded for {e0.def_ref}; module not validated", e0.def_ref)
|
|
90
|
+
else:
|
|
91
|
+
dg.add(e0.file, e0.line, path, "ECUC-UNKNOWN-DEF", "error",
|
|
92
|
+
f"container definition {e0.def_ref} does not exist in the loaded param defs", e0.def_ref)
|
|
93
|
+
return
|
|
94
|
+
if e0.def_dest and e0.def_dest != d.tag and not (e0.def_dest in GENERIC_CONTAINER_DESTS and d.is_container):
|
|
95
|
+
dg.add(e0.file, e0.line, path, "ECUC-DEST-MISMATCH", "warning",
|
|
96
|
+
f"DEFINITION-REF DEST={e0.def_dest} but the def is {d.tag}", d.path)
|
|
97
|
+
if e0.kind == "container" and e0.parent is not None:
|
|
98
|
+
pd = _resolve_def(defs, e0.parent.def_ref)
|
|
99
|
+
if pd is not None and d.parent != pd.path:
|
|
100
|
+
dg.add(e0.file, e0.line, path, "ECUC-WRONG-PARENT", "error",
|
|
101
|
+
f"{d.name} is not a sub-container of {pd.name} (allowed: "
|
|
102
|
+
f"{', '.join(defs.by_path[c].name for c in pd.containers) or 'none'})", d.path)
|
|
103
|
+
if d.obsolete:
|
|
104
|
+
dg.add(e0.file, e0.line, path, "ECUC-OBSOLETE", "warning", f"{d.name} is obsolete", d.path)
|
|
105
|
+
|
|
106
|
+
# children (merged across split fragments)
|
|
107
|
+
children = [c for e in group for c in e.children]
|
|
108
|
+
params = [(e, pv) for e in group for pv in e.params]
|
|
109
|
+
counts: dict = {}
|
|
110
|
+
seen_child_paths = set()
|
|
111
|
+
for c in children:
|
|
112
|
+
if c.path in seen_child_paths:
|
|
113
|
+
continue
|
|
114
|
+
seen_child_paths.add(c.path)
|
|
115
|
+
cd = _resolve_def(defs, c.def_ref)
|
|
116
|
+
if cd is not None:
|
|
117
|
+
counts[cd.path] = counts.get(cd.path, 0) + 1
|
|
118
|
+
if d.tag == D.CHOICE_TAG:
|
|
119
|
+
n = sum(counts.get(c, 0) for c in d.containers)
|
|
120
|
+
if n != 1:
|
|
121
|
+
dg.add(e0.file, e0.line, path, "ECUC-CHOICE", "error",
|
|
122
|
+
f"choice container {d.name} holds {n} choices, needs exactly 1 of "
|
|
123
|
+
f"{', '.join(defs.by_path[c].name for c in d.containers)}", d.path)
|
|
124
|
+
else:
|
|
125
|
+
for cpath in d.containers:
|
|
126
|
+
cd = defs.by_path[cpath]
|
|
127
|
+
n = counts.get(cpath, 0)
|
|
128
|
+
if n < cd.lower or (cd.upper is not None and n > cd.upper):
|
|
129
|
+
dg.add(e0.file, e0.line, path, "ECUC-MULT-CONTAINER", "error",
|
|
130
|
+
f"{d.name} '{e0.name}': {cd.name} has {n} instance(s), def requires {fmult(cd.lower, cd.upper)}",
|
|
131
|
+
cd.path)
|
|
132
|
+
pcounts: dict = {}
|
|
133
|
+
for e, pv in params:
|
|
134
|
+
pd = _resolve_def(defs, pv.def_ref)
|
|
135
|
+
line = pv.node.line
|
|
136
|
+
if pd is None or pd.parent != d.path:
|
|
137
|
+
dg.add(e.file, line, path, "ECUC-UNKNOWN-DEF", "error",
|
|
138
|
+
f"parameter/reference {pv.def_ref} is not defined for {d.name}", pv.def_ref)
|
|
139
|
+
continue
|
|
140
|
+
pcounts[pd.path] = pcounts.get(pd.path, 0) + 1
|
|
141
|
+
if pd.obsolete:
|
|
142
|
+
dg.add(e.file, line, path, "ECUC-OBSOLETE", "warning", f"{pd.name} is obsolete", pd.path)
|
|
143
|
+
if pv.def_dest and pv.def_dest != pd.tag:
|
|
144
|
+
dg.add(e.file, line, path, "ECUC-DEST-MISMATCH", "warning",
|
|
145
|
+
f"{pd.name}: DEFINITION-REF DEST={pv.def_dest} but the def is {pd.tag}", pd.path)
|
|
146
|
+
exp = expected_value_tag(pd)
|
|
147
|
+
if exp and pv.tag != exp:
|
|
148
|
+
dg.add(e.file, line, path, "ECUC-VALUE-TYPE", "warning",
|
|
149
|
+
f"{pd.name} is a {pd.kind} def; value element should be {exp}, found {pv.tag}", pd.path)
|
|
150
|
+
if D.is_ref_kind(pd.kind or ""):
|
|
151
|
+
check_ref(e, pv, pd, path, line)
|
|
152
|
+
elif pd.kind != "ADD-INFO" and not pv.is_auto:
|
|
153
|
+
for rule, msg in check_value(pd, pv.value):
|
|
154
|
+
sev = "warning" if rule == "ECUC-EMPTY-VALUE" else "error"
|
|
155
|
+
dg.add(e.file, (pv.value_node or pv.node).line, path, rule, sev, f"{d.name} '{e0.name}': {msg}", pd.path)
|
|
156
|
+
for ppath in d.params + d.refs:
|
|
157
|
+
pd = defs.by_path[ppath]
|
|
158
|
+
n = pcounts.get(ppath, 0)
|
|
159
|
+
if n == 0 and pd.lower > 0:
|
|
160
|
+
dg.add(e0.file, e0.line, path, "ECUC-MISSING-PARAM", "error",
|
|
161
|
+
f"{d.name} '{e0.name}': mandatory {pd.name} ({pd.kind}, {pd.mult}) is missing", pd.path)
|
|
162
|
+
elif n < pd.lower or (pd.upper is not None and n > pd.upper):
|
|
163
|
+
dg.add(e0.file, e0.line, path, "ECUC-MULT-PARAM", "error",
|
|
164
|
+
f"{d.name} '{e0.name}': {pd.name} has {n} value(s), def requires {pd.mult}", pd.path)
|
|
165
|
+
for c in children:
|
|
166
|
+
check_elem_group(c.path)
|
|
167
|
+
|
|
168
|
+
def check_ref(e, pv, pd, path, line):
|
|
169
|
+
tgt = (pv.value or "").strip()
|
|
170
|
+
if pd.kind in ("INSTANCE-REFERENCE", "URI-REFERENCE"):
|
|
171
|
+
return
|
|
172
|
+
if not tgt:
|
|
173
|
+
dg.add(e.file, line, path, "ECUC-EMPTY-VALUE", "error", f"{pd.name}: empty reference", pd.path)
|
|
174
|
+
return
|
|
175
|
+
if pd.kind == "FOREIGN-REFERENCE":
|
|
176
|
+
hit = project.identifiables.get(tgt)
|
|
177
|
+
if hit is None:
|
|
178
|
+
if project.input_loaded:
|
|
179
|
+
dg.add(e.file, line, path, "ECUC-REF-DANGLING", "error",
|
|
180
|
+
f"{pd.name} -> {tgt}: target not found in any loaded ARXML", pd.path)
|
|
181
|
+
else:
|
|
182
|
+
dg.add(e.file, line, path, "ECUC-REF-UNRESOLVED", "warning",
|
|
183
|
+
f"{pd.name} -> {tgt}: target not in loaded files (add the System/SWC ARXML to "
|
|
184
|
+
f"'input' in .hydron/autosar.json)", pd.path)
|
|
185
|
+
return
|
|
186
|
+
# DESTINATION-TYPE is often an abstract metaclass (RTE-EVENT for TIMING-EVENT), which needs the
|
|
187
|
+
# metamodel to check; the VALUE-REF DEST attribute must name the target's concrete type exactly.
|
|
188
|
+
if pv.dest and pv.dest != hit[0]:
|
|
189
|
+
dg.add(e.file, line, path, "ECUC-REF-DEST", "error",
|
|
190
|
+
f"{pd.name} -> {tgt}: VALUE-REF DEST={pv.dest} but the target is a {hit[0]}", pd.path)
|
|
191
|
+
return
|
|
192
|
+
targets = project.find(tgt)
|
|
193
|
+
if not targets:
|
|
194
|
+
dg.add(e.file, line, path, "ECUC-REF-DANGLING", "error",
|
|
195
|
+
f"{pd.name} -> {tgt}: no ECUC container with that path", pd.path)
|
|
196
|
+
return
|
|
197
|
+
t = targets[0]
|
|
198
|
+
if pd.dest_refs and not any(_same_def(defs, t.def_ref, dr) for dr in pd.dest_refs):
|
|
199
|
+
dg.add(e.file, line, path, "ECUC-REF-DEST", "error",
|
|
200
|
+
f"{pd.name} -> {tgt} is a {t.def_ref.rsplit('/', 1)[-1] if t.def_ref else '?'}, def requires "
|
|
201
|
+
f"{' or '.join(r.rsplit('/', 1)[-1] for r in pd.dest_refs)}", pd.path)
|
|
202
|
+
|
|
203
|
+
for m in project.modules:
|
|
204
|
+
if module and m.name != module and (m.def_ref or "").rsplit("/", 1)[-1] != module:
|
|
205
|
+
continue
|
|
206
|
+
check_elem_group(m.path)
|
|
207
|
+
|
|
208
|
+
items = dg.items
|
|
209
|
+
if files:
|
|
210
|
+
want = {project.rel(f) for f in files} | set(files)
|
|
211
|
+
items = [i for i in items if i["file"] in want]
|
|
212
|
+
items.sort(key=lambda i: (i["file"], i["line"], SEV_ORDER.get(i["severity"], 9), i["rule"]))
|
|
213
|
+
summary: dict = {}
|
|
214
|
+
for i in items:
|
|
215
|
+
summary[i["severity"]] = summary.get(i["severity"], 0) + 1
|
|
216
|
+
return {
|
|
217
|
+
"validated": bool(defs),
|
|
218
|
+
"defsLoaded": len(defs.modules),
|
|
219
|
+
"modules": sorted({m.name for m in project.modules}),
|
|
220
|
+
"summary": summary,
|
|
221
|
+
"diagnostics": items,
|
|
222
|
+
}
|