@h2loop/autosar 0.1.2 → 1.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/README.md +4 -0
- package/agents/autosar-integrator.md +34 -2
- package/package.json +4 -1
- package/plugins/index.js +53 -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 +372 -69
- package/python/ecuc_engine/cli.py +119 -10
- package/python/ecuc_engine/codegen/__init__.py +185 -0
- package/python/ecuc_engine/codegen/binding.py +210 -0
- package/python/ecuc_engine/codegen/bindings/sip_standin.json +111 -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 +395 -0
- package/python/ecuc_engine/codegen/handles.py +89 -0
- package/python/ecuc_engine/codegen/mcal/tc4d.json +60 -0
- package/python/ecuc_engine/codegen/model.py +259 -0
- package/python/ecuc_engine/codegen/modules/__init__.py +7 -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 +133 -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/diff.py +13 -5
- package/python/ecuc_engine/find.py +162 -0
- package/python/ecuc_engine/gen.py +105 -0
- package/python/ecuc_engine/history.py +146 -0
- package/python/ecuc_engine/model.py +33 -29
- package/python/ecuc_engine/paths.py +221 -0
- 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/sysmodel.py +152 -0
- package/python/ecuc_engine/validate.py +52 -3
- package/python/ecuc_engine/verify_rte.py +445 -0
- package/python/pyproject.toml +1 -1
- package/skills/autosar/SKILL.md +57 -1
- package/skills/autosar/references/codegen.md +30 -0
- package/skills/autosar/references/project-layout.md +11 -2
- package/skills/autosar/references/runnable-code.md +50 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Local, dependency-free C function index: definitions, bodies, callees, Rte_* calls and the call graph.
|
|
2
|
+
|
|
3
|
+
Function definitions are found at brace depth 0 in comment/string-stripped text (audit.strip_comments, which
|
|
4
|
+
keeps offsets): a '{' preceded by ')' whose matching '(' follows an identifier is a definition of that
|
|
5
|
+
identifier. Works with AUTOSAR compiler-abstraction macros (FUNC(void, X_CODE) Name(void)) because the name is
|
|
6
|
+
the identifier directly before the parameter list. Preprocessor lines are ignored.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
from . import paths
|
|
16
|
+
from .audit import is_generated, iter_calls, strip_comments
|
|
17
|
+
|
|
18
|
+
KEYWORDS = {"if", "for", "while", "switch", "return", "sizeof", "defined", "else", "do", "case", "goto", "typeof",
|
|
19
|
+
"_Alignof", "__attribute__", "__asm__", "asm", "offsetof"}
|
|
20
|
+
RTE_CALL = re.compile(r"\b(Rte_\w+)\s*\(")
|
|
21
|
+
ANY_CALL = re.compile(r"\b([A-Za-z_]\w*)\s*\(")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class Func:
|
|
26
|
+
name: str
|
|
27
|
+
file: str
|
|
28
|
+
line: int
|
|
29
|
+
end_line: int
|
|
30
|
+
start: int # offset of the declaration start
|
|
31
|
+
body_start: int # offset of '{'
|
|
32
|
+
end: int # offset after '}'
|
|
33
|
+
static: bool
|
|
34
|
+
signature: str
|
|
35
|
+
calls: list = field(default_factory=list) # callee names in order of first use
|
|
36
|
+
rte: list = field(default_factory=list) # [(name, line, args)]
|
|
37
|
+
|
|
38
|
+
def text(self, src: str) -> str:
|
|
39
|
+
return src[self.start:self.end]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _blank_preproc(text: str) -> str:
|
|
43
|
+
out = []
|
|
44
|
+
cont = False
|
|
45
|
+
for ln in text.split("\n"):
|
|
46
|
+
if cont or ln.lstrip().startswith("#"):
|
|
47
|
+
cont = ln.rstrip().endswith("\\")
|
|
48
|
+
out.append(" " * len(ln))
|
|
49
|
+
else:
|
|
50
|
+
out.append(ln)
|
|
51
|
+
return "\n".join(out)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _match_open(s: str, close: int) -> int:
|
|
55
|
+
depth = 0
|
|
56
|
+
for i in range(close, -1, -1):
|
|
57
|
+
if s[i] == ")":
|
|
58
|
+
depth += 1
|
|
59
|
+
elif s[i] == "(":
|
|
60
|
+
depth -= 1
|
|
61
|
+
if depth == 0:
|
|
62
|
+
return i
|
|
63
|
+
return -1
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def functions(path: str, raw: Optional[str] = None) -> tuple:
|
|
67
|
+
"""-> (list of Func, raw source)."""
|
|
68
|
+
if raw is None:
|
|
69
|
+
try:
|
|
70
|
+
raw = open(path, encoding="utf-8", errors="replace").read()
|
|
71
|
+
except OSError:
|
|
72
|
+
return [], ""
|
|
73
|
+
s = _blank_preproc(strip_comments(raw))
|
|
74
|
+
out = []
|
|
75
|
+
depth = 0
|
|
76
|
+
i, n = 0, len(s)
|
|
77
|
+
last_end = 0 # end of the previous top-level construct (for the declaration start)
|
|
78
|
+
while i < n:
|
|
79
|
+
c = s[i]
|
|
80
|
+
if c == "{":
|
|
81
|
+
if depth == 0:
|
|
82
|
+
j = i - 1
|
|
83
|
+
while j >= 0 and s[j].isspace():
|
|
84
|
+
j -= 1
|
|
85
|
+
fn = None
|
|
86
|
+
if j >= 0 and s[j] == ")":
|
|
87
|
+
o = _match_open(s, j)
|
|
88
|
+
k = o - 1
|
|
89
|
+
while k >= 0 and s[k].isspace():
|
|
90
|
+
k -= 1
|
|
91
|
+
m = re.search(r"([A-Za-z_]\w*)$", s[max(0, k - 200):k + 1]) if k >= 0 else None
|
|
92
|
+
if m and m.group(1) not in KEYWORDS:
|
|
93
|
+
fn = m.group(1)
|
|
94
|
+
if fn:
|
|
95
|
+
# matching close brace
|
|
96
|
+
d, e = 0, i
|
|
97
|
+
while e < n:
|
|
98
|
+
if s[e] == "{":
|
|
99
|
+
d += 1
|
|
100
|
+
elif s[e] == "}":
|
|
101
|
+
d -= 1
|
|
102
|
+
if d == 0:
|
|
103
|
+
break
|
|
104
|
+
e += 1
|
|
105
|
+
head_start = max(s.rfind(";", 0, i), s.rfind("}", 0, i), last_end - 1) + 1
|
|
106
|
+
while head_start < i and s[head_start].isspace():
|
|
107
|
+
head_start += 1
|
|
108
|
+
sig = " ".join(s[head_start:i].split())
|
|
109
|
+
f = Func(fn, path, s.count("\n", 0, head_start) + 1, s.count("\n", 0, e) + 1, head_start, i,
|
|
110
|
+
e + 1, bool(re.match(r"(?:\w+\s+)*?static\b", sig)), sig)
|
|
111
|
+
body = s[i:e + 1]
|
|
112
|
+
seen = []
|
|
113
|
+
for mm in ANY_CALL.finditer(body):
|
|
114
|
+
name = mm.group(1)
|
|
115
|
+
if name in KEYWORDS or name == fn:
|
|
116
|
+
continue
|
|
117
|
+
if name not in seen:
|
|
118
|
+
seen.append(name)
|
|
119
|
+
f.calls = seen
|
|
120
|
+
for name, off, _line, args in iter_calls(body, RTE_CALL):
|
|
121
|
+
f.rte.append((name, f.line + body.count("\n", 0, off) + (s.count("\n", head_start, i)), args))
|
|
122
|
+
out.append(f)
|
|
123
|
+
i = e + 1
|
|
124
|
+
last_end = i
|
|
125
|
+
continue
|
|
126
|
+
depth += 1
|
|
127
|
+
elif c == "}":
|
|
128
|
+
depth = max(0, depth - 1)
|
|
129
|
+
if depth == 0:
|
|
130
|
+
last_end = i + 1
|
|
131
|
+
elif c == ";" and depth == 0:
|
|
132
|
+
last_end = i + 1
|
|
133
|
+
i += 1
|
|
134
|
+
return out, raw
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class CIndex:
|
|
138
|
+
"""All function definitions under the given C files (in-house first; generated files kept separately)."""
|
|
139
|
+
|
|
140
|
+
def __init__(self, root: str, files: Optional[list] = None, include_generated: bool = False):
|
|
141
|
+
self.root = root
|
|
142
|
+
all_c = files if files is not None else paths.walk(root, paths.is_c).files
|
|
143
|
+
self.cfiles = sorted(f for f in all_c if f.lower().endswith(".c"))
|
|
144
|
+
self.generated = {f for f in self.cfiles if is_generated(f)}
|
|
145
|
+
self.funcs: dict = {} # name -> [Func]
|
|
146
|
+
self.src: dict = {}
|
|
147
|
+
self.includes: dict = {}
|
|
148
|
+
for f in self.cfiles:
|
|
149
|
+
if f in self.generated and not include_generated:
|
|
150
|
+
continue
|
|
151
|
+
fs, raw = functions(f)
|
|
152
|
+
self.src[f] = raw
|
|
153
|
+
self.includes[f] = re.findall(r'^\s*#\s*include\s*[<"]([^>"]+)[>"]', raw, re.M)
|
|
154
|
+
for fn in fs:
|
|
155
|
+
self.funcs.setdefault(fn.name, []).append(fn)
|
|
156
|
+
|
|
157
|
+
def get(self, name: str, prefer_file: Optional[str] = None) -> Optional[Func]:
|
|
158
|
+
lst = self.funcs.get(name) or []
|
|
159
|
+
if prefer_file:
|
|
160
|
+
for f in lst:
|
|
161
|
+
if f.file == prefer_file:
|
|
162
|
+
return f
|
|
163
|
+
nonstatic = [f for f in lst if not f.static]
|
|
164
|
+
return (nonstatic or lst or [None])[0]
|
|
165
|
+
|
|
166
|
+
def callee(self, caller: Func, name: str) -> Optional[Func]:
|
|
167
|
+
"""Resolve a call from ``caller``: a static function in the same file wins, else a global one."""
|
|
168
|
+
lst = self.funcs.get(name) or []
|
|
169
|
+
same = [f for f in lst if f.file == caller.file]
|
|
170
|
+
if same:
|
|
171
|
+
return same[0]
|
|
172
|
+
glob = [f for f in lst if not f.static]
|
|
173
|
+
return glob[0] if glob else None
|
|
174
|
+
|
|
175
|
+
def reachable(self, start: Func, limit: int = 2000) -> list:
|
|
176
|
+
seen, order, stack = {id(start)}, [start], [start]
|
|
177
|
+
while stack and len(order) < limit:
|
|
178
|
+
f = stack.pop()
|
|
179
|
+
for c in f.calls:
|
|
180
|
+
g = self.callee(f, c)
|
|
181
|
+
if g is not None and id(g) not in seen:
|
|
182
|
+
seen.add(id(g))
|
|
183
|
+
order.append(g)
|
|
184
|
+
stack.append(g)
|
|
185
|
+
return order
|
|
186
|
+
|
|
187
|
+
def rel(self, f: str) -> str:
|
|
188
|
+
r = os.path.relpath(f, self.root)
|
|
189
|
+
return f if r.startswith("..") else r
|
|
@@ -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:
|
|
@@ -9,7 +9,13 @@ from __future__ import annotations
|
|
|
9
9
|
import os
|
|
10
10
|
from typing import Optional
|
|
11
11
|
|
|
12
|
-
from .
|
|
12
|
+
from . import paths
|
|
13
|
+
from .model import MODULE_VALUES, CONTAINER_VALUE, build_file_model
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _expand(target: str, root: Optional[str] = None) -> list:
|
|
17
|
+
"""A dir (recursive), a glob (** allowed) or a file, relative to root -> ARXML files."""
|
|
18
|
+
return paths.expand(root or os.getcwd(), [target], paths.is_arxml)
|
|
13
19
|
from .values import parse_int, parse_float
|
|
14
20
|
from .xmltree import Node
|
|
15
21
|
|
|
@@ -42,8 +48,10 @@ def _canon(node: Node, top: bool = True) -> str:
|
|
|
42
48
|
return f"<{node.tag}[{attrs}]{txt}{''.join(kids)}>"
|
|
43
49
|
|
|
44
50
|
|
|
45
|
-
def _side(target: str) -> dict:
|
|
46
|
-
files =
|
|
51
|
+
def _side(target: str, root: Optional[str] = None) -> dict:
|
|
52
|
+
files = _expand(target, root)
|
|
53
|
+
if not files:
|
|
54
|
+
raise FileNotFoundError(f"{target!r} matched no .arxml file (file, directory or glob, relative to {root or os.getcwd()})")
|
|
47
55
|
out: dict = {}
|
|
48
56
|
for f in files:
|
|
49
57
|
with open(f, "rb") as fh:
|
|
@@ -82,8 +90,8 @@ def _side(target: str) -> dict:
|
|
|
82
90
|
return out
|
|
83
91
|
|
|
84
92
|
|
|
85
|
-
def diff(a: str, b: str, rel=lambda f: f) -> dict:
|
|
86
|
-
A, B = _side(a), _side(b)
|
|
93
|
+
def diff(a: str, b: str, rel=lambda f: f, root: Optional[str] = None) -> dict:
|
|
94
|
+
A, B = _side(a, root), _side(b, root)
|
|
87
95
|
added = [{"path": p, "tag": B[p]["tag"], "at": f"{rel(B[p]['file'])}:{B[p]['line']}"} for p in sorted(set(B) - set(A))]
|
|
88
96
|
removed = [{"path": p, "tag": A[p]["tag"], "at": f"{rel(A[p]['file'])}:{A[p]['line']}"} for p in sorted(set(A) - set(B))]
|
|
89
97
|
changed = []
|
|
@@ -0,0 +1,162 @@
|
|
|
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 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": f"{project.rel(e.file)}:{e.line}",
|
|
120
|
+
"values": {k: (v[0] if len(v) == 1 else v) for k, v in own.items()}})
|
|
121
|
+
return {"count": total, "returned": len(hits), "matches": hits}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
ELEMENTS = {
|
|
125
|
+
"Com": ["Com/ComConfig/ComSignal", "Com/ComConfig/ComIPdu"],
|
|
126
|
+
"PduR": ["PduR/PduRRoutingPaths/PduRRoutingPath"],
|
|
127
|
+
"EcuC": ["EcuC/EcucConfigSet/EcucPduCollection/Pdu"],
|
|
128
|
+
"NvM": ["NvM/NvMBlockDescriptor"],
|
|
129
|
+
"Fee": ["Fee/FeeBlockConfiguration"],
|
|
130
|
+
"Ea": ["Ea/EaBlockConfiguration"],
|
|
131
|
+
"CanIf": ["CanIf/CanIfInitCfg/CanIfTxPduCfg", "CanIf/CanIfInitCfg/CanIfRxPduCfg"],
|
|
132
|
+
"Dem": ["Dem/DemConfigSet/DemEventParameter"],
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def crosscheck(project: Project, module: str, limit: int = 200) -> dict:
|
|
137
|
+
from .rules import RuleContext, run_rules
|
|
138
|
+
mods = [m for m in project.modules if module in (m.name, (m.def_ref or "").rsplit("/", 1)[-1])]
|
|
139
|
+
if not mods:
|
|
140
|
+
return {"error": f"module {module!r} is not configured (modules: {', '.join(sorted({m.name for m in project.modules}))})"}
|
|
141
|
+
roots = [m.path for m in mods]
|
|
142
|
+
diags = [d for d in run_rules(project) if any(d["path"] == r or d["path"].startswith(r + "/") for r in roots)
|
|
143
|
+
or d["rule"].endswith("-000")]
|
|
144
|
+
ctx = RuleContext(project)
|
|
145
|
+
kinds = ELEMENTS.get((mods[0].def_ref or "").rsplit("/", 1)[-1] or module, [])
|
|
146
|
+
rows, counts = [], {"checked": 0, "findings": 0}
|
|
147
|
+
by_path: dict = {}
|
|
148
|
+
for d in diags:
|
|
149
|
+
by_path.setdefault(d["path"], []).append(d)
|
|
150
|
+
for k in kinds:
|
|
151
|
+
for e in ctx.containers(k):
|
|
152
|
+
counts["checked"] += 1
|
|
153
|
+
f = by_path.get(e.path, [])
|
|
154
|
+
if f:
|
|
155
|
+
counts["findings"] += 1
|
|
156
|
+
rows.append({"element": e.path, "def": k.rsplit("/", 1)[-1], "at": f"{project.rel(e.file)}:{e.line}",
|
|
157
|
+
"status": "FINDING", "findings": [f"{x['rule']} {x['severity']}: {x['message']}" for x in f]})
|
|
158
|
+
notes = [d["message"] for d in diags if d["severity"] == "info"]
|
|
159
|
+
other = [d for d in diags if d["severity"] != "info" and not any(r["element"] == d["path"] for r in rows)]
|
|
160
|
+
return {"module": module, "elementKinds": [k.rsplit("/", 1)[-1] for k in kinds], "counts": counts,
|
|
161
|
+
"rows": rows[:limit], "otherFindings": other[:limit], "notes": notes,
|
|
162
|
+
"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}
|