code-constraints 0.1.0__py3-none-any.whl
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.
- code_constraints/__init__.py +1 -0
- code_constraints/cli/__init__.py +0 -0
- code_constraints/cli/__main__.py +1555 -0
- code_constraints/cli/_assets/agents/cdec-architect.md +468 -0
- code_constraints/cli/_assets/agents/oop-refactor-architect.md +317 -0
- code_constraints/cli/_assets/shims/csharp/CodeConstraintsRules.cs +94 -0
- code_constraints/cli/_assets/shims/julia/CdecRules.jl +129 -0
- code_constraints/cli/_assets/shims/lua/cdec_rules.lua +92 -0
- code_constraints/cli/_assets/shims/odin/cdec_rules.odin +67 -0
- code_constraints/cli/_assets/shims/python/cdec_rules.py +94 -0
- code_constraints/cli/_assets/skills/cdec-architecture-loop/SKILL.md +152 -0
- code_constraints/cli/depstamp.py +118 -0
- code_constraints/cli/detect.py +77 -0
- code_constraints/cli/interactive.py +304 -0
- code_constraints/cli/scaffold.py +602 -0
- code_constraints/cli/update.py +157 -0
- code_constraints/core/__init__.py +41 -0
- code_constraints/core/annotations.py +217 -0
- code_constraints/core/associations.py +134 -0
- code_constraints/core/diff.py +302 -0
- code_constraints/core/editor_io.py +280 -0
- code_constraints/core/graph_model.py +681 -0
- code_constraints/core/keys.py +105 -0
- code_constraints/core/model.py +294 -0
- code_constraints/core/model_io.py +65 -0
- code_constraints/core/receivers.py +34 -0
- code_constraints/core/rules.py +177 -0
- code_constraints/core/rulesdoc.py +208 -0
- code_constraints/core/tags.py +114 -0
- code_constraints/core/ts_fingerprint.py +88 -0
- code_constraints/core/xmi_reader.py +358 -0
- code_constraints/core/xmi_writer.py +373 -0
- code_constraints/csharp/__init__.py +3 -0
- code_constraints/csharp/activity.py +250 -0
- code_constraints/csharp/conformance.py +331 -0
- code_constraints/csharp/fingerprint.py +274 -0
- code_constraints/csharp/parser.py +436 -0
- code_constraints/csharp/rules_extract.py +78 -0
- code_constraints/csharp/sequence.py +295 -0
- code_constraints/enforce/__init__.py +15 -0
- code_constraints/enforce/engine.py +122 -0
- code_constraints/enforce/model.py +74 -0
- code_constraints/julia/__init__.py +5 -0
- code_constraints/julia/conformance.py +282 -0
- code_constraints/julia/fingerprint.py +226 -0
- code_constraints/julia/parser.py +523 -0
- code_constraints/julia/rules_extract.py +216 -0
- code_constraints/lint/__init__.py +10 -0
- code_constraints/lint/baseline.py +96 -0
- code_constraints/lint/config.py +239 -0
- code_constraints/lint/engine.py +179 -0
- code_constraints/lint/pipeline.py +108 -0
- code_constraints/lint/report.py +151 -0
- code_constraints/lint/rules/__init__.py +50 -0
- code_constraints/lint/rules/base.py +200 -0
- code_constraints/lint/rules/cyclic_package_dependencies.py +69 -0
- code_constraints/lint/rules/dangling_classes.py +98 -0
- code_constraints/lint/rules/forbidden_package_references.py +47 -0
- code_constraints/lint/rules/forbidden_references.py +48 -0
- code_constraints/lint/rules/frozen_members.py +67 -0
- code_constraints/lint/rules/frozen_rules.py +105 -0
- code_constraints/lint/rules/implementation_locks.py +156 -0
- code_constraints/lint/rules/layer_dependencies.py +92 -0
- code_constraints/lint/rules/max_class_fanout.py +41 -0
- code_constraints/lint/rules/no_new_classes.py +27 -0
- code_constraints/lint/rules/no_removed_classes.py +27 -0
- code_constraints/lint/rules/reference_architecture.py +111 -0
- code_constraints/lint/rules/subclass_naming.py +71 -0
- code_constraints/lint/rules/tag_conformance.py +76 -0
- code_constraints/lock/__init__.py +73 -0
- code_constraints/lock/engine.py +395 -0
- code_constraints/lock/model.py +235 -0
- code_constraints/lock/store.py +144 -0
- code_constraints/lua/__init__.py +5 -0
- code_constraints/lua/conformance.py +239 -0
- code_constraints/lua/fingerprint.py +252 -0
- code_constraints/lua/parser.py +500 -0
- code_constraints/lua/rules_extract.py +55 -0
- code_constraints/mcp/__init__.py +20 -0
- code_constraints/mcp/__main__.py +73 -0
- code_constraints/mcp/server.py +1203 -0
- code_constraints/odin/__init__.py +5 -0
- code_constraints/odin/conformance.py +244 -0
- code_constraints/odin/fingerprint.py +159 -0
- code_constraints/odin/parser.py +471 -0
- code_constraints/odin/rules_extract.py +38 -0
- code_constraints/python/__init__.py +3 -0
- code_constraints/python/activity.py +278 -0
- code_constraints/python/conformance.py +249 -0
- code_constraints/python/fingerprint.py +231 -0
- code_constraints/python/parser.py +330 -0
- code_constraints/python/rules_extract.py +83 -0
- code_constraints/python/sequence.py +257 -0
- code_constraints/reference/__init__.py +15 -0
- code_constraints/reference/compare.py +356 -0
- code_constraints/reference/report.py +38 -0
- code_constraints/svelte/__init__.py +3 -0
- code_constraints/svelte/parser.py +523 -0
- code_constraints/typescript/__init__.py +3 -0
- code_constraints/typescript/parser.py +590 -0
- code_constraints/waivers/__init__.py +89 -0
- code_constraints/waivers/collect.py +167 -0
- code_constraints/waivers/model.py +90 -0
- code_constraints/waivers/ops.py +150 -0
- code_constraints/waivers/review.py +156 -0
- code_constraints/waivers/store.py +300 -0
- code_constraints/web/__init__.py +0 -0
- code_constraints/web/_static/assets/index-3ivBsYY4.css +1 -0
- code_constraints/web/_static/assets/index-BTzTqGFp.js +9 -0
- code_constraints/web/_static/index.html +13 -0
- code_constraints/web/app.py +1076 -0
- code_constraints-0.1.0.dist-info/METADATA +663 -0
- code_constraints-0.1.0.dist-info/RECORD +116 -0
- code_constraints-0.1.0.dist-info/WHEEL +4 -0
- code_constraints-0.1.0.dist-info/entry_points.txt +3 -0
- code_constraints-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"""Serialise `Project` to XMI 2.1 (OMG standard) with a custom diff namespace.
|
|
2
|
+
|
|
3
|
+
XMI itself has no concept of "this element was added / removed in a diff";
|
|
4
|
+
we attach `cdec:status` and `cdec:changeKind` attributes in a private namespace
|
|
5
|
+
that conforming readers are free to ignore.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from hashlib import sha1
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from lxml import etree
|
|
14
|
+
|
|
15
|
+
from code_constraints.core.model import (
|
|
16
|
+
Activity,
|
|
17
|
+
ActivityEdge,
|
|
18
|
+
ActivityNode,
|
|
19
|
+
Association,
|
|
20
|
+
Attribute,
|
|
21
|
+
Class,
|
|
22
|
+
DiffStatus,
|
|
23
|
+
EdgeLayout,
|
|
24
|
+
Layout,
|
|
25
|
+
Message,
|
|
26
|
+
Operation,
|
|
27
|
+
Package,
|
|
28
|
+
Parameter,
|
|
29
|
+
Project,
|
|
30
|
+
RuleAnnotation,
|
|
31
|
+
Sequence,
|
|
32
|
+
Visibility,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
NS = {
|
|
36
|
+
"xmi": "http://www.omg.org/spec/XMI/20110701",
|
|
37
|
+
"uml": "http://www.omg.org/spec/UML/20110701",
|
|
38
|
+
"cdec": "http://code-constraints/ext/1",
|
|
39
|
+
}
|
|
40
|
+
XMI = f"{{{NS['xmi']}}}"
|
|
41
|
+
UML = f"{{{NS['uml']}}}"
|
|
42
|
+
CDEC = f"{{{NS['cdec']}}}"
|
|
43
|
+
|
|
44
|
+
XMI_VERSION = "2.1"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def write_project(project: Project, path: str | Path) -> Path:
|
|
48
|
+
"""Serialise `project` to `path` (XMI 2.1). Returns the path."""
|
|
49
|
+
tree = build_tree(project)
|
|
50
|
+
out = Path(path)
|
|
51
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
52
|
+
tree.write(
|
|
53
|
+
str(out),
|
|
54
|
+
pretty_print=True,
|
|
55
|
+
xml_declaration=True,
|
|
56
|
+
encoding="UTF-8",
|
|
57
|
+
)
|
|
58
|
+
return out
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def build_tree(project: Project) -> etree._ElementTree:
|
|
62
|
+
root = etree.Element(f"{XMI}XMI", nsmap=NS)
|
|
63
|
+
root.set(f"{XMI}version", XMI_VERSION)
|
|
64
|
+
|
|
65
|
+
model = etree.SubElement(root, f"{UML}Model")
|
|
66
|
+
model.set(f"{XMI}id", "model-root")
|
|
67
|
+
model.set("name", "RootModel")
|
|
68
|
+
_set_status_attr(model, DiffStatus.UNCHANGED) # always emit so readers see it
|
|
69
|
+
model.set(f"{CDEC}sourceLanguage", project.source_language)
|
|
70
|
+
if project.root_path:
|
|
71
|
+
model.set(f"{CDEC}rootPath", project.root_path)
|
|
72
|
+
|
|
73
|
+
for pkg in project.packages:
|
|
74
|
+
_write_package(model, pkg)
|
|
75
|
+
|
|
76
|
+
for activity in project.activities:
|
|
77
|
+
_write_activity(model, activity)
|
|
78
|
+
|
|
79
|
+
for seq in project.sequences:
|
|
80
|
+
_write_sequence(model, seq)
|
|
81
|
+
|
|
82
|
+
for assoc in project.associations:
|
|
83
|
+
_write_association(model, assoc)
|
|
84
|
+
|
|
85
|
+
return etree.ElementTree(root)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _write_association(parent: etree._Element, assoc: Association) -> None:
|
|
89
|
+
el = etree.SubElement(parent, "packagedElement")
|
|
90
|
+
el.set(f"{XMI}type", "uml:Association")
|
|
91
|
+
if assoc.name:
|
|
92
|
+
el.set("name", assoc.name)
|
|
93
|
+
el.set(f"{CDEC}source", assoc.source)
|
|
94
|
+
el.set(f"{CDEC}target", assoc.target)
|
|
95
|
+
if assoc.source_multiplicity:
|
|
96
|
+
el.set(f"{CDEC}sourceMultiplicity", assoc.source_multiplicity)
|
|
97
|
+
if assoc.target_multiplicity:
|
|
98
|
+
el.set(f"{CDEC}targetMultiplicity", assoc.target_multiplicity)
|
|
99
|
+
if assoc.source_role:
|
|
100
|
+
el.set(f"{CDEC}sourceRole", assoc.source_role)
|
|
101
|
+
if assoc.target_role:
|
|
102
|
+
el.set(f"{CDEC}targetRole", assoc.target_role)
|
|
103
|
+
_set_status_attr(el, assoc.status)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# ---------- packages / classes ----------
|
|
107
|
+
|
|
108
|
+
def _write_package(parent: etree._Element, pkg: Package) -> None:
|
|
109
|
+
el = etree.SubElement(parent, "packagedElement")
|
|
110
|
+
el.set(f"{XMI}type", "uml:Package")
|
|
111
|
+
el.set(f"{XMI}id", pkg.stable_id())
|
|
112
|
+
el.set("name", pkg.name)
|
|
113
|
+
el.set(f"{CDEC}qualifiedName", pkg.qualified_name)
|
|
114
|
+
_set_status_attr(el, pkg.status)
|
|
115
|
+
_set_layout_attrs(el, pkg.layout)
|
|
116
|
+
_write_owned_comment(el, pkg.description, owner_key=f"Package|{pkg.qualified_name}")
|
|
117
|
+
for cls in pkg.classes:
|
|
118
|
+
_write_class(el, cls)
|
|
119
|
+
for sub in pkg.sub_packages:
|
|
120
|
+
_write_package(el, sub)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _write_class(parent: etree._Element, cls: Class) -> None:
|
|
124
|
+
el = etree.SubElement(parent, "packagedElement")
|
|
125
|
+
el.set(f"{XMI}type", _class_xmi_type(cls.kind))
|
|
126
|
+
el.set(f"{XMI}id", cls.stable_id())
|
|
127
|
+
el.set("name", cls.name)
|
|
128
|
+
el.set(f"{CDEC}qualifiedName", cls.qualified_name)
|
|
129
|
+
el.set(f"{CDEC}kind", cls.kind)
|
|
130
|
+
_set_status_attr(el, cls.status)
|
|
131
|
+
_set_layout_attrs(el, cls.layout)
|
|
132
|
+
if cls.kind == "abstract":
|
|
133
|
+
el.set("isAbstract", "true")
|
|
134
|
+
if cls.location is not None:
|
|
135
|
+
el.set(f"{CDEC}file", cls.location.file)
|
|
136
|
+
el.set(f"{CDEC}startLine", str(cls.location.start_line))
|
|
137
|
+
el.set(f"{CDEC}endLine", str(cls.location.end_line))
|
|
138
|
+
|
|
139
|
+
_write_owned_comment(el, cls.description, owner_key=f"Class|{cls.qualified_name}")
|
|
140
|
+
_write_rules(el, cls.rules)
|
|
141
|
+
|
|
142
|
+
for base in cls.bases:
|
|
143
|
+
gen = etree.SubElement(el, "generalization")
|
|
144
|
+
gen.set(f"{XMI}type", "uml:Generalization")
|
|
145
|
+
gen.set(f"{CDEC}general", base)
|
|
146
|
+
|
|
147
|
+
for dep in cls.dependencies:
|
|
148
|
+
dep_el = etree.SubElement(el, f"{CDEC}dependency")
|
|
149
|
+
dep_el.set("type", dep)
|
|
150
|
+
|
|
151
|
+
for attr in cls.attributes:
|
|
152
|
+
_write_attribute(el, attr, owner_qname=cls.qualified_name)
|
|
153
|
+
for op in cls.operations:
|
|
154
|
+
_write_operation(el, op, owner_qname=cls.qualified_name)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _class_xmi_type(kind: str) -> str:
|
|
158
|
+
if kind == "interface":
|
|
159
|
+
return "uml:Interface"
|
|
160
|
+
if kind == "enum":
|
|
161
|
+
return "uml:Enumeration"
|
|
162
|
+
return "uml:Class"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _write_attribute(parent: etree._Element, attr: Attribute, *, owner_qname: str = "") -> None:
|
|
166
|
+
el = etree.SubElement(parent, "ownedAttribute")
|
|
167
|
+
el.set(f"{XMI}type", "uml:Property")
|
|
168
|
+
el.set("name", attr.name)
|
|
169
|
+
el.set("visibility", attr.visibility.value)
|
|
170
|
+
el.set(f"{CDEC}type", attr.type)
|
|
171
|
+
if attr.is_static:
|
|
172
|
+
el.set("isStatic", "true")
|
|
173
|
+
if attr.is_readonly:
|
|
174
|
+
el.set("isReadOnly", "true")
|
|
175
|
+
if attr.default is not None:
|
|
176
|
+
el.set(f"{CDEC}default", attr.default)
|
|
177
|
+
_set_status_attr(el, attr.status)
|
|
178
|
+
_write_owned_comment(
|
|
179
|
+
el, attr.description, owner_key=f"Attribute|{owner_qname}|{attr.signature()}"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _write_operation(parent: etree._Element, op: Operation, *, owner_qname: str = "") -> None:
|
|
184
|
+
el = etree.SubElement(parent, "ownedOperation")
|
|
185
|
+
el.set(f"{XMI}type", "uml:Operation")
|
|
186
|
+
el.set("name", op.name)
|
|
187
|
+
el.set("visibility", op.visibility.value)
|
|
188
|
+
if op.is_static:
|
|
189
|
+
el.set("isStatic", "true")
|
|
190
|
+
if op.is_abstract:
|
|
191
|
+
el.set("isAbstract", "true")
|
|
192
|
+
_set_status_attr(el, op.status)
|
|
193
|
+
_write_owned_comment(
|
|
194
|
+
el, op.description, owner_key=f"Operation|{owner_qname}|{op.signature()}"
|
|
195
|
+
)
|
|
196
|
+
_write_rules(el, op.rules)
|
|
197
|
+
for param in op.parameters:
|
|
198
|
+
_write_parameter(el, param, direction="in")
|
|
199
|
+
if op.return_type:
|
|
200
|
+
_write_parameter(el, Parameter(name="return", type=op.return_type), direction="return")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _write_parameter(parent: etree._Element, p: Parameter, *, direction: str) -> None:
|
|
204
|
+
el = etree.SubElement(parent, "ownedParameter")
|
|
205
|
+
el.set(f"{XMI}type", "uml:Parameter")
|
|
206
|
+
el.set("name", p.name)
|
|
207
|
+
el.set("direction", direction)
|
|
208
|
+
el.set(f"{CDEC}type", p.type)
|
|
209
|
+
if p.default is not None:
|
|
210
|
+
el.set(f"{CDEC}default", p.default)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# ---------- activities ----------
|
|
214
|
+
|
|
215
|
+
def _write_activity(parent: etree._Element, act: Activity) -> None:
|
|
216
|
+
el = etree.SubElement(parent, "packagedElement")
|
|
217
|
+
el.set(f"{XMI}type", "uml:Activity")
|
|
218
|
+
el.set(f"{XMI}id", act.stable_id())
|
|
219
|
+
el.set("name", act.name)
|
|
220
|
+
el.set(f"{CDEC}granularity", act.granularity)
|
|
221
|
+
_set_status_attr(el, act.status)
|
|
222
|
+
if act.location is not None:
|
|
223
|
+
el.set(f"{CDEC}file", act.location.file)
|
|
224
|
+
el.set(f"{CDEC}startLine", str(act.location.start_line))
|
|
225
|
+
el.set(f"{CDEC}endLine", str(act.location.end_line))
|
|
226
|
+
|
|
227
|
+
for node in act.nodes:
|
|
228
|
+
_write_activity_node(el, node)
|
|
229
|
+
for edge in act.edges:
|
|
230
|
+
_write_activity_edge(el, edge)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
_NODE_XMI_TYPE = {
|
|
234
|
+
"initial": "uml:InitialNode",
|
|
235
|
+
"final": "uml:ActivityFinalNode",
|
|
236
|
+
"action": "uml:OpaqueAction",
|
|
237
|
+
"decision": "uml:DecisionNode",
|
|
238
|
+
"merge": "uml:MergeNode",
|
|
239
|
+
"fork": "uml:ForkNode",
|
|
240
|
+
"join": "uml:JoinNode",
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _write_activity_node(parent: etree._Element, node: ActivityNode) -> None:
|
|
245
|
+
el = etree.SubElement(parent, "node")
|
|
246
|
+
el.set(f"{XMI}type", _NODE_XMI_TYPE[node.kind])
|
|
247
|
+
el.set(f"{XMI}id", node.id)
|
|
248
|
+
if node.label:
|
|
249
|
+
el.set("name", node.label)
|
|
250
|
+
_set_status_attr(el, node.status)
|
|
251
|
+
_set_layout_attrs(el, node.layout)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _write_activity_edge(parent: etree._Element, edge: ActivityEdge) -> None:
|
|
255
|
+
el = etree.SubElement(parent, "edge")
|
|
256
|
+
el.set(f"{XMI}type", "uml:ControlFlow")
|
|
257
|
+
el.set("source", edge.source)
|
|
258
|
+
el.set("target", edge.target)
|
|
259
|
+
if edge.guard:
|
|
260
|
+
el.set(f"{CDEC}guard", edge.guard)
|
|
261
|
+
_set_status_attr(el, edge.status)
|
|
262
|
+
_set_edge_layout_attrs(el, edge.edge_layout)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# ---------- sequences ----------
|
|
266
|
+
|
|
267
|
+
def _write_sequence(parent: etree._Element, seq: Sequence) -> None:
|
|
268
|
+
el = etree.SubElement(parent, "packagedElement")
|
|
269
|
+
el.set(f"{XMI}type", "uml:Interaction")
|
|
270
|
+
el.set(f"{XMI}id", seq.stable_id())
|
|
271
|
+
el.set("name", seq.name)
|
|
272
|
+
_set_status_attr(el, seq.status)
|
|
273
|
+
if seq.location is not None:
|
|
274
|
+
el.set(f"{CDEC}file", seq.location.file)
|
|
275
|
+
el.set(f"{CDEC}startLine", str(seq.location.start_line))
|
|
276
|
+
el.set(f"{CDEC}endLine", str(seq.location.end_line))
|
|
277
|
+
|
|
278
|
+
for lifeline in seq.lifelines:
|
|
279
|
+
ll = etree.SubElement(el, "lifeline")
|
|
280
|
+
ll.set(f"{XMI}type", "uml:Lifeline")
|
|
281
|
+
ll.set("name", lifeline.name)
|
|
282
|
+
ll.set(f"{CDEC}represents", lifeline.represents)
|
|
283
|
+
if lifeline.column_x is not None:
|
|
284
|
+
ll.set(f"{CDEC}columnX", str(lifeline.column_x))
|
|
285
|
+
_set_status_attr(ll, lifeline.status)
|
|
286
|
+
|
|
287
|
+
for msg in seq.messages:
|
|
288
|
+
m = etree.SubElement(el, "message")
|
|
289
|
+
m.set(f"{XMI}type", "uml:Message")
|
|
290
|
+
m.set("name", msg.label)
|
|
291
|
+
m.set(f"{CDEC}sender", msg.sender)
|
|
292
|
+
m.set(f"{CDEC}receiver", msg.receiver)
|
|
293
|
+
if msg.is_return:
|
|
294
|
+
m.set(f"{CDEC}isReturn", "true")
|
|
295
|
+
if msg.guard:
|
|
296
|
+
m.set(f"{CDEC}guard", msg.guard)
|
|
297
|
+
_set_status_attr(m, msg.status)
|
|
298
|
+
|
|
299
|
+
for frag in seq.fragments:
|
|
300
|
+
f = etree.SubElement(el, "fragment")
|
|
301
|
+
f.set(f"{XMI}type", "uml:CombinedFragment")
|
|
302
|
+
f.set(f"{CDEC}kind", frag.kind)
|
|
303
|
+
f.set("name", frag.label)
|
|
304
|
+
f.set(f"{CDEC}startRow", str(frag.start_row))
|
|
305
|
+
f.set(f"{CDEC}endRow", str(frag.end_row))
|
|
306
|
+
_set_status_attr(f, frag.status)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
# ---------- helpers ----------
|
|
310
|
+
|
|
311
|
+
def _write_owned_comment(parent: etree._Element, text: str | None, *, owner_key: str) -> None:
|
|
312
|
+
"""Emit a standards-compliant `<ownedComment xmi:type="uml:Comment">` child.
|
|
313
|
+
|
|
314
|
+
`owner_key` is folded into a deterministic `xmi:id` so repeated emits of the
|
|
315
|
+
same project produce byte-identical XMI.
|
|
316
|
+
"""
|
|
317
|
+
if not text:
|
|
318
|
+
return
|
|
319
|
+
el = etree.SubElement(parent, "ownedComment")
|
|
320
|
+
el.set(f"{XMI}type", "uml:Comment")
|
|
321
|
+
el.set(f"{XMI}id", _comment_id(owner_key))
|
|
322
|
+
body = etree.SubElement(el, "body")
|
|
323
|
+
body.text = text
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _comment_id(owner_key: str) -> str:
|
|
327
|
+
h = sha1(f"comment|{owner_key}".encode("utf-8")).hexdigest()[:16]
|
|
328
|
+
return f"comment-{h}"
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _write_rules(parent: etree._Element, rules: list[RuleAnnotation]) -> None:
|
|
332
|
+
"""Emit one `<cdec:rule>` child per architectural-rule tag, preserving
|
|
333
|
+
declaration order. Args/kwargs round-trip as `<cdec:arg>` / `<cdec:kwarg>`
|
|
334
|
+
children so the source text survives verbatim."""
|
|
335
|
+
for rule in rules:
|
|
336
|
+
el = etree.SubElement(parent, f"{CDEC}rule")
|
|
337
|
+
el.set("name", rule.name)
|
|
338
|
+
for value in rule.args:
|
|
339
|
+
arg = etree.SubElement(el, f"{CDEC}arg")
|
|
340
|
+
arg.set("value", value)
|
|
341
|
+
for key, value in rule.kwargs.items():
|
|
342
|
+
kw = etree.SubElement(el, f"{CDEC}kwarg")
|
|
343
|
+
kw.set("key", key)
|
|
344
|
+
kw.set("value", value)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _set_status_attr(el: etree._Element, status: DiffStatus) -> None:
|
|
348
|
+
el.set(f"{CDEC}status", status.value)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _set_layout_attrs(el: etree._Element, layout: Layout | None) -> None:
|
|
352
|
+
if layout is None:
|
|
353
|
+
return
|
|
354
|
+
el.set(f"{CDEC}x", _fmt_num(layout.x))
|
|
355
|
+
el.set(f"{CDEC}y", _fmt_num(layout.y))
|
|
356
|
+
el.set(f"{CDEC}width", _fmt_num(layout.width))
|
|
357
|
+
el.set(f"{CDEC}height", _fmt_num(layout.height))
|
|
358
|
+
if layout.collapsed:
|
|
359
|
+
el.set(f"{CDEC}collapsed", "true")
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _set_edge_layout_attrs(el: etree._Element, edge_layout: EdgeLayout | None) -> None:
|
|
363
|
+
if edge_layout is None or not edge_layout.waypoints:
|
|
364
|
+
return
|
|
365
|
+
encoded = ";".join(f"{_fmt_num(x)},{_fmt_num(y)}" for x, y in edge_layout.waypoints)
|
|
366
|
+
el.set(f"{CDEC}waypoints", encoded)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _fmt_num(n: float) -> str:
|
|
370
|
+
"""Compact numeric format: integers stay integral, others go through repr."""
|
|
371
|
+
if n == int(n):
|
|
372
|
+
return str(int(n))
|
|
373
|
+
return repr(n)
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""Build a UML `Activity` from a tagged C# region.
|
|
2
|
+
|
|
3
|
+
Mirrors `code_constraints.python.activity` but operates on tree-sitter nodes. Only the
|
|
4
|
+
control-flow granularity is fully featured; statement/calls fall back to
|
|
5
|
+
flat action sequences.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from tree_sitter import Node, Tree
|
|
11
|
+
|
|
12
|
+
from code_constraints.core.model import Activity, ActivityEdge, ActivityNode, SourceLocation
|
|
13
|
+
from code_constraints.core.tags import TagInstance
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def build_activity_from_tag(
|
|
17
|
+
tag: TagInstance, tree: Tree, source: bytes, *, file: str
|
|
18
|
+
) -> Activity | None:
|
|
19
|
+
if not tag.name:
|
|
20
|
+
return None
|
|
21
|
+
granularity = tag.attributes.get("granularity", "control-flow")
|
|
22
|
+
if granularity not in {"control-flow", "statement", "calls"}:
|
|
23
|
+
granularity = "control-flow"
|
|
24
|
+
|
|
25
|
+
stmts = _statements_in_range(tree.root_node, tag.start_line, tag.end_line)
|
|
26
|
+
if not stmts:
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
builder = _ActivityBuilder()
|
|
30
|
+
builder.start()
|
|
31
|
+
if granularity == "statement":
|
|
32
|
+
for s in stmts:
|
|
33
|
+
builder.action(_short(_text(s, source)))
|
|
34
|
+
elif granularity == "calls":
|
|
35
|
+
for s in stmts:
|
|
36
|
+
for call in _walk_calls(s):
|
|
37
|
+
builder.action(_short(_text(call, source)))
|
|
38
|
+
else:
|
|
39
|
+
for s in stmts:
|
|
40
|
+
_emit_control_flow(builder, s, source)
|
|
41
|
+
builder.finish()
|
|
42
|
+
|
|
43
|
+
return Activity(
|
|
44
|
+
name=tag.name,
|
|
45
|
+
nodes=builder.nodes,
|
|
46
|
+
edges=builder.edges,
|
|
47
|
+
granularity=granularity, # type: ignore[arg-type]
|
|
48
|
+
location=SourceLocation(
|
|
49
|
+
file=file, start_line=tag.start_line, end_line=tag.end_line
|
|
50
|
+
),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
_BODY_HOLDERS = {
|
|
55
|
+
"method_declaration",
|
|
56
|
+
"constructor_declaration",
|
|
57
|
+
"local_function_statement",
|
|
58
|
+
"destructor_declaration",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _statements_in_range(root: Node, start: int, end: int) -> list[Node]:
|
|
63
|
+
"""Find the body that contains the tag region, then return its top-level
|
|
64
|
+
statements whose first line falls strictly inside [start, end]."""
|
|
65
|
+
best: list[Node] | None = None
|
|
66
|
+
best_start = -1
|
|
67
|
+
|
|
68
|
+
stack: list[Node] = [root]
|
|
69
|
+
while stack:
|
|
70
|
+
node = stack.pop()
|
|
71
|
+
if node.type in _BODY_HOLDERS:
|
|
72
|
+
body = node.child_by_field_name("body")
|
|
73
|
+
if body is not None and body.type == "block":
|
|
74
|
+
first_stmt = next(
|
|
75
|
+
(c for c in body.named_children if not _is_comment(c)), None
|
|
76
|
+
)
|
|
77
|
+
if first_stmt is None:
|
|
78
|
+
continue
|
|
79
|
+
body_start_line = first_stmt.start_point[0] + 1
|
|
80
|
+
body_end_line = body.end_point[0] + 1
|
|
81
|
+
node_def_line = node.start_point[0] + 1
|
|
82
|
+
if node_def_line < start and body_end_line >= start:
|
|
83
|
+
stmts = [
|
|
84
|
+
c
|
|
85
|
+
for c in body.named_children
|
|
86
|
+
if not _is_comment(c)
|
|
87
|
+
and start < (c.start_point[0] + 1) < end
|
|
88
|
+
]
|
|
89
|
+
if stmts and node_def_line > best_start:
|
|
90
|
+
best = stmts
|
|
91
|
+
best_start = node_def_line
|
|
92
|
+
for child in node.children:
|
|
93
|
+
stack.append(child)
|
|
94
|
+
return best or []
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _is_comment(node: Node) -> bool:
|
|
98
|
+
return node.type == "comment"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class _ActivityBuilder:
|
|
102
|
+
def __init__(self) -> None:
|
|
103
|
+
self.nodes: list[ActivityNode] = []
|
|
104
|
+
self.edges: list[ActivityEdge] = []
|
|
105
|
+
self._counter = 0
|
|
106
|
+
self._last_id: str | None = None
|
|
107
|
+
|
|
108
|
+
def _new_id(self, prefix: str) -> str:
|
|
109
|
+
self._counter += 1
|
|
110
|
+
return f"{prefix}{self._counter}"
|
|
111
|
+
|
|
112
|
+
def _add(self, node: ActivityNode) -> str:
|
|
113
|
+
self.nodes.append(node)
|
|
114
|
+
if self._last_id is not None:
|
|
115
|
+
self.edges.append(ActivityEdge(source=self._last_id, target=node.id))
|
|
116
|
+
self._last_id = node.id
|
|
117
|
+
return node.id
|
|
118
|
+
|
|
119
|
+
def start(self) -> None:
|
|
120
|
+
self._add(ActivityNode(id=self._new_id("init"), kind="initial"))
|
|
121
|
+
|
|
122
|
+
def finish(self) -> None:
|
|
123
|
+
self._add(ActivityNode(id=self._new_id("final"), kind="final"))
|
|
124
|
+
|
|
125
|
+
def action(self, label: str) -> str:
|
|
126
|
+
return self._add(ActivityNode(id=self._new_id("a"), kind="action", label=label))
|
|
127
|
+
|
|
128
|
+
def decision(self, label: str) -> str:
|
|
129
|
+
node = ActivityNode(id=self._new_id("d"), kind="decision", label=label)
|
|
130
|
+
if self._last_id is not None:
|
|
131
|
+
self.edges.append(ActivityEdge(source=self._last_id, target=node.id))
|
|
132
|
+
self.nodes.append(node)
|
|
133
|
+
self._last_id = node.id
|
|
134
|
+
return node.id
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _emit_control_flow(builder: _ActivityBuilder, stmt: Node, source: bytes) -> None:
|
|
138
|
+
if stmt.type == "if_statement":
|
|
139
|
+
_emit_if(builder, stmt, source)
|
|
140
|
+
elif stmt.type in ("for_statement", "for_each_statement", "while_statement"):
|
|
141
|
+
_emit_loop(builder, stmt, source)
|
|
142
|
+
elif stmt.type == "return_statement":
|
|
143
|
+
builder.action(_short("return " + _text(stmt, source)))
|
|
144
|
+
elif stmt.type == "throw_statement":
|
|
145
|
+
builder.action(_short("throw " + _text(stmt, source)))
|
|
146
|
+
elif stmt.type == "try_statement":
|
|
147
|
+
builder.action("try")
|
|
148
|
+
# catch clauses are surfaced as follow-on actions
|
|
149
|
+
for c in stmt.named_children:
|
|
150
|
+
if c.type == "catch_clause":
|
|
151
|
+
builder.action("catch")
|
|
152
|
+
else:
|
|
153
|
+
builder.action(_short(_text(stmt, source)))
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _emit_if(builder: _ActivityBuilder, stmt: Node, source: bytes) -> None:
|
|
157
|
+
cond = stmt.child_by_field_name("condition")
|
|
158
|
+
cond_text = _short(_text(cond, source)) if cond is not None else "?"
|
|
159
|
+
decision_id = builder.decision(cond_text)
|
|
160
|
+
merge_id = builder._new_id("m")
|
|
161
|
+
merge = ActivityNode(id=merge_id, kind="merge")
|
|
162
|
+
|
|
163
|
+
consequence = stmt.child_by_field_name("consequence")
|
|
164
|
+
alternative = stmt.child_by_field_name("alternative")
|
|
165
|
+
|
|
166
|
+
builder._last_id = decision_id
|
|
167
|
+
prev = len(builder.edges)
|
|
168
|
+
if consequence is not None:
|
|
169
|
+
_emit_block_like(builder, consequence, source)
|
|
170
|
+
if len(builder.edges) > prev:
|
|
171
|
+
builder.edges[prev].guard = "yes"
|
|
172
|
+
if builder._last_id is not None:
|
|
173
|
+
builder.edges.append(ActivityEdge(source=builder._last_id, target=merge_id))
|
|
174
|
+
|
|
175
|
+
builder._last_id = decision_id
|
|
176
|
+
prev = len(builder.edges)
|
|
177
|
+
if alternative is not None:
|
|
178
|
+
_emit_block_like(builder, alternative, source)
|
|
179
|
+
if len(builder.edges) > prev:
|
|
180
|
+
builder.edges[prev].guard = "no"
|
|
181
|
+
if builder._last_id is not None:
|
|
182
|
+
builder.edges.append(ActivityEdge(source=builder._last_id, target=merge_id))
|
|
183
|
+
else:
|
|
184
|
+
builder.edges.append(
|
|
185
|
+
ActivityEdge(source=decision_id, target=merge_id, guard="no")
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
builder.nodes.append(merge)
|
|
189
|
+
builder._last_id = merge_id
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _emit_loop(builder: _ActivityBuilder, stmt: Node, source: bytes) -> None:
|
|
193
|
+
cond = stmt.child_by_field_name("condition")
|
|
194
|
+
label = "loop"
|
|
195
|
+
if cond is not None:
|
|
196
|
+
label = _short(_text(cond, source))
|
|
197
|
+
else:
|
|
198
|
+
label = _short(_text(stmt, source).split("{")[0])
|
|
199
|
+
decision_id = builder.decision(label)
|
|
200
|
+
merge_id = builder._new_id("m")
|
|
201
|
+
merge = ActivityNode(id=merge_id, kind="merge")
|
|
202
|
+
|
|
203
|
+
body = stmt.child_by_field_name("body")
|
|
204
|
+
builder._last_id = decision_id
|
|
205
|
+
prev = len(builder.edges)
|
|
206
|
+
if body is not None:
|
|
207
|
+
_emit_block_like(builder, body, source)
|
|
208
|
+
if len(builder.edges) > prev:
|
|
209
|
+
builder.edges[prev].guard = "loop"
|
|
210
|
+
if builder._last_id is not None:
|
|
211
|
+
builder.edges.append(ActivityEdge(source=builder._last_id, target=decision_id))
|
|
212
|
+
|
|
213
|
+
builder.edges.append(
|
|
214
|
+
ActivityEdge(source=decision_id, target=merge_id, guard="exit")
|
|
215
|
+
)
|
|
216
|
+
builder.nodes.append(merge)
|
|
217
|
+
builder._last_id = merge_id
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _emit_block_like(builder: _ActivityBuilder, node: Node, source: bytes) -> None:
|
|
221
|
+
if node.type == "block":
|
|
222
|
+
for child in node.named_children:
|
|
223
|
+
if _is_comment(child):
|
|
224
|
+
continue
|
|
225
|
+
_emit_control_flow(builder, child, source)
|
|
226
|
+
else:
|
|
227
|
+
_emit_control_flow(builder, node, source)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _walk_calls(node: Node) -> list[Node]:
|
|
231
|
+
out: list[Node] = []
|
|
232
|
+
stack = [node]
|
|
233
|
+
while stack:
|
|
234
|
+
n = stack.pop()
|
|
235
|
+
if n.type == "invocation_expression":
|
|
236
|
+
out.append(n)
|
|
237
|
+
for c in n.children:
|
|
238
|
+
stack.append(c)
|
|
239
|
+
return out
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _text(node: Node | None, source: bytes) -> str:
|
|
243
|
+
if node is None:
|
|
244
|
+
return ""
|
|
245
|
+
return source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _short(text: str, limit: int = 60) -> str:
|
|
249
|
+
first = next((line for line in text.splitlines() if line.strip()), text).strip()
|
|
250
|
+
return first[:limit] + ("…" if len(first) > limit else "")
|