@h2loop/autosar 1.5.3 → 1.6.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 +1 -0
- package/agents/autosar-integrator.md +1 -0
- package/package.json +1 -1
- package/plugins/index.js +51 -49
- package/python/ecuc_engine/add_like.py +471 -0
- package/python/ecuc_engine/add_signal.py +8 -2
- package/python/ecuc_engine/arxml_edit.py +219 -16
- package/python/ecuc_engine/cli.py +57 -5
- package/skills/autosar/SKILL.md +15 -0
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
"""autosar_add_like: add a new X like an existing Y, end to end, across all ARXML (ECUC + System/SWC), or refuse.
|
|
2
|
+
|
|
3
|
+
Projects name the elements of one thing after it: a message BCM_DoorStatus is the frame BCM_DoorStatus_CanFrame,
|
|
4
|
+
the I-PDU, the PDU/frame triggerings, the EcuC Pdu, the CanIf TX PDU, the PduR routing path and the Com I-PDU, all
|
|
5
|
+
linked by references. Cloning that chain, renamed, gives a new message that is consistent with the project's own
|
|
6
|
+
conventions, in every file at once. The same works for a port, a runnable with its event, a Dem event, an NvM
|
|
7
|
+
block, a calibration parameter: whatever the template's chain is.
|
|
8
|
+
|
|
9
|
+
chain elements whose SHORT-NAME contains `like` and that are linked to each other by references
|
|
10
|
+
(`seed` picks the chain when several unconnected ones carry the stem)
|
|
11
|
+
members inside a cloned element, sub-elements whose name lacks the stem and that refer to elements outside the
|
|
12
|
+
chain (a message's signal mappings, an I-PDU's ComIPduSignalRefs) are the template's own content: they
|
|
13
|
+
are not copied (`keep_members=true` copies them)
|
|
14
|
+
links a list outside the chain that refers to a chain element (ECU-extract FIBEX elements, ...) gets an entry
|
|
15
|
+
for the clone; a single-valued slot is reported, not changed
|
|
16
|
+
ids ECUC *HandleId / *PduId values get the lowest unused value of that parameter
|
|
17
|
+
set {"ParamDefName" | "XML-TAG" | "Owner:key": value} applied to the clones (Owner = part of a clone's
|
|
18
|
+
SHORT-NAME or its tag); every key must match at least one value
|
|
19
|
+
Everything is checked (schema, every reference + DEST) and validated before/after; any new finding rolls back.
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import collections
|
|
24
|
+
import os
|
|
25
|
+
import re
|
|
26
|
+
import uuid
|
|
27
|
+
from typing import Optional
|
|
28
|
+
|
|
29
|
+
from .arxml_edit import ArxmlEditor, FileTree, _ad
|
|
30
|
+
from .model import Project
|
|
31
|
+
from .xmltree import escape, line_indent
|
|
32
|
+
|
|
33
|
+
REF_TAG = re.compile(r"(?:-REF|-TREF|-IREF|^VALUE-REF)$")
|
|
34
|
+
REGISTRY_MIN = 12 # a holder with this many outgoing references is a list of everything, not an owner
|
|
35
|
+
OWNER_DEST = re.compile(r"(?:PORT-PROTOTYPE|RUNNABLE-ENTITY|COMPONENT-PROTOTYPE|SW-COMPONENT-TYPE|INTERNAL-BEHAVIOR)$")
|
|
36
|
+
# Handle IDs exactly as validation sees them (rules/handles.py): with param defs, an integer with SYMBOLIC-NAME-VALUE;
|
|
37
|
+
# without defs, by name.
|
|
38
|
+
from .rules.handles import NAME as HANDLE_NAME # noqa: E402
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class AddLikeError(Exception):
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _is_ref(node) -> bool:
|
|
46
|
+
"""A reference to another configured/modelled element (not DEFINITION-REF, which points at the param def)."""
|
|
47
|
+
return bool(REF_TAG.search(node.tag)) and not node.children and node.tag != "DEFINITION-REF"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _holder(node):
|
|
51
|
+
"""Nearest ancestor (or self) that has a SHORT-NAME: the identifiable that owns a reference."""
|
|
52
|
+
n = node.parent
|
|
53
|
+
while n is not None and n.short_name is None:
|
|
54
|
+
n = n.parent
|
|
55
|
+
return n
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class Graph:
|
|
59
|
+
def __init__(self, ed: ArxmlEditor, files: list):
|
|
60
|
+
self.ed = ed
|
|
61
|
+
self.trees = {}
|
|
62
|
+
self.where = {} # path -> (file, node)
|
|
63
|
+
self.path_of = {} # id(node) -> path
|
|
64
|
+
self.refs = [] # (file, ref node, holder path, target)
|
|
65
|
+
for f in files:
|
|
66
|
+
try:
|
|
67
|
+
ft = ed.load(f)
|
|
68
|
+
except Exception: # noqa: BLE001 -- unparseable files are left alone
|
|
69
|
+
continue
|
|
70
|
+
self.trees[f] = ft
|
|
71
|
+
for p, n in ft.by_path.items():
|
|
72
|
+
self.where.setdefault(p, (f, n))
|
|
73
|
+
self.path_of[id(n)] = p
|
|
74
|
+
for f, ft in self.trees.items():
|
|
75
|
+
for n in ft.root.iter():
|
|
76
|
+
if _is_ref(n):
|
|
77
|
+
t = n.text.strip()
|
|
78
|
+
h = _holder(n)
|
|
79
|
+
hp = self.path_of.get(id(h)) if h is not None else None
|
|
80
|
+
if t.startswith("/") and hp:
|
|
81
|
+
self.refs.append((f, n, hp, t))
|
|
82
|
+
self.referrers = collections.defaultdict(list)
|
|
83
|
+
self.out = collections.defaultdict(list)
|
|
84
|
+
for f, n, hp, t in self.refs:
|
|
85
|
+
self.referrers[t].append((f, n, hp))
|
|
86
|
+
self.out[hp].append((t, n.attrs.get("DEST", "")))
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def name(path: str) -> str:
|
|
90
|
+
return path.rsplit("/", 1)[-1]
|
|
91
|
+
|
|
92
|
+
def under(self, path: str, roots) -> bool:
|
|
93
|
+
return any(path == r or path.startswith(r + "/") for r in roots)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _chain(g: Graph, pairs: list, seed: Optional[str]) -> list:
|
|
97
|
+
stems = [a for a, _ in pairs]
|
|
98
|
+
stem = pairs[-1][0] if len(pairs) == 1 else " / ".join(stems)
|
|
99
|
+
cands = {p for p in g.where if any(a in Graph.name(p) for a in stems)}
|
|
100
|
+
if not cands:
|
|
101
|
+
raise AddLikeError(f"no element's SHORT-NAME contains {stem!r}")
|
|
102
|
+
for a in stems:
|
|
103
|
+
if not any(a in Graph.name(p) for p in cands):
|
|
104
|
+
raise AddLikeError(f"no element's SHORT-NAME contains {a!r}")
|
|
105
|
+
adj = collections.defaultdict(set)
|
|
106
|
+
for f, n, hp, t in g.refs:
|
|
107
|
+
a = next((x for x in _ancestors(hp) if x in cands), None)
|
|
108
|
+
b = next((x for x in _ancestors(t) if x in cands), None)
|
|
109
|
+
if a and b and a != b:
|
|
110
|
+
adj[a].add(b)
|
|
111
|
+
adj[b].add(a)
|
|
112
|
+
for p in cands: # containment links a named child to a named parent
|
|
113
|
+
par = next((x for x in _ancestors(p)[1:] if x in cands), None)
|
|
114
|
+
if par:
|
|
115
|
+
adj[p].add(par)
|
|
116
|
+
adj[par].add(p)
|
|
117
|
+
comps, seen = [], set()
|
|
118
|
+
for p in sorted(cands):
|
|
119
|
+
if p in seen:
|
|
120
|
+
continue
|
|
121
|
+
comp, todo = set(), [p]
|
|
122
|
+
while todo:
|
|
123
|
+
x = todo.pop()
|
|
124
|
+
if x in comp:
|
|
125
|
+
continue
|
|
126
|
+
comp.add(x)
|
|
127
|
+
todo.extend(adj[x] - comp)
|
|
128
|
+
seen |= comp
|
|
129
|
+
comps.append(comp)
|
|
130
|
+
if seed:
|
|
131
|
+
comps = [c for c in comps if any(seed == x or x.endswith("/" + seed.strip("/")) or seed.strip("/") == Graph.name(x)
|
|
132
|
+
for x in c)]
|
|
133
|
+
if not comps:
|
|
134
|
+
raise AddLikeError(f"seed {seed!r} is not an element whose SHORT-NAME contains {stem!r}")
|
|
135
|
+
if len(stems) > 1: # the template and its named members must form ONE chain
|
|
136
|
+
comps = [c for c in comps if all(any(a in Graph.name(x) for x in c) for a in stems)] or comps
|
|
137
|
+
linked = [c for c in comps if len(c) > 1] or comps
|
|
138
|
+
if len(linked) > 1:
|
|
139
|
+
heads = ["; ".join(sorted(c)[:3]) for c in sorted(linked, key=len, reverse=True)[:6]]
|
|
140
|
+
raise AddLikeError(f"{len(linked)} separate chains carry {stem!r}; pass seed = one element of the one you mean: "
|
|
141
|
+
+ " | ".join(heads))
|
|
142
|
+
comp = set(linked[0])
|
|
143
|
+
# A member of the template (named after an `also` stem, not the main one) that points at an outside element
|
|
144
|
+
# OWNING something (a port, runnable, component, behavior) belongs to that owner, not to the template: e.g. the
|
|
145
|
+
# write access of the OLD port to a data element of the cloned interface. Leave it out.
|
|
146
|
+
main = pairs[0][0] if len(pairs) == 1 else next(a for a, _ in pairs if a == g.main)
|
|
147
|
+
named = lambda x: any(a in Graph.name(x) for a in stems) # noqa: E731
|
|
148
|
+
mains = {p for p in comp if main in Graph.name(p)}
|
|
149
|
+
for p in sorted(comp):
|
|
150
|
+
if p in mains or any(a in mains for a in _ancestors(p)[1:]):
|
|
151
|
+
continue
|
|
152
|
+
outs = [(t, d) for hp, lst in g.out.items() if hp == p or hp.startswith(p + "/") for t, d in lst]
|
|
153
|
+
if any(OWNER_DEST.search(d or "") and not named(t) and not any(t == c or t.startswith(c + "/") for c in comp)
|
|
154
|
+
for t, d in outs):
|
|
155
|
+
comp.discard(p)
|
|
156
|
+
return sorted(p for p in comp if not any(a in comp for a in _ancestors(p)[1:])) # top-most only
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _ancestors(path: str) -> list:
|
|
160
|
+
segs = path.strip("/").split("/")
|
|
161
|
+
return ["/" + "/".join(segs[:i]) for i in range(len(segs), 0, -1)]
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _multi(project: Project, ref, wrapper) -> bool:
|
|
165
|
+
"""Whether a reference sits in a multi-valued slot (a list the clone can leave out of)."""
|
|
166
|
+
if wrapper.tag.endswith("REFERENCE-VALUE"):
|
|
167
|
+
dr = wrapper.child("DEFINITION-REF")
|
|
168
|
+
d = project.defs.get(dr.text.strip()) if (dr is not None and project.defs) else None
|
|
169
|
+
if d is not None:
|
|
170
|
+
return d.upper is None or d.upper > 1
|
|
171
|
+
return False # without the def, assume single and keep it
|
|
172
|
+
return wrapper.parent is not None and wrapper.parent.tag.endswith("S")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _wrapper(node, holder):
|
|
176
|
+
"""The smallest element around a reference that can be repeated on its own (ECUC-REFERENCE-VALUE, *-CONDITIONAL)."""
|
|
177
|
+
if node.parent is not None and node.parent.tag.endswith("REFERENCE-VALUE") and \
|
|
178
|
+
all(c is node or c.tag == "DEFINITION-REF" for c in node.parent.children):
|
|
179
|
+
return node.parent # ECUC: the whole ECUC-(INSTANCE-)REFERENCE-VALUE entry
|
|
180
|
+
w = node
|
|
181
|
+
while w.parent is not None and w.parent is not holder and len(w.parent.children) == 1 and not w.parent.tag.endswith("S"):
|
|
182
|
+
w = w.parent
|
|
183
|
+
return w
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def add_like(project: Project, like: str, name: str, seed: Optional[str] = None, set_values: Optional[dict] = None,
|
|
187
|
+
keep_members: bool = False, dry_run: bool = False, also: Optional[dict] = None) -> dict:
|
|
188
|
+
"""also = {template member stem: new stem}, e.g. {"DrvDoorOpenSts": "SettingsAckCounter"}: those members of the
|
|
189
|
+
template (a signal of the message, a data element of the interface, ...) are cloned and renamed too."""
|
|
190
|
+
from .validate import validate
|
|
191
|
+
stem, new = (like or "").strip(), (name or "").strip()
|
|
192
|
+
if not stem or not new:
|
|
193
|
+
raise AddLikeError("like (the name stem of an existing element, e.g. BCM_DoorStatus) and name (the new stem) "
|
|
194
|
+
"are both required")
|
|
195
|
+
pairs = [(stem, new)] + [(str(a).strip(), str(b).strip()) for a, b in (also or {}).items()]
|
|
196
|
+
for a, b in pairs:
|
|
197
|
+
if not a or not re.fullmatch(r"[A-Za-z_]\w*", b):
|
|
198
|
+
raise AddLikeError(f"{a!r} -> {b!r}: the new name must be a valid SHORT-NAME")
|
|
199
|
+
pairs.sort(key=lambda x: len(x[0]), reverse=True) # longer stems first, so one does not eat another
|
|
200
|
+
ed = ArxmlEditor(project)
|
|
201
|
+
files = list(project.sources.config) + list(project.sources.input)
|
|
202
|
+
g = Graph(ed, files)
|
|
203
|
+
g.main = stem
|
|
204
|
+
chain = _chain(g, pairs, seed)
|
|
205
|
+
newpaths = {p: _rn(p, pairs) for p in chain}
|
|
206
|
+
clash = [q for q in newpaths.values() if q in g.where]
|
|
207
|
+
if clash:
|
|
208
|
+
raise AddLikeError(f"already exists: {', '.join(clash[:5])}")
|
|
209
|
+
chain_roots = set(chain)
|
|
210
|
+
in_chain = lambda t: g.under(t, chain_roots) # noqa: E731
|
|
211
|
+
# The template's OWN content: targets referenced only from the chain, or only from other such targets
|
|
212
|
+
# (a message's I-SIGNAL is referenced by its mapping and by its I-SIGNAL-TRIGGERING, which only the message's
|
|
213
|
+
# PDU triggering references). Shared things (a debounce class used by every Dem event) are never "own".
|
|
214
|
+
# Registries (an ECU extract's FIBEX-ELEMENTS, an ECUC value collection: one holder listing many elements) say
|
|
215
|
+
# nothing about ownership and are ignored here; they are still linked for the clones below.
|
|
216
|
+
registry = {hp for hp, lst in g.out.items() if len(lst) >= REGISTRY_MIN}
|
|
217
|
+
targets = {t for _, _, _, t in g.refs if not in_chain(t)}
|
|
218
|
+
exclusive: set = set()
|
|
219
|
+
grew = True
|
|
220
|
+
while grew:
|
|
221
|
+
grew = False
|
|
222
|
+
for t in targets - exclusive:
|
|
223
|
+
owners = [hp for _, _, hp in g.referrers[t] if hp not in registry]
|
|
224
|
+
if owners and all(g.under(hp, chain_roots) or g.under(hp, exclusive) for hp in owners):
|
|
225
|
+
exclusive.add(t)
|
|
226
|
+
grew = True
|
|
227
|
+
named = lambda p: any(a in Graph.name(p) for a, _ in pairs) # noqa: E731
|
|
228
|
+
notes, dropped, linked, single = [], [], [], []
|
|
229
|
+
before = {_dkey(d) for d in validate(project, check_generated=False)["diagnostics"]
|
|
230
|
+
if d["severity"] in ("error", "warning")}
|
|
231
|
+
|
|
232
|
+
# ---------- clone every chain element next to its template ----------
|
|
233
|
+
inserts = collections.defaultdict(list) # file -> [(end offset, bytes)]
|
|
234
|
+
for p in chain:
|
|
235
|
+
f, node = g.where[p]
|
|
236
|
+
ft = g.trees[f]
|
|
237
|
+
data = ft.data
|
|
238
|
+
cuts = [] # (start, end) byte spans inside the element to leave out
|
|
239
|
+
if not keep_members:
|
|
240
|
+
for d in node.iter():
|
|
241
|
+
if d is node:
|
|
242
|
+
continue
|
|
243
|
+
dp = g.path_of.get(id(d))
|
|
244
|
+
if dp and not named(dp):
|
|
245
|
+
outside = [t for t in _refs_below(d) if t in exclusive and not named(t)]
|
|
246
|
+
if outside and not any(c[0] <= d.start < c[1] for c in cuts):
|
|
247
|
+
cuts.append((_line_start(data, d.start), d.end))
|
|
248
|
+
dropped.append(f"{_rn(dp, pairs)} (the template's {d.tag} {Graph.name(dp)})")
|
|
249
|
+
elif _is_ref(d) and not any(c[0] <= d.start < c[1] for c in cuts):
|
|
250
|
+
t = d.text.strip()
|
|
251
|
+
if t in exclusive and not named(t):
|
|
252
|
+
w = _wrapper(d, _holder(d))
|
|
253
|
+
if not _multi(project, d, w): # a single slot (CanIfTxPduBufferRef): keep, it is shared now
|
|
254
|
+
notes.append(f"{_rn(g.path_of.get(id(_holder(d)), ''), pairs)}: {d.tag} still -> {t} "
|
|
255
|
+
"(the template's; single-valued, so the clone shares it; change it if it must not)")
|
|
256
|
+
elif w is not d or d.parent.tag.endswith("S"):
|
|
257
|
+
if not any(c[0] <= w.start < c[1] for c in cuts):
|
|
258
|
+
cuts.append((_line_start(data, w.start), w.end))
|
|
259
|
+
dropped.append(f"{d.tag} -> {t} (the template's own member)")
|
|
260
|
+
text = _excise(data, node.start, node.end, cuts).decode("utf-8")
|
|
261
|
+
text = _retext(text, pairs, in_chain)
|
|
262
|
+
ind = line_indent(data, node.start) or b""
|
|
263
|
+
inserts[f].append((node.end, ft.nl + ind + text.encode("utf-8")))
|
|
264
|
+
|
|
265
|
+
# ---------- link the clones where the template is listed ----------
|
|
266
|
+
for p in chain:
|
|
267
|
+
for f, n, hp in g.referrers.get(p, []):
|
|
268
|
+
if g.under(hp, chain_roots):
|
|
269
|
+
continue
|
|
270
|
+
ft = g.trees[f]
|
|
271
|
+
holder = _holder(n)
|
|
272
|
+
w = _wrapper(n, holder)
|
|
273
|
+
repeatable = w.parent is not None and (w.parent.tag.endswith("S") or w is not n)
|
|
274
|
+
if not repeatable:
|
|
275
|
+
single.append(f"{hp}: {n.tag} -> {p} (single-valued; not changed)")
|
|
276
|
+
continue
|
|
277
|
+
t = ft.data[w.start:w.end].decode("utf-8").replace(f">{p}<", f">{newpaths[p]}<")
|
|
278
|
+
t = re.sub(r'\sUUID="[^"]*"', lambda _m: f' UUID="{uuid.uuid4()}"', t)
|
|
279
|
+
ind = line_indent(ft.data, w.start) or b""
|
|
280
|
+
inserts[f].append((w.end, ft.nl + ind + t.encode("utf-8")))
|
|
281
|
+
linked.append(f"{hp}: {n.tag} -> {newpaths[p]}")
|
|
282
|
+
|
|
283
|
+
for f, ins in inserts.items():
|
|
284
|
+
data = g.trees[f].data
|
|
285
|
+
for off, b in sorted(ins, key=lambda x: x[0], reverse=True):
|
|
286
|
+
data = data[:off] + b + data[off:]
|
|
287
|
+
ed.files[f] = FileTree(f, data)
|
|
288
|
+
|
|
289
|
+
# ---------- handle ids + requested values ----------
|
|
290
|
+
created = sorted(newpaths.values())
|
|
291
|
+
ids = _assign_ids(ed, created)
|
|
292
|
+
applied = _apply_set(ed, created, set_values or {})
|
|
293
|
+
|
|
294
|
+
probs = ed.check()
|
|
295
|
+
changes = {f: ft.data for f, ft in ed.files.items() if ft.data != ed.original.get(f)}
|
|
296
|
+
res = {"like": stem, "name": new, "created": created, "linked": linked, "notCopied": dropped,
|
|
297
|
+
"singleValuedNotLinked": single, "ids": ids, "set": applied, "files": sorted(project.rel(f) for f in changes),
|
|
298
|
+
"notes": notes}
|
|
299
|
+
if probs:
|
|
300
|
+
return {**res, "ok": False, "written": False, "error": "the clone would leave broken ARXML; nothing written",
|
|
301
|
+
"problems": probs[:20]}
|
|
302
|
+
if dry_run:
|
|
303
|
+
return {**res, "ok": True, "written": False}
|
|
304
|
+
originals = {f: ed.original[f] for f in changes}
|
|
305
|
+
for f, b in changes.items():
|
|
306
|
+
with open(f, "wb") as fh:
|
|
307
|
+
fh.write(b)
|
|
308
|
+
after = validate(Project(project.root, project.sources.config, project.sources.defs), check_generated=False)
|
|
309
|
+
fresh = [d for d in after["diagnostics"] if d["severity"] in ("error", "warning")
|
|
310
|
+
and _dkey(d) not in before]
|
|
311
|
+
if fresh:
|
|
312
|
+
for f, b in originals.items():
|
|
313
|
+
with open(f, "wb") as fh:
|
|
314
|
+
fh.write(b)
|
|
315
|
+
return {**res, "ok": False, "written": False, "error": "validation found new problems; all changes rolled back",
|
|
316
|
+
"diagnostics": fresh[:20]}
|
|
317
|
+
return {**res, "ok": True, "written": True, "validation": after["summary"],
|
|
318
|
+
"next": "check each created element against the request (ecuc_read / the diff), set what is still the "
|
|
319
|
+
"template's (ecuc_edit / arxml_edit), add the new signals (autosar_add_signal), then regenerate "
|
|
320
|
+
"and build"}
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _rn(path: str, pairs, new: Optional[str] = None) -> str:
|
|
324
|
+
if isinstance(pairs, str):
|
|
325
|
+
pairs = [(pairs, new)]
|
|
326
|
+
out = []
|
|
327
|
+
for seg in path.split("/"):
|
|
328
|
+
for a, b in pairs:
|
|
329
|
+
if a in seg:
|
|
330
|
+
seg = seg.replace(a, b)
|
|
331
|
+
break
|
|
332
|
+
out.append(seg)
|
|
333
|
+
return "/".join(out)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _refs_below(node) -> list:
|
|
337
|
+
return [d.text.strip() for d in node.iter() if _is_ref(d) and d.text.strip().startswith("/")]
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _line_start(data: bytes, pos: int) -> int:
|
|
341
|
+
if line_indent(data, pos) is not None:
|
|
342
|
+
nl = data.rfind(b"\n", 0, pos)
|
|
343
|
+
return nl if nl >= 0 else pos
|
|
344
|
+
return pos
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _excise(data: bytes, start: int, end: int, cuts: list) -> bytes:
|
|
348
|
+
out, at = [], start
|
|
349
|
+
for a, b in sorted(c for c in cuts if start <= c[0] and c[1] <= end):
|
|
350
|
+
if a < at:
|
|
351
|
+
continue
|
|
352
|
+
out.append(data[at:a])
|
|
353
|
+
at = b
|
|
354
|
+
out.append(data[at:end])
|
|
355
|
+
return b"".join(out)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _retext(text: str, pairs: list, in_chain) -> str:
|
|
359
|
+
"""Rename the stems in SHORT-NAMEs and in references into the chain; fresh UUIDs."""
|
|
360
|
+
text = re.sub(r"(<SHORT-NAME>)([^<]*)(</SHORT-NAME>)", lambda m: m.group(1) + _rn(m.group(2), pairs) + m.group(3), text)
|
|
361
|
+
|
|
362
|
+
def ref(m):
|
|
363
|
+
t = m.group(2).strip()
|
|
364
|
+
return m.group(1) + (_rn(t, pairs) if in_chain(t) else m.group(2)) + m.group(3)
|
|
365
|
+
text = re.sub(r"(<[A-Z0-9-]*(?:-REF|-TREF|-IREF|VALUE-REF)\b[^>]*>)([^<]*)(</)", ref, text)
|
|
366
|
+
return re.sub(r'\sUUID="[^"]*"', lambda _m: f' UUID="{uuid.uuid4()}"', text)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _num_values(ft: FileTree, root):
|
|
370
|
+
"""(DEFINITION-REF, def short name, VALUE node) for each ECUC numerical value below root."""
|
|
371
|
+
for n in root.iter():
|
|
372
|
+
if n.tag == "ECUC-NUMERICAL-PARAM-VALUE":
|
|
373
|
+
d = n.child("DEFINITION-REF")
|
|
374
|
+
v = n.child("VALUE")
|
|
375
|
+
if d is not None and v is not None:
|
|
376
|
+
yield d.text.strip(), d.text.strip().rsplit("/", 1)[-1], v
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _is_handle(project: Project, defref: str, name: str) -> bool:
|
|
380
|
+
d = project.defs.get(defref) if project.defs else None
|
|
381
|
+
if d is not None:
|
|
382
|
+
return d.kind == "INTEGER" and bool(d.symbolic_name)
|
|
383
|
+
return bool(HANDLE_NAME.search(name))
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _range(project: Project, defref: str) -> tuple:
|
|
387
|
+
d = project.defs.get(defref) if project.defs else None
|
|
388
|
+
lo, hi = 0, (1 << 16) - 1
|
|
389
|
+
try:
|
|
390
|
+
if d is not None and d.min not in (None, ""):
|
|
391
|
+
lo = int(str(d.min), 0) + (1 if d.min_open else 0)
|
|
392
|
+
if d is not None and d.max not in (None, ""):
|
|
393
|
+
hi = min(hi, int(str(d.max), 0) - (1 if d.max_open else 0))
|
|
394
|
+
except ValueError:
|
|
395
|
+
pass
|
|
396
|
+
return max(lo, 0), hi
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _assign_ids(ed: ArxmlEditor, created: list) -> list:
|
|
400
|
+
out = []
|
|
401
|
+
for p in created:
|
|
402
|
+
ft, node = ed.tree_for(p)
|
|
403
|
+
todo = [(dr, dn) for dr, dn, _ in _num_values(ft, node) if _is_handle(ed.p, dr, dn)]
|
|
404
|
+
for k, (dr, dn) in enumerate(todo):
|
|
405
|
+
ft, node = ed.tree_for(p) # positions move after each patch
|
|
406
|
+
mine = [v for r, _, v in _num_values(ft, node) if r == dr]
|
|
407
|
+
target = mine[min(k - sum(1 for x in todo[:k] if x[0] != dr), len(mine) - 1)] if mine else None
|
|
408
|
+
if target is None:
|
|
409
|
+
continue
|
|
410
|
+
used = set()
|
|
411
|
+
for ft2 in ed.files.values():
|
|
412
|
+
for r2, _, v2 in _num_values(ft2, ft2.root):
|
|
413
|
+
if r2 == dr and not (ft2 is ft and v2.start == target.start):
|
|
414
|
+
try:
|
|
415
|
+
used.add(int(v2.text.strip(), 0))
|
|
416
|
+
except ValueError:
|
|
417
|
+
pass
|
|
418
|
+
lo, hi = _range(ed.p, dr)
|
|
419
|
+
nid = next((i for i in range(lo, hi + 1) if i not in used), None)
|
|
420
|
+
if nid is None:
|
|
421
|
+
raise AddLikeError(f"no free {dn} value in [{lo}..{hi}]")
|
|
422
|
+
ed.patch(ft, target.start, target.end, f"<VALUE>{nid}</VALUE>".encode())
|
|
423
|
+
out.append(f"{p}/{dn} = {nid}")
|
|
424
|
+
return out
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _apply_set(ed: ArxmlEditor, created: list, values: dict) -> list:
|
|
428
|
+
applied = []
|
|
429
|
+
for key, value in values.items():
|
|
430
|
+
owner, _, k = str(key).rpartition(":")
|
|
431
|
+
hits = 0
|
|
432
|
+
for p in created:
|
|
433
|
+
done = 0
|
|
434
|
+
while True:
|
|
435
|
+
ft, node = ed.tree_for(p) # re-read: every patch moves the byte offsets
|
|
436
|
+
cands = []
|
|
437
|
+
for c in _value_nodes(ft, node, k):
|
|
438
|
+
h = _holder(c) or node
|
|
439
|
+
hp = next((q for q, n in ft.by_path.items() if n is h), p)
|
|
440
|
+
if not owner or owner in hp.rsplit("/", 1)[-1] or owner.upper() == h.tag:
|
|
441
|
+
cands.append((c, hp))
|
|
442
|
+
if done >= len(cands):
|
|
443
|
+
break
|
|
444
|
+
c, hp = cands[done]
|
|
445
|
+
ed.patch(ft, c.start, c.end, f"<{c.tag}>{escape(str(value))}</{c.tag}>".encode())
|
|
446
|
+
applied.append(f"{hp}/{k} = {value}")
|
|
447
|
+
done += 1
|
|
448
|
+
hits += 1
|
|
449
|
+
if not hits:
|
|
450
|
+
raise AddLikeError(f"set {key!r}: no ECUC parameter or element {k!r} in the cloned elements "
|
|
451
|
+
f"({', '.join(created[:6])}); use the parameter's definition name or the XML tag")
|
|
452
|
+
return applied
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _value_nodes(ft: FileTree, root, key: str) -> list:
|
|
456
|
+
"""VALUE nodes of ECUC params named `key` and leaf elements tagged `key` below root."""
|
|
457
|
+
out = []
|
|
458
|
+
for n in root.iter():
|
|
459
|
+
if n.tag in ("ECUC-NUMERICAL-PARAM-VALUE", "ECUC-TEXTUAL-PARAM-VALUE"):
|
|
460
|
+
d, v = n.child("DEFINITION-REF"), n.child("VALUE")
|
|
461
|
+
if d is not None and v is not None and d.text.strip().rsplit("/", 1)[-1] == key:
|
|
462
|
+
out.append(v)
|
|
463
|
+
elif n.tag == key.upper() and not n.children and n.tag != "SHORT-NAME":
|
|
464
|
+
out.append(n)
|
|
465
|
+
return out
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def _dkey(d: dict) -> tuple:
|
|
469
|
+
"""A finding without line numbers (file.arxml:123), so one that only moved because text was inserted above it
|
|
470
|
+
is not taken for a new one."""
|
|
471
|
+
return d["rule"], d["path"], re.sub(r"(\.arxml|\.ARXML|\.dbc):\d+", r"\1", d["message"])
|
|
@@ -95,7 +95,7 @@ def add_signal(project: Project, name: str, ipdu: str, bit_size: Optional[int] =
|
|
|
95
95
|
sib = next((m for m in members if m is not None and (m.def_ref or "").endswith("/ComSignal")), None)
|
|
96
96
|
if sib is None:
|
|
97
97
|
raise AddSignalError(f"{ip.name} has no ComSignal to use as template")
|
|
98
|
-
before = {(d
|
|
98
|
+
before = {_dkey(d) for d in validate(project, check_generated=False)["diagnostics"]
|
|
99
99
|
if d["severity"] in ("error", "warning")}
|
|
100
100
|
notes: list = []
|
|
101
101
|
|
|
@@ -209,7 +209,7 @@ def add_signal(project: Project, name: str, ipdu: str, bit_size: Optional[int] =
|
|
|
209
209
|
# same inputs as `before` (explicit --defs/--config included), so only the edit's own findings count as new
|
|
210
210
|
after = validate(Project(project.root, project.sources.config, project.sources.defs), check_generated=False)
|
|
211
211
|
new = [d for d in after["diagnostics"] if d["severity"] in ("error", "warning")
|
|
212
|
-
and (d
|
|
212
|
+
and _dkey(d) not in before]
|
|
213
213
|
if new:
|
|
214
214
|
for f, b in originals.items():
|
|
215
215
|
with open(f, "wb") as fh:
|
|
@@ -397,3 +397,9 @@ def _dbc_line(project: Project, pdu, name, pos, size, endian, typ, notes, member
|
|
|
397
397
|
out[f] = (text[:block_end] + line + text[block_end:]).encode()
|
|
398
398
|
notes.append(f"added SG_ {name} to BO_ {m.group(1)} {m.group(2)} in {project.rel(f)}")
|
|
399
399
|
return out
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _dkey(d: dict) -> tuple:
|
|
403
|
+
"""A finding without line numbers (file.arxml:123), so one that only moved because text was inserted above it
|
|
404
|
+
is not taken for a new one."""
|
|
405
|
+
return d["rule"], d["path"], re.sub(r"(\.arxml|\.ARXML|\.dbc):\d+", r"\1", d["message"])
|