@h2loop/autosar 1.0.1 → 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 +328 -48
- package/python/ecuc_engine/cli.py +114 -5
- 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/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 +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/sysmodel.py +152 -0
- package/python/ecuc_engine/validate.py +15 -2
- package/python/ecuc_engine/verify_rte.py +445 -0
- package/python/pyproject.toml +1 -1
- package/skills/autosar/SKILL.md +47 -1
- package/skills/autosar/references/codegen.md +30 -0
- package/skills/autosar/references/project-layout.md +7 -1
- 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:
|
|
@@ -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}
|
|
@@ -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
|
|