@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,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,82 @@
|
|
|
1
|
+
"""Hand-written semantic rule packs (the "H" checks the param defs cannot express).
|
|
2
|
+
|
|
3
|
+
A rule is a function registered with @rule(...). It receives a RuleContext and yields
|
|
4
|
+
(elem, line, message, def_ref) tuples; the registry adds the rule id, severity and spec citation.
|
|
5
|
+
Rules are keyed by module-relative DEFINITION-REFs ("Com/ComConfig/ComSignal/ComBitPosition"), so
|
|
6
|
+
they work for /AUTOSAR/EcucDefs/... and vendor-relocated defs alike, and run with or without the
|
|
7
|
+
param defs loaded. To add a pack: create a module here with @rule functions and import it below.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Callable, Iterable, Optional
|
|
13
|
+
|
|
14
|
+
from ..model import Elem, ParamVal, Project
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class Rule:
|
|
19
|
+
id: str
|
|
20
|
+
severity: str
|
|
21
|
+
cite: str
|
|
22
|
+
title: str
|
|
23
|
+
fn: Callable
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
RULES: list = []
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def rule(id: str, severity: str, cite: str, title: str):
|
|
30
|
+
def deco(fn):
|
|
31
|
+
RULES.append(Rule(id, severity, cite, title, fn))
|
|
32
|
+
return fn
|
|
33
|
+
return deco
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class RuleContext:
|
|
37
|
+
def __init__(self, project: Project):
|
|
38
|
+
self.p = project
|
|
39
|
+
self.defs = project.defs
|
|
40
|
+
self._by_rel: Optional[dict] = None
|
|
41
|
+
|
|
42
|
+
def rel(self, def_ref: Optional[str]) -> str:
|
|
43
|
+
return self.defs.rel(def_ref) if def_ref else ""
|
|
44
|
+
|
|
45
|
+
def containers(self, rel_def: str) -> list:
|
|
46
|
+
"""All container values (first fragment per path) whose def is ``rel_def`` (module-relative)."""
|
|
47
|
+
if self._by_rel is None:
|
|
48
|
+
self._by_rel = {}
|
|
49
|
+
for path, group in self.p.by_path.items():
|
|
50
|
+
self._by_rel.setdefault(self.rel(group[0].def_ref), []).append(group[0])
|
|
51
|
+
return self._by_rel.get(rel_def, [])
|
|
52
|
+
|
|
53
|
+
def values(self, e: Elem, name: str) -> list:
|
|
54
|
+
return [pv for g in self.p.find(e.path) for pv in g.params if pv.name == name]
|
|
55
|
+
|
|
56
|
+
def value(self, e: Elem, name: str) -> Optional[ParamVal]:
|
|
57
|
+
v = self.values(e, name)
|
|
58
|
+
return v[0] if v else None
|
|
59
|
+
|
|
60
|
+
def text(self, e: Elem, name: str) -> Optional[str]:
|
|
61
|
+
v = self.value(e, name)
|
|
62
|
+
return v.value if v is not None else None
|
|
63
|
+
|
|
64
|
+
def target(self, pv: Optional[ParamVal]) -> Optional[Elem]:
|
|
65
|
+
if pv is None or not pv.value:
|
|
66
|
+
return None
|
|
67
|
+
hit = self.p.find(pv.value.strip())
|
|
68
|
+
return hit[0] if hit else None
|
|
69
|
+
|
|
70
|
+
def module_of(self, e: Elem) -> Elem:
|
|
71
|
+
while e.parent is not None:
|
|
72
|
+
e = e.parent
|
|
73
|
+
return e
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def run_rules(project: Project) -> Iterable[dict]:
|
|
77
|
+
from . import com, handles # noqa: F401 (registers the packs)
|
|
78
|
+
ctx = RuleContext(project)
|
|
79
|
+
for r in RULES:
|
|
80
|
+
for e, line, msg, def_ref in r.fn(ctx):
|
|
81
|
+
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}
|
|
@@ -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
|