@h2loop/autosar 1.0.1 → 1.2.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/README.md +11 -1
- package/agents/autosar-integrator.md +45 -2
- package/commands/autosar-trace.md +2 -2
- package/package.json +4 -1
- package/plugins/index.js +55 -23
- package/python/ecuc_engine/__init__.py +1 -1
- package/python/ecuc_engine/add_signal.py +351 -0
- package/python/ecuc_engine/arxml_edit.py +387 -0
- package/python/ecuc_engine/audit.py +352 -50
- package/python/ecuc_engine/cli.py +125 -5
- package/python/ecuc_engine/codegen/__init__.py +233 -0
- package/python/ecuc_engine/codegen/binding.py +305 -0
- package/python/ecuc_engine/codegen/bindings/sip_standin.json +378 -0
- package/python/ecuc_engine/codegen/cdiff.py +395 -0
- package/python/ecuc_engine/codegen/cwriter.py +150 -0
- package/python/ecuc_engine/codegen/emit.py +463 -0
- package/python/ecuc_engine/codegen/handles.py +176 -0
- package/python/ecuc_engine/codegen/mcal/tc4d.json +60 -0
- package/python/ecuc_engine/codegen/model.py +264 -0
- package/python/ecuc_engine/codegen/modules/__init__.py +7 -0
- package/python/ecuc_engine/codegen/modules/com.py +145 -0
- package/python/ecuc_engine/codegen/modules/comstack.py +14 -0
- package/python/ecuc_engine/codegen/modules/dem.py +22 -0
- package/python/ecuc_engine/codegen/modules/mcal.py +170 -0
- package/python/ecuc_engine/codegen/modules/memstack.py +24 -0
- package/python/ecuc_engine/codegen/registry.py +208 -0
- package/python/ecuc_engine/codegen/schm_memmap.py +159 -0
- package/python/ecuc_engine/context.py +481 -0
- package/python/ecuc_engine/csrc.py +189 -0
- package/python/ecuc_engine/defs.py +30 -0
- package/python/ecuc_engine/ecucjson.py +75 -22
- package/python/ecuc_engine/find.py +164 -0
- package/python/ecuc_engine/gen.py +105 -0
- package/python/ecuc_engine/history.py +146 -0
- package/python/ecuc_engine/model.py +4 -1
- package/python/ecuc_engine/probe.py +229 -0
- package/python/ecuc_engine/rte_api.py +935 -0
- package/python/ecuc_engine/rules/__init__.py +5 -3
- package/python/ecuc_engine/rules/mem.py +84 -0
- package/python/ecuc_engine/rules/nvm.py +131 -0
- package/python/ecuc_engine/rules/pdur.py +87 -0
- package/python/ecuc_engine/rules/system.py +162 -0
- package/python/ecuc_engine/specids.py +163 -0
- package/python/ecuc_engine/sysmodel.py +152 -0
- package/python/ecuc_engine/validate.py +15 -2
- package/python/ecuc_engine/values.py +26 -0
- package/python/ecuc_engine/verify_rte.py +445 -0
- package/python/pyproject.toml +1 -1
- package/skills/autosar/SKILL.md +78 -6
- package/skills/autosar/references/codegen.md +58 -0
- package/skills/autosar/references/ecuc-model.md +4 -1
- package/skills/autosar/references/project-layout.md +10 -1
- package/skills/autosar/references/runnable-code.md +50 -0
|
@@ -76,6 +76,11 @@ class DefNode:
|
|
|
76
76
|
refs: list = field(default_factory=list) # child ref def paths
|
|
77
77
|
parent: Optional[str] = None
|
|
78
78
|
module: Optional[str] = None # module def path
|
|
79
|
+
# Config classes per IMPLEMENTATION-CONFIG-VARIANT ({"VARIANT-PRE-COMPILE": "PRE-COMPILE", ...}).
|
|
80
|
+
config_classes: Optional[dict] = None # VALUE-CONFIG-CLASSES (params/refs)
|
|
81
|
+
mult_classes: Optional[dict] = None # MULTIPLICITY-CONFIG-CLASSES (containers, params)
|
|
82
|
+
requires_symbolic: bool = False # REQUIRES-SYMBOLIC-NAME-VALUE (refs)
|
|
83
|
+
variants: Optional[list] = None # SUPPORTED-CONFIG-VARIANTS (module defs)
|
|
79
84
|
|
|
80
85
|
@property
|
|
81
86
|
def is_container(self) -> bool:
|
|
@@ -218,6 +223,13 @@ def _make_def(el, tag: str, sn: str, path: str, path_file: str) -> DefNode:
|
|
|
218
223
|
d.upper = None
|
|
219
224
|
else:
|
|
220
225
|
d.upper = _int(direct["UPPER-MULTIPLICITY"].text if "UPPER-MULTIPLICITY" in direct else None, 1)
|
|
226
|
+
for wrap, attr in (("VALUE-CONFIG-CLASSES", "config_classes"), ("MULTIPLICITY-CONFIG-CLASSES", "mult_classes")):
|
|
227
|
+
w = direct.get(wrap)
|
|
228
|
+
if w is not None:
|
|
229
|
+
setattr(d, attr, _config_classes(w))
|
|
230
|
+
sv = direct.get("SUPPORTED-CONFIG-VARIANTS")
|
|
231
|
+
if sv is not None:
|
|
232
|
+
d.variants = [(c.text or "").strip() for c in sv if _local(c.tag) == "SUPPORTED-CONFIG-VARIANT"]
|
|
221
233
|
tr = direct.get("RELATED-TRACE-ITEM-REF")
|
|
222
234
|
if tr is not None:
|
|
223
235
|
d.trace = (tr.text or "").strip()
|
|
@@ -256,9 +268,27 @@ def _make_def(el, tag: str, sn: str, path: str, path_file: str) -> DefNode:
|
|
|
256
268
|
d.with_auto = txt.lower() == "true"
|
|
257
269
|
elif t == "SYMBOLIC-NAME-VALUE":
|
|
258
270
|
d.symbolic_name = txt.lower() == "true"
|
|
271
|
+
elif t == "REQUIRES-SYMBOLIC-NAME-VALUE":
|
|
272
|
+
d.requires_symbolic = txt.lower() == "true"
|
|
259
273
|
return d
|
|
260
274
|
|
|
261
275
|
|
|
276
|
+
def _config_classes(wrapper) -> dict:
|
|
277
|
+
"""<VALUE-CONFIG-CLASSES>/<MULTIPLICITY-CONFIG-CLASSES> -> {CONFIG-VARIANT: CONFIG-CLASS}."""
|
|
278
|
+
out = {}
|
|
279
|
+
for c in wrapper:
|
|
280
|
+
cls = var = None
|
|
281
|
+
for f in c:
|
|
282
|
+
t = _local(f.tag)
|
|
283
|
+
if t == "CONFIG-CLASS":
|
|
284
|
+
cls = (f.text or "").strip()
|
|
285
|
+
elif t == "CONFIG-VARIANT":
|
|
286
|
+
var = (f.text or "").strip()
|
|
287
|
+
if cls and var:
|
|
288
|
+
out[var] = cls
|
|
289
|
+
return out
|
|
290
|
+
|
|
291
|
+
|
|
262
292
|
def load_defs(paths: list[str], use_cache: bool = True) -> DefIndex:
|
|
263
293
|
idx = DefIndex()
|
|
264
294
|
for p in paths:
|
|
@@ -8,6 +8,10 @@ Keyed by def names, not XML structure:
|
|
|
8
8
|
"ComSignal:DoorLatch": {...}}}}}
|
|
9
9
|
A container key is "<DefName>:<ShortName>", shortened to "<ShortName>" when the two are equal.
|
|
10
10
|
Multi-valued parameters are arrays. References are "->/AUTOSAR/path".
|
|
11
|
+
Every container carries "@at" (file:line of the container element). Every value is followed by
|
|
12
|
+
"@at:<Param>" = file:line of its VALUE / VALUE-REF element, and integer-typed values by "@hex:<Param>"
|
|
13
|
+
(and "@dec:<Param>" when the ARXML text is not decimal), so citations and base conversions are never guessed.
|
|
14
|
+
Keys starting with "@" are metadata; ecuc_edit ignores them.
|
|
11
15
|
"""
|
|
12
16
|
from __future__ import annotations
|
|
13
17
|
|
|
@@ -16,7 +20,7 @@ from typing import Optional
|
|
|
16
20
|
from . import defs as D
|
|
17
21
|
from .model import Elem, Project
|
|
18
22
|
from .validate import _resolve_def
|
|
19
|
-
from .values import to_json
|
|
23
|
+
from .values import int_forms, to_json
|
|
20
24
|
|
|
21
25
|
|
|
22
26
|
def key_of(e: Elem) -> str:
|
|
@@ -24,27 +28,55 @@ def key_of(e: Elem) -> str:
|
|
|
24
28
|
return e.name if dn == e.name else f"{dn}:{e.name}"
|
|
25
29
|
|
|
26
30
|
|
|
31
|
+
def value_at(project: Project, e: Elem, pv) -> str:
|
|
32
|
+
"""file:line of a parameter's VALUE / VALUE-REF element (not its container)."""
|
|
33
|
+
n = pv.value_node if pv.value_node is not None else pv.node
|
|
34
|
+
return f"{project.rel(e.file)}:{n.line}"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def value_info(project: Project, e: Elem, pv) -> dict:
|
|
38
|
+
"""One configured value with its exact location and, for integers, both bases."""
|
|
39
|
+
pd = _resolve_def(project.defs, pv.def_ref)
|
|
40
|
+
numerical = pv.tag == "ECUC-NUMERICAL-PARAM-VALUE"
|
|
41
|
+
out = {"value": to_json(pd, pv.value, pv.is_ref, numerical), "at": value_at(project, e, pv)}
|
|
42
|
+
nums = int_forms(pd, pv.value, pv.is_ref, numerical, pv.def_dest)
|
|
43
|
+
if nums:
|
|
44
|
+
out.update(nums)
|
|
45
|
+
return out
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _put(out: dict, name: str, items: list, multi: bool):
|
|
49
|
+
one = lambda xs: xs if multi else xs[0] # noqa: E731
|
|
50
|
+
out[name] = one([x["value"] for x in items])
|
|
51
|
+
out[f"@at:{name}"] = one([x["at"] for x in items])
|
|
52
|
+
if any("hex" in x for x in items):
|
|
53
|
+
out[f"@hex:{name}"] = one([x.get("hex") for x in items])
|
|
54
|
+
if any("dec" in x and x["value"] != x["dec"] for x in items):
|
|
55
|
+
out[f"@dec:{name}"] = one([x.get("dec") for x in items])
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def annotated_values(project: Project, group: list) -> dict:
|
|
59
|
+
"""{param: value, "@at:param": file:line, "@hex:param": ...} for the params of one element (all its files)."""
|
|
60
|
+
items: dict = {}
|
|
61
|
+
multi: dict = {}
|
|
62
|
+
for e in group:
|
|
63
|
+
for pv in e.params:
|
|
64
|
+
pd = _resolve_def(project.defs, pv.def_ref)
|
|
65
|
+
items.setdefault(pv.name, []).append(value_info(project, e, pv))
|
|
66
|
+
multi[pv.name] = multi.get(pv.name, False) or (pd is not None and (pd.upper is None or pd.upper > 1))
|
|
67
|
+
out: dict = {}
|
|
68
|
+
for name, xs in items.items():
|
|
69
|
+
_put(out, name, xs, multi[name] or len(xs) > 1)
|
|
70
|
+
return out
|
|
71
|
+
|
|
72
|
+
|
|
27
73
|
def render(project: Project, group: list, depth: int, top: bool = True) -> dict:
|
|
28
|
-
defs = project.defs
|
|
29
74
|
e0 = group[0]
|
|
30
75
|
out: dict = {"@path": e0.path}
|
|
31
76
|
if top:
|
|
32
77
|
out["@def"] = e0.def_ref
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
for e in group:
|
|
36
|
-
for pv in e.params:
|
|
37
|
-
pd = _resolve_def(defs, pv.def_ref)
|
|
38
|
-
v = to_json(pd, pv.value, pv.is_ref, pv.tag == "ECUC-NUMERICAL-PARAM-VALUE")
|
|
39
|
-
name = pv.name
|
|
40
|
-
multi = pd is not None and (pd.upper is None or pd.upper > 1)
|
|
41
|
-
if name in vals:
|
|
42
|
-
if not isinstance(vals[name], list) or not multi:
|
|
43
|
-
vals[name] = vals[name] if isinstance(vals[name], list) else [vals[name]]
|
|
44
|
-
vals[name].append(v)
|
|
45
|
-
else:
|
|
46
|
-
vals[name] = [v] if multi else v
|
|
47
|
-
out.update(vals)
|
|
78
|
+
out["@at"] = ", ".join(f"{project.rel(e.file)}:{e.line}" for e in group)
|
|
79
|
+
out.update(annotated_values(project, group))
|
|
48
80
|
seen = set()
|
|
49
81
|
for e in group:
|
|
50
82
|
for c in e.children:
|
|
@@ -61,6 +93,24 @@ def render(project: Project, group: list, depth: int, top: bool = True) -> dict:
|
|
|
61
93
|
return out
|
|
62
94
|
|
|
63
95
|
|
|
96
|
+
def value_sites(project: Project, paths: list) -> dict:
|
|
97
|
+
"""AUTOSAR path -> {"at": container file:line, "values": {param: {value, at, dec?, hex?} | [..]}} from the
|
|
98
|
+
local ECUC, or None when the element is not in the local checkout."""
|
|
99
|
+
out: dict = {}
|
|
100
|
+
for p in paths:
|
|
101
|
+
g = project.find(p)
|
|
102
|
+
if not g:
|
|
103
|
+
out[p] = None
|
|
104
|
+
continue
|
|
105
|
+
vals: dict = {}
|
|
106
|
+
for e in g:
|
|
107
|
+
for pv in e.params:
|
|
108
|
+
vals.setdefault(pv.name, []).append(value_info(project, e, pv))
|
|
109
|
+
out[p] = {"at": ", ".join(f"{project.rel(e.file)}:{e.line}" for e in g),
|
|
110
|
+
"values": {k: (v[0] if len(v) == 1 else v) for k, v in vals.items()}}
|
|
111
|
+
return out
|
|
112
|
+
|
|
113
|
+
|
|
64
114
|
def read(project: Project, path: Optional[str], depth: int = 2, schema: bool = False) -> dict:
|
|
65
115
|
res: dict = {"defsLoaded": len(project.defs.modules)}
|
|
66
116
|
if project.warnings:
|
|
@@ -101,12 +151,15 @@ def read(project: Project, path: Optional[str], depth: int = 2, schema: bool = F
|
|
|
101
151
|
parent, _, last = path.rstrip("/").rpartition("/")
|
|
102
152
|
pg = project.find(parent)
|
|
103
153
|
if pg:
|
|
104
|
-
hits = [pv for e in pg for pv in e.params if pv.name == last]
|
|
154
|
+
hits = [(e, pv) for e in pg for pv in e.params if pv.name == last]
|
|
105
155
|
if hits:
|
|
106
|
-
pd = _resolve_def(project.defs, hits[0].def_ref)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
"
|
|
156
|
+
pd = _resolve_def(project.defs, hits[0][1].def_ref)
|
|
157
|
+
info = [value_info(project, e, pv) for e, pv in hits]
|
|
158
|
+
res["param"] = {"@path": path, "@def": hits[0][1].def_ref,
|
|
159
|
+
"@at": [x["at"] for x in info], "value": [x["value"] for x in info]}
|
|
160
|
+
if any("hex" in x for x in info):
|
|
161
|
+
res["param"]["dec"] = [x.get("dec") for x in info]
|
|
162
|
+
res["param"]["hex"] = [x.get("hex") for x in info]
|
|
110
163
|
if pd is not None:
|
|
111
164
|
res["schema"] = D.describe(pd, project.defs)
|
|
112
165
|
return res
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Structured ECUC queries (ecuc_find) and per-module cross-check tables (ecuc_crosscheck)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from .model import Project
|
|
8
|
+
from .values import parse_int
|
|
9
|
+
|
|
10
|
+
import operator
|
|
11
|
+
|
|
12
|
+
CMP = {">": operator.gt, "<": operator.lt, ">=": operator.ge, "<=": operator.le}
|
|
13
|
+
OPS = re.compile(r"^\s*([A-Za-z_]\w*)\s*(!=|>=|<=|=|>|<|~)\s*(.*?)\s*$")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _where(where) -> list:
|
|
17
|
+
if not where:
|
|
18
|
+
return []
|
|
19
|
+
if isinstance(where, dict):
|
|
20
|
+
return [(k, "=", v) for k, v in where.items()]
|
|
21
|
+
out = []
|
|
22
|
+
for part in re.split(r"\s*(?:,|&&|\band\b)\s*", str(where)):
|
|
23
|
+
m = OPS.match(part)
|
|
24
|
+
if m:
|
|
25
|
+
out.append((m.group(1), m.group(2), m.group(3)))
|
|
26
|
+
return out
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _cmp(vals: list, op: str, want) -> bool:
|
|
30
|
+
w = str(want)
|
|
31
|
+
w = w[2:] if w.startswith("->") else w
|
|
32
|
+
for v in vals:
|
|
33
|
+
s = (v or "").strip()
|
|
34
|
+
if op == "=" and (s == w or s.lower() == w.lower()):
|
|
35
|
+
return True
|
|
36
|
+
if op == "!=" and s != w:
|
|
37
|
+
return True
|
|
38
|
+
if op == "~" and re.search(w, s):
|
|
39
|
+
return True
|
|
40
|
+
if op in (">", "<", ">=", "<="):
|
|
41
|
+
a, b = parse_int(s), parse_int(w)
|
|
42
|
+
if a is not None and b is not None and CMP[op](a, b):
|
|
43
|
+
return True
|
|
44
|
+
return op == "!=" and not vals
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _own(project: Project, e) -> dict:
|
|
48
|
+
vals: dict = {}
|
|
49
|
+
for g in project.find(e.path):
|
|
50
|
+
for pv in g.params:
|
|
51
|
+
vals.setdefault(pv.name, []).append(pv.value)
|
|
52
|
+
return vals
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _refs(project: Project, e, depth: int) -> set:
|
|
56
|
+
"""Reference targets of e, following references up to ``depth`` hops."""
|
|
57
|
+
out, frontier = set(), [e]
|
|
58
|
+
for _ in range(depth):
|
|
59
|
+
nxt = []
|
|
60
|
+
for x in frontier:
|
|
61
|
+
for g in project.find(x.path):
|
|
62
|
+
for pv in g.params:
|
|
63
|
+
if pv.is_ref and pv.value:
|
|
64
|
+
t = pv.value.strip()
|
|
65
|
+
if t not in out:
|
|
66
|
+
out.add(t)
|
|
67
|
+
hit = project.find(t)
|
|
68
|
+
if hit:
|
|
69
|
+
nxt.append(hit[0])
|
|
70
|
+
frontier = nxt
|
|
71
|
+
return out
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def find(project: Project, module: Optional[str] = None, container_def: Optional[str] = None, where=None,
|
|
75
|
+
ref_to: Optional[str] = None, referenced_by: Optional[str] = None, limit: int = 50, depth: int = 2) -> dict:
|
|
76
|
+
from .ecucjson import annotated_values, resolve
|
|
77
|
+
conds = _where(where)
|
|
78
|
+
targets = None
|
|
79
|
+
if ref_to:
|
|
80
|
+
r = resolve(project, ref_to)
|
|
81
|
+
if isinstance(r, dict):
|
|
82
|
+
sys = project.system.get(ref_to)
|
|
83
|
+
if sys is None:
|
|
84
|
+
return {"error": r["error"], "didYouMean": r.get("didYouMean", [])}
|
|
85
|
+
r = ref_to
|
|
86
|
+
targets = r
|
|
87
|
+
owner_refs = None
|
|
88
|
+
if referenced_by:
|
|
89
|
+
r = resolve(project, referenced_by)
|
|
90
|
+
if isinstance(r, dict):
|
|
91
|
+
return {"error": r["error"], "didYouMean": r.get("didYouMean", [])}
|
|
92
|
+
owner_refs = _refs(project, project.find(r)[0], 1)
|
|
93
|
+
hits = []
|
|
94
|
+
total = 0
|
|
95
|
+
for path, group in project.by_path.items():
|
|
96
|
+
e = group[0]
|
|
97
|
+
if e.kind != "container":
|
|
98
|
+
continue
|
|
99
|
+
dname = (e.def_ref or "").rsplit("/", 1)[-1]
|
|
100
|
+
if container_def and not (dname == container_def or (e.def_ref or "").endswith("/" + container_def.strip("/"))):
|
|
101
|
+
continue
|
|
102
|
+
if module:
|
|
103
|
+
m = e
|
|
104
|
+
while m.parent is not None:
|
|
105
|
+
m = m.parent
|
|
106
|
+
if module not in (m.name, (m.def_ref or "").rsplit("/", 1)[-1]):
|
|
107
|
+
continue
|
|
108
|
+
if owner_refs is not None and path not in owner_refs:
|
|
109
|
+
continue
|
|
110
|
+
own = _own(project, e)
|
|
111
|
+
if any(not _cmp(own.get(k, []), op, v) for k, op, v in conds):
|
|
112
|
+
continue
|
|
113
|
+
if targets is not None:
|
|
114
|
+
refs = _refs(project, e, depth)
|
|
115
|
+
if not any(t == targets or t.startswith(targets + "/") for t in refs):
|
|
116
|
+
continue
|
|
117
|
+
total += 1
|
|
118
|
+
if len(hits) < limit:
|
|
119
|
+
hits.append({"path": path, "def": dname, "at": ", ".join(f"{project.rel(x.file)}:{x.line}" for x in group),
|
|
120
|
+
"values": annotated_values(project, group)})
|
|
121
|
+
return {"count": total, "returned": len(hits), "matches": hits,
|
|
122
|
+
"legend": "at = container element; values[\"@at:P\"] = file:line of P's VALUE element; "
|
|
123
|
+
"@hex:/@dec: = the same integer in the other base"}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
ELEMENTS = {
|
|
127
|
+
"Com": ["Com/ComConfig/ComSignal", "Com/ComConfig/ComIPdu"],
|
|
128
|
+
"PduR": ["PduR/PduRRoutingPaths/PduRRoutingPath"],
|
|
129
|
+
"EcuC": ["EcuC/EcucConfigSet/EcucPduCollection/Pdu"],
|
|
130
|
+
"NvM": ["NvM/NvMBlockDescriptor"],
|
|
131
|
+
"Fee": ["Fee/FeeBlockConfiguration"],
|
|
132
|
+
"Ea": ["Ea/EaBlockConfiguration"],
|
|
133
|
+
"CanIf": ["CanIf/CanIfInitCfg/CanIfTxPduCfg", "CanIf/CanIfInitCfg/CanIfRxPduCfg"],
|
|
134
|
+
"Dem": ["Dem/DemConfigSet/DemEventParameter"],
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def crosscheck(project: Project, module: str, limit: int = 200) -> dict:
|
|
139
|
+
from .rules import RuleContext, run_rules
|
|
140
|
+
mods = [m for m in project.modules if module in (m.name, (m.def_ref or "").rsplit("/", 1)[-1])]
|
|
141
|
+
if not mods:
|
|
142
|
+
return {"error": f"module {module!r} is not configured (modules: {', '.join(sorted({m.name for m in project.modules}))})"}
|
|
143
|
+
roots = [m.path for m in mods]
|
|
144
|
+
diags = [d for d in run_rules(project) if any(d["path"] == r or d["path"].startswith(r + "/") for r in roots)
|
|
145
|
+
or d["rule"].endswith("-000")]
|
|
146
|
+
ctx = RuleContext(project)
|
|
147
|
+
kinds = ELEMENTS.get((mods[0].def_ref or "").rsplit("/", 1)[-1] or module, [])
|
|
148
|
+
rows, counts = [], {"checked": 0, "findings": 0}
|
|
149
|
+
by_path: dict = {}
|
|
150
|
+
for d in diags:
|
|
151
|
+
by_path.setdefault(d["path"], []).append(d)
|
|
152
|
+
for k in kinds:
|
|
153
|
+
for e in ctx.containers(k):
|
|
154
|
+
counts["checked"] += 1
|
|
155
|
+
f = by_path.get(e.path, [])
|
|
156
|
+
if f:
|
|
157
|
+
counts["findings"] += 1
|
|
158
|
+
rows.append({"element": e.path, "def": k.rsplit("/", 1)[-1], "at": f"{project.rel(e.file)}:{e.line}",
|
|
159
|
+
"status": "FINDING", "findings": [f"{x['rule']} {x['severity']}: {x['message']}" for x in f]})
|
|
160
|
+
notes = [d["message"] for d in diags if d["severity"] == "info"]
|
|
161
|
+
other = [d for d in diags if d["severity"] != "info" and not any(r["element"] == d["path"] for r in rows)]
|
|
162
|
+
return {"module": module, "elementKinds": [k.rsplit("/", 1)[-1] for k in kinds], "counts": counts,
|
|
163
|
+
"rows": rows[:limit], "otherFindings": other[:limit], "notes": notes,
|
|
164
|
+
"summary": f"{counts['checked']} element(s) checked, {counts['findings']} with findings"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""H2L-GEN-001: is the generated code up to date with the configuration?
|
|
2
|
+
|
|
3
|
+
Runs the project's configured `generate` command in a scratch copy (never in place), then compares the
|
|
4
|
+
copy with the tree. Every file the generator would change is reported with a short diff summary.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import difflib
|
|
9
|
+
import os
|
|
10
|
+
import shutil
|
|
11
|
+
import subprocess
|
|
12
|
+
import tempfile
|
|
13
|
+
import time
|
|
14
|
+
|
|
15
|
+
from . import paths
|
|
16
|
+
|
|
17
|
+
DEFAULT_TIMEOUT = 300
|
|
18
|
+
AUTO_LIMIT = 5 * 1024 * 1024 # on by default only when the configuration is smaller than this
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def generate_cmd(cfg: dict):
|
|
22
|
+
g = cfg.get("generate")
|
|
23
|
+
if isinstance(g, str):
|
|
24
|
+
return g, None, DEFAULT_TIMEOUT
|
|
25
|
+
if isinstance(g, dict) and g.get("command"):
|
|
26
|
+
return g["command"], g.get("cwd"), int(g.get("timeoutSec") or DEFAULT_TIMEOUT)
|
|
27
|
+
return None, None, 0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def default_on(project, cfg: dict) -> bool:
|
|
31
|
+
if "checkGenerated" in cfg:
|
|
32
|
+
return bool(cfg["checkGenerated"])
|
|
33
|
+
if not generate_cmd(cfg)[0]:
|
|
34
|
+
return False
|
|
35
|
+
return sum(os.path.getsize(f) for f in project.files) < AUTO_LIMIT
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _snapshot(root: str) -> dict:
|
|
39
|
+
out = {}
|
|
40
|
+
for dp, dns, fns in os.walk(root):
|
|
41
|
+
dns[:] = [d for d in dns if d not in paths.TOOLING_DIRS]
|
|
42
|
+
for fn in fns:
|
|
43
|
+
f = os.path.join(dp, fn)
|
|
44
|
+
try:
|
|
45
|
+
st = os.stat(f)
|
|
46
|
+
except OSError:
|
|
47
|
+
continue
|
|
48
|
+
out[os.path.relpath(f, root)] = (st.st_size, st.st_mtime_ns)
|
|
49
|
+
return out
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def check(project, cfg: dict, timeout: int = 0) -> list:
|
|
53
|
+
cmd, cwd, tmo = generate_cmd(cfg)
|
|
54
|
+
if not cmd:
|
|
55
|
+
return [_d("info", "", 0, "no `generate` command in .hydron/autosar.json; stale-Output check skipped")]
|
|
56
|
+
tmo = timeout or tmo
|
|
57
|
+
root = project.root
|
|
58
|
+
before = _snapshot(root)
|
|
59
|
+
t0 = time.time()
|
|
60
|
+
with tempfile.TemporaryDirectory(prefix="h2l-gen-") as tmp:
|
|
61
|
+
scratch = os.path.join(tmp, "proj")
|
|
62
|
+
shutil.copytree(root, scratch, symlinks=True, ignore=shutil.ignore_patterns(*paths.TOOLING_DIRS))
|
|
63
|
+
try:
|
|
64
|
+
r = subprocess.run(cmd, shell=True, cwd=os.path.join(scratch, cwd) if cwd else scratch,
|
|
65
|
+
capture_output=True, text=True, timeout=tmo)
|
|
66
|
+
except subprocess.TimeoutExpired:
|
|
67
|
+
return [_d("info", "", 0, f"NOT CHECKED: generate command timed out after {tmo}s in a scratch copy: {cmd}")]
|
|
68
|
+
if r.returncode != 0:
|
|
69
|
+
tail = (r.stderr or r.stdout).strip().splitlines()[-5:]
|
|
70
|
+
return [_d("warning", "", 0, f"generate command failed in a scratch copy (exit {r.returncode}): {cmd}: "
|
|
71
|
+
+ " | ".join(tail) + " — the configuration may not be generatable")]
|
|
72
|
+
after = _snapshot(scratch)
|
|
73
|
+
out = []
|
|
74
|
+
for rel in sorted(set(before) | set(after)):
|
|
75
|
+
if rel in before and rel not in after:
|
|
76
|
+
continue # build artefacts removed by the generator are not staleness
|
|
77
|
+
a, b = os.path.join(root, rel), os.path.join(scratch, rel)
|
|
78
|
+
if rel not in before:
|
|
79
|
+
out.append(_d("error", rel, 0, f"{rel}: the generator creates this file but it is missing from the tree"))
|
|
80
|
+
continue
|
|
81
|
+
try:
|
|
82
|
+
x, y = open(a, "rb").read(), open(b, "rb").read()
|
|
83
|
+
except OSError:
|
|
84
|
+
continue
|
|
85
|
+
if x == y:
|
|
86
|
+
continue
|
|
87
|
+
xl = x.decode("utf-8", "replace").splitlines()
|
|
88
|
+
yl = y.decode("utf-8", "replace").splitlines()
|
|
89
|
+
diff = [ln for ln in difflib.unified_diff(xl, yl, "tree", "regenerated", n=0, lineterm="")
|
|
90
|
+
if ln.startswith(("+", "-")) and not ln.startswith(("+++", "---"))]
|
|
91
|
+
plus = sum(1 for ln in diff if ln.startswith("+"))
|
|
92
|
+
minus = len(diff) - plus
|
|
93
|
+
first = next((i for i, (p, q) in enumerate(zip(xl, yl)) if p != q), min(len(xl), len(yl))) + 1
|
|
94
|
+
sample = "; ".join(ln[:120] for ln in diff[:4])
|
|
95
|
+
out.append(_d("error", rel, first, f"{rel} is stale: regenerating changes {minus} line(s) / adds {plus} "
|
|
96
|
+
f"(e.g. {sample})"))
|
|
97
|
+
if not out:
|
|
98
|
+
out.append(_d("info", "", 0, f"generated files are up to date (regenerated in {time.time() - t0:.1f}s in a scratch copy)"))
|
|
99
|
+
return out
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _d(sev, file, line, msg):
|
|
103
|
+
return {"file": file, "line": line, "path": "", "rule": "H2L-GEN-001", "severity": sev,
|
|
104
|
+
"message": msg + " [TR_Methodology: generated BSW configuration must be regenerated after ECUC changes]",
|
|
105
|
+
"defRef": None}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Git-history-lite: "similar past changes" for a SW-C, without embeddings.
|
|
2
|
+
|
|
3
|
+
Takes ``git log`` (a bounded number of commits) touching the SW-C's ARXML and C files and those of similar
|
|
4
|
+
SW-Cs, summarises each commit as an ARXML delta (the semantic ``arxml_diff`` of the before/after blobs) plus the
|
|
5
|
+
C diff hunks, and ranks by overlap with the task's terms (ports, data elements, words of the task) and recency.
|
|
6
|
+
Everything is bounded: commits scanned, hunks per commit, characters per summary, results returned.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
import subprocess
|
|
13
|
+
import tempfile
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
MAX_COMMITS = 60
|
|
17
|
+
MAX_HUNK_CHARS = 1500
|
|
18
|
+
MAX_DELTA_ITEMS = 12
|
|
19
|
+
MAX_COMMIT_CHARS = 4000 # all C hunks of one commit together
|
|
20
|
+
MAX_FILES_PER_COMMIT = 6
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _git(root: str, *args, timeout: int = 60) -> Optional[str]:
|
|
24
|
+
try:
|
|
25
|
+
r = subprocess.run(["git", "-C", root, *args], capture_output=True, timeout=timeout)
|
|
26
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
27
|
+
return None
|
|
28
|
+
return r.stdout.decode("utf-8", "replace") if r.returncode == 0 else None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def is_repo(root: str) -> bool:
|
|
32
|
+
return (_git(root, "rev-parse", "--is-inside-work-tree") or "").strip() == "true"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def terms_of(text: str) -> set:
|
|
36
|
+
"""Identifier-ish terms, with CamelCase / snake_case parts, lower-cased; short noise words dropped."""
|
|
37
|
+
out = set()
|
|
38
|
+
for tok in re.findall(r"[A-Za-z_][A-Za-z0-9_]{2,}", text or ""):
|
|
39
|
+
out.add(tok.lower())
|
|
40
|
+
for part in re.findall(r"[A-Z]+(?![a-z])|[A-Z]?[a-z]+|\d+", tok):
|
|
41
|
+
if len(part) >= 3:
|
|
42
|
+
out.add(part.lower())
|
|
43
|
+
return out - STOP
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
STOP = {"the", "and", "for", "with", "from", "this", "that", "into", "when", "add", "use", "new", "void", "rte",
|
|
47
|
+
"std", "returntype", "uint8", "uint16", "uint32", "boolean", "return", "data", "static", "const", "then",
|
|
48
|
+
"should", "must", "set", "get", "true", "false", "null", "ptr", "null_ptr", "not", "are", "has", "its"}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _blob(root: str, rev: str, rel: str) -> Optional[bytes]:
|
|
52
|
+
try:
|
|
53
|
+
r = subprocess.run(["git", "-C", root, "show", f"{rev}:./{rel}"], capture_output=True, timeout=60)
|
|
54
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
55
|
+
return None
|
|
56
|
+
return r.stdout if r.returncode == 0 else None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def arxml_delta(root: str, commit: str, rel: str) -> dict:
|
|
60
|
+
"""Semantic delta of one ARXML file across ``commit`` (parent -> commit), via diff.diff on temp copies."""
|
|
61
|
+
from .diff import diff
|
|
62
|
+
before, after = _blob(root, f"{commit}^", rel), _blob(root, commit, rel)
|
|
63
|
+
if before is None and after is None:
|
|
64
|
+
return {"file": rel, "error": "blob not readable"}
|
|
65
|
+
if before is None or after is None:
|
|
66
|
+
return {"file": rel, "status": "added" if before is None else "deleted"}
|
|
67
|
+
with tempfile.TemporaryDirectory(prefix="h2l-hist-") as tmp:
|
|
68
|
+
a, b = os.path.join(tmp, "a.arxml"), os.path.join(tmp, "b.arxml")
|
|
69
|
+
open(a, "wb").write(before)
|
|
70
|
+
open(b, "wb").write(after)
|
|
71
|
+
try:
|
|
72
|
+
d = diff(a, b, root=tmp)
|
|
73
|
+
except Exception as ex: # noqa: BLE001
|
|
74
|
+
return {"file": rel, "error": f"arxml_diff failed: {ex}"}
|
|
75
|
+
items = [f"+ {x['path']} ({x['tag']})" for x in d["added"]] + [f"- {x['path']} ({x['tag']})" for x in d["removed"]]
|
|
76
|
+
for x in d["changed"]:
|
|
77
|
+
vals = x.get("values")
|
|
78
|
+
items.append(f"~ {x['path']}" + (": " + ", ".join(f"{k} {v[0]}->{v[1]}" for k, v in sorted(vals.items()))
|
|
79
|
+
if vals else " (content)"))
|
|
80
|
+
return {"file": rel, "summary": d["summary"], "items": items[:MAX_DELTA_ITEMS],
|
|
81
|
+
"more": max(0, len(items) - MAX_DELTA_ITEMS)}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def c_hunks(root: str, commit: str, rel: str) -> str:
|
|
85
|
+
out = _git(root, "show", "--format=", "--no-color", "--no-ext-diff", "-U2", commit, "--", rel) or ""
|
|
86
|
+
body = "\n".join(ln for ln in out.splitlines() if not ln.startswith(("diff --git", "index ", "--- ", "+++ ")))
|
|
87
|
+
return body if len(body) <= MAX_HUNK_CHARS else body[:MAX_HUNK_CHARS] + "\n… (hunk truncated)"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def similar_changes(root: str, files: list, related: list, task_terms: set, limit: int = 5,
|
|
91
|
+
max_commits: int = MAX_COMMITS) -> dict:
|
|
92
|
+
"""files: the SW-C's own ARXML/C files (abs); related: those of similar SW-Cs. -> {changes, scanned, notes}."""
|
|
93
|
+
if not is_repo(root):
|
|
94
|
+
return {"changes": [], "scanned": 0, "notes": ["not a git repository; no history"]}
|
|
95
|
+
rels = sorted({os.path.relpath(f, root) for f in files + related if f and os.path.exists(f)})
|
|
96
|
+
own = {os.path.relpath(f, root) for f in files if f}
|
|
97
|
+
if not rels:
|
|
98
|
+
return {"changes": [], "scanned": 0, "notes": ["no files to look up in history"]}
|
|
99
|
+
log = _git(root, "log", f"-n{max_commits}", "--format=%x1e%H%x1f%ct%x1f%s", "--name-only", "--", *rels) or ""
|
|
100
|
+
commits = []
|
|
101
|
+
for chunk in log.split("\x1e"):
|
|
102
|
+
if not chunk.strip():
|
|
103
|
+
continue
|
|
104
|
+
head, *names = chunk.strip("\n").split("\n")
|
|
105
|
+
h, ct, subj = (head.split("\x1f") + ["", ""])[:3]
|
|
106
|
+
commits.append((h, int(ct or 0), subj, [n for n in names if n.strip() in rels]))
|
|
107
|
+
scored = []
|
|
108
|
+
for i, (h, ct, subj, touched) in enumerate(commits):
|
|
109
|
+
text = subj + "\n" + "\n".join(touched)
|
|
110
|
+
overlap = terms_of(text) & task_terms
|
|
111
|
+
scored.append((i, h, ct, subj, touched, overlap))
|
|
112
|
+
# cheap first pass by subject/file overlap + recency; expensive deltas only for the best candidates
|
|
113
|
+
pre = sorted(scored, key=lambda x: (-(len(x[5]) * 2 + (1 if set(x[4]) & own else 0)), x[0]))[:limit * 3]
|
|
114
|
+
out = []
|
|
115
|
+
for i, h, ct, subj, touched, _ov in pre:
|
|
116
|
+
deltas, hunks = [], []
|
|
117
|
+
budget = MAX_COMMIT_CHARS
|
|
118
|
+
# the SW-C's own files first, then the rest, at most MAX_FILES_PER_COMMIT
|
|
119
|
+
touched = sorted(touched, key=lambda r: (r not in own, r))[:MAX_FILES_PER_COMMIT]
|
|
120
|
+
for rel in touched:
|
|
121
|
+
if rel.lower().endswith(".arxml"):
|
|
122
|
+
deltas.append(arxml_delta(root, h, rel))
|
|
123
|
+
elif rel.lower().endswith((".c", ".h")) and budget > 0:
|
|
124
|
+
d = c_hunks(root, h, rel)
|
|
125
|
+
if len(d) > budget:
|
|
126
|
+
d = d[:budget] + "\n… (commit diff truncated)"
|
|
127
|
+
budget -= len(d)
|
|
128
|
+
hunks.append({"file": rel, "diff": d})
|
|
129
|
+
full = subj + "\n" + "\n".join(json_lines(deltas)) + "\n" + "\n".join(x["diff"] for x in hunks)
|
|
130
|
+
overlap = sorted(terms_of(full) & task_terms)
|
|
131
|
+
recency = 1.0 / (1 + i)
|
|
132
|
+
score = round(len(overlap) + recency + (0.5 if set(touched) & own else 0.0), 4)
|
|
133
|
+
out.append({"commit": h[:12], "subject": subj, "score": score, "overlap": overlap[:20],
|
|
134
|
+
"ownFiles": sorted(set(touched) & own), "arxml": deltas, "c": hunks, "_order": i})
|
|
135
|
+
out.sort(key=lambda x: (-x["score"], x["_order"]))
|
|
136
|
+
for x in out:
|
|
137
|
+
x.pop("_order")
|
|
138
|
+
return {"changes": out[:limit], "scanned": len(commits), "notes": []}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def json_lines(deltas: list) -> list:
|
|
142
|
+
lines = []
|
|
143
|
+
for d in deltas:
|
|
144
|
+
lines.append(d.get("file", ""))
|
|
145
|
+
lines.extend(d.get("items", []))
|
|
146
|
+
return lines
|
|
@@ -283,9 +283,12 @@ class Project:
|
|
|
283
283
|
self._load(f)
|
|
284
284
|
self.foreign: dict = {}
|
|
285
285
|
self.input_loaded = False
|
|
286
|
+
from .sysmodel import SystemIndex
|
|
287
|
+
self.system = SystemIndex()
|
|
286
288
|
if load_input:
|
|
287
289
|
for f in self.sources.input:
|
|
288
|
-
self.
|
|
290
|
+
self.system.add_file(f)
|
|
291
|
+
self.foreign = self.system.foreign()
|
|
289
292
|
self.input_loaded = bool(self.sources.input)
|
|
290
293
|
self.reindex()
|
|
291
294
|
|