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,295 @@
|
|
|
1
|
+
"""Build a UML `Sequence` from a tagged C# region.
|
|
2
|
+
|
|
3
|
+
Mirrors the semantics of the Python parser — see that module for the
|
|
4
|
+
overall design (post-order calls, paired returns, fragments per if-branch,
|
|
5
|
+
self-return for `return X` at the end). This module just adapts the same
|
|
6
|
+
rules to tree-sitter's C# grammar.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from tree_sitter import Node, Tree
|
|
12
|
+
|
|
13
|
+
from code_constraints.core.model import Fragment, Lifeline, Message, Sequence, SourceLocation
|
|
14
|
+
from code_constraints.core.tags import TagInstance
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def build_sequence_from_tag(
|
|
18
|
+
tag: TagInstance, tree: Tree, source: bytes, *, file: str
|
|
19
|
+
) -> Sequence | None:
|
|
20
|
+
if not tag.name:
|
|
21
|
+
return None
|
|
22
|
+
root = tag.attributes.get("root", "this")
|
|
23
|
+
stmts = _statements_in_range(tree.root_node, tag.start_line, tag.end_line)
|
|
24
|
+
if not stmts:
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
state = _State(root=root, source=source)
|
|
28
|
+
_emit_messages(stmts, [], state)
|
|
29
|
+
|
|
30
|
+
return Sequence(
|
|
31
|
+
name=tag.name,
|
|
32
|
+
lifelines=list(state.lifelines.values()),
|
|
33
|
+
messages=state.messages,
|
|
34
|
+
fragments=state.fragments,
|
|
35
|
+
location=SourceLocation(file=file, start_line=tag.start_line, end_line=tag.end_line),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class _State:
|
|
40
|
+
def __init__(self, root: str, source: bytes) -> None:
|
|
41
|
+
self.root = root
|
|
42
|
+
self.source = source
|
|
43
|
+
self.lifelines: dict[str, Lifeline] = {root: Lifeline(name=root, represents=root)}
|
|
44
|
+
self.messages: list[Message] = []
|
|
45
|
+
self.fragments: list[Fragment] = []
|
|
46
|
+
|
|
47
|
+
def ensure_lifeline(self, name: str) -> None:
|
|
48
|
+
if name not in self.lifelines:
|
|
49
|
+
self.lifelines[name] = Lifeline(name=name, represents=name)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
_BODY_HOLDERS = {
|
|
53
|
+
"method_declaration",
|
|
54
|
+
"constructor_declaration",
|
|
55
|
+
"local_function_statement",
|
|
56
|
+
"destructor_declaration",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _statements_in_range(root: Node, start: int, end: int) -> list[Node]:
|
|
61
|
+
best: list[Node] | None = None
|
|
62
|
+
best_start = -1
|
|
63
|
+
stack: list[Node] = [root]
|
|
64
|
+
while stack:
|
|
65
|
+
node = stack.pop()
|
|
66
|
+
if node.type in _BODY_HOLDERS:
|
|
67
|
+
body = node.child_by_field_name("body")
|
|
68
|
+
if body is not None and body.type == "block":
|
|
69
|
+
node_def_line = node.start_point[0] + 1
|
|
70
|
+
body_end_line = body.end_point[0] + 1
|
|
71
|
+
if node_def_line < start and body_end_line >= start:
|
|
72
|
+
stmts = [
|
|
73
|
+
c
|
|
74
|
+
for c in body.named_children
|
|
75
|
+
if c.type != "comment"
|
|
76
|
+
and start < (c.start_point[0] + 1) < end
|
|
77
|
+
]
|
|
78
|
+
if stmts and node_def_line > best_start:
|
|
79
|
+
best = stmts
|
|
80
|
+
best_start = node_def_line
|
|
81
|
+
for c in node.children:
|
|
82
|
+
stack.append(c)
|
|
83
|
+
return best or []
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _emit_messages(
|
|
87
|
+
stmts: list[Node],
|
|
88
|
+
active_guards: list[str],
|
|
89
|
+
state: _State,
|
|
90
|
+
) -> None:
|
|
91
|
+
guard_str = " and ".join(active_guards)
|
|
92
|
+
for stmt in stmts:
|
|
93
|
+
if stmt.type == "if_statement":
|
|
94
|
+
_emit_if(stmt, active_guards, state)
|
|
95
|
+
elif stmt.type == "return_statement":
|
|
96
|
+
value_node = _return_value(stmt)
|
|
97
|
+
_emit_calls_in(value_node, guard_str, state, return_label="")
|
|
98
|
+
value_text = _text(value_node, state.source).strip() if value_node else ""
|
|
99
|
+
label = f"return {value_text}" if value_text else "return"
|
|
100
|
+
state.messages.append(
|
|
101
|
+
Message(
|
|
102
|
+
sender=state.root,
|
|
103
|
+
receiver=state.root,
|
|
104
|
+
label=label,
|
|
105
|
+
is_return=True,
|
|
106
|
+
guard=guard_str,
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
elif stmt.type == "local_declaration_statement":
|
|
110
|
+
# Walk variable_declarator(s); the initializer is the last named
|
|
111
|
+
# child of the declarator. We pair the OUTERMOST call's return
|
|
112
|
+
# with the variable name.
|
|
113
|
+
for declarator, init_node in _iter_declarators(stmt):
|
|
114
|
+
name = ""
|
|
115
|
+
name_node = declarator.child_by_field_name("name")
|
|
116
|
+
if name_node is not None:
|
|
117
|
+
name = _text(name_node, state.source).strip()
|
|
118
|
+
_emit_calls_in(init_node, guard_str, state, return_label=name)
|
|
119
|
+
elif stmt.type == "expression_statement":
|
|
120
|
+
inner = stmt.named_child(0) if stmt.named_child_count else None
|
|
121
|
+
# If it's an assignment_expression and the RHS is a single
|
|
122
|
+
# invocation, treat it like a local declaration so the return
|
|
123
|
+
# gets labeled with the LHS target name.
|
|
124
|
+
if inner is not None and inner.type == "assignment_expression":
|
|
125
|
+
left = inner.child_by_field_name("left")
|
|
126
|
+
right = inner.child_by_field_name("right")
|
|
127
|
+
target = _text(left, state.source).strip() if left else ""
|
|
128
|
+
_emit_calls_in(right, guard_str, state, return_label=target)
|
|
129
|
+
else:
|
|
130
|
+
_emit_calls_in(stmt, guard_str, state, return_label="")
|
|
131
|
+
else:
|
|
132
|
+
_emit_calls_in(stmt, guard_str, state, return_label="")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _iter_declarators(local_decl: Node):
|
|
136
|
+
"""Yield (declarator, initializer_expr) pairs in `local_declaration_statement`."""
|
|
137
|
+
var_decl = None
|
|
138
|
+
for c in local_decl.named_children:
|
|
139
|
+
if c.type == "variable_declaration":
|
|
140
|
+
var_decl = c
|
|
141
|
+
break
|
|
142
|
+
if var_decl is None:
|
|
143
|
+
return
|
|
144
|
+
for declarator in var_decl.named_children:
|
|
145
|
+
if declarator.type != "variable_declarator":
|
|
146
|
+
continue
|
|
147
|
+
# The initializer expression is the named child that isn't `name`.
|
|
148
|
+
name_node = declarator.child_by_field_name("name")
|
|
149
|
+
init = None
|
|
150
|
+
for c in declarator.named_children:
|
|
151
|
+
if c is name_node:
|
|
152
|
+
continue
|
|
153
|
+
init = c
|
|
154
|
+
yield declarator, init
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _return_value(return_stmt: Node) -> Node | None:
|
|
158
|
+
"""Get the optional expression child of a `return_statement`."""
|
|
159
|
+
for c in return_stmt.named_children:
|
|
160
|
+
if c.type != "comment":
|
|
161
|
+
return c
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _emit_if(stmt: Node, active_guards: list[str], state: _State) -> None:
|
|
166
|
+
cond_node = stmt.child_by_field_name("condition")
|
|
167
|
+
cond_text = _text(cond_node, state.source).strip() if cond_node else ""
|
|
168
|
+
|
|
169
|
+
# Calls inside the condition execute unconditionally (relative to the
|
|
170
|
+
# enclosing context) — keep current guards.
|
|
171
|
+
if cond_node is not None:
|
|
172
|
+
_emit_calls_in(cond_node, " and ".join(active_guards), state, return_label="")
|
|
173
|
+
|
|
174
|
+
consequence = stmt.child_by_field_name("consequence")
|
|
175
|
+
has_else = stmt.child_by_field_name("alternative") is not None
|
|
176
|
+
if consequence is not None:
|
|
177
|
+
body_start = len(state.messages)
|
|
178
|
+
_emit_messages(
|
|
179
|
+
_branch_stmts(consequence),
|
|
180
|
+
active_guards + [cond_text] if cond_text else active_guards,
|
|
181
|
+
state,
|
|
182
|
+
)
|
|
183
|
+
body_end = len(state.messages) - 1
|
|
184
|
+
if body_end >= body_start:
|
|
185
|
+
state.fragments.append(
|
|
186
|
+
Fragment(
|
|
187
|
+
kind="alt" if has_else else "opt",
|
|
188
|
+
label=cond_text,
|
|
189
|
+
start_row=body_start,
|
|
190
|
+
end_row=body_end,
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
alternative = stmt.child_by_field_name("alternative")
|
|
195
|
+
if alternative is not None:
|
|
196
|
+
neg = f"!({cond_text})" if cond_text else ""
|
|
197
|
+
else_start = len(state.messages)
|
|
198
|
+
_emit_messages(
|
|
199
|
+
_branch_stmts(alternative),
|
|
200
|
+
active_guards + [neg] if neg else active_guards,
|
|
201
|
+
state,
|
|
202
|
+
)
|
|
203
|
+
else_end = len(state.messages) - 1
|
|
204
|
+
if else_end >= else_start:
|
|
205
|
+
state.fragments.append(
|
|
206
|
+
Fragment(
|
|
207
|
+
kind="alt",
|
|
208
|
+
label=f"else ({cond_text} is false)",
|
|
209
|
+
start_row=else_start,
|
|
210
|
+
end_row=else_end,
|
|
211
|
+
)
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _branch_stmts(node: Node) -> list[Node]:
|
|
216
|
+
if node.type == "block":
|
|
217
|
+
return [c for c in node.named_children if c.type != "comment"]
|
|
218
|
+
return [node]
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _emit_calls_in(
|
|
222
|
+
node: Node | None,
|
|
223
|
+
guard: str,
|
|
224
|
+
state: _State,
|
|
225
|
+
*,
|
|
226
|
+
return_label: str,
|
|
227
|
+
) -> None:
|
|
228
|
+
if node is None:
|
|
229
|
+
return
|
|
230
|
+
invs = _invocations_post_order(node)
|
|
231
|
+
if not invs:
|
|
232
|
+
return
|
|
233
|
+
last_idx = len(invs) - 1
|
|
234
|
+
for i, inv in enumerate(invs):
|
|
235
|
+
rl = return_label if i == last_idx else ""
|
|
236
|
+
_emit_call(inv, state.root, guard, state, return_label=rl)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _invocations_post_order(node: Node) -> list[Node]:
|
|
240
|
+
out: list[Node] = []
|
|
241
|
+
|
|
242
|
+
def visit(n: Node) -> None:
|
|
243
|
+
for c in n.children:
|
|
244
|
+
visit(c)
|
|
245
|
+
if n.type == "invocation_expression":
|
|
246
|
+
out.append(n)
|
|
247
|
+
|
|
248
|
+
visit(node)
|
|
249
|
+
return out
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _emit_call(
|
|
253
|
+
inv: Node,
|
|
254
|
+
root: str,
|
|
255
|
+
guard: str,
|
|
256
|
+
state: _State,
|
|
257
|
+
*,
|
|
258
|
+
return_label: str,
|
|
259
|
+
) -> None:
|
|
260
|
+
sender, receiver, label = _resolve_invocation(inv, state.source, root)
|
|
261
|
+
state.ensure_lifeline(receiver)
|
|
262
|
+
state.ensure_lifeline(sender)
|
|
263
|
+
state.messages.append(
|
|
264
|
+
Message(sender=sender, receiver=receiver, label=label, guard=guard)
|
|
265
|
+
)
|
|
266
|
+
state.messages.append(
|
|
267
|
+
Message(
|
|
268
|
+
sender=receiver,
|
|
269
|
+
receiver=sender,
|
|
270
|
+
label=return_label,
|
|
271
|
+
is_return=True,
|
|
272
|
+
guard=guard,
|
|
273
|
+
)
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _resolve_invocation(
|
|
278
|
+
inv: Node, source: bytes, root: str
|
|
279
|
+
) -> tuple[str, str, str]:
|
|
280
|
+
func = inv.child_by_field_name("function")
|
|
281
|
+
if func is None:
|
|
282
|
+
return root, "module", "()"
|
|
283
|
+
if func.type == "member_access_expression":
|
|
284
|
+
expr = func.child_by_field_name("expression")
|
|
285
|
+
name = func.child_by_field_name("name")
|
|
286
|
+
receiver = _text(expr, source) if expr else "obj"
|
|
287
|
+
label = (_text(name, source) if name else "method") + "()"
|
|
288
|
+
return root, receiver, label
|
|
289
|
+
return root, "module", _text(func, source) + "()"
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _text(node: Node | None, source: bytes) -> str:
|
|
293
|
+
if node is None:
|
|
294
|
+
return ""
|
|
295
|
+
return source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Implementation-conformance engine (Engine B).
|
|
2
|
+
|
|
3
|
+
Answers "does the code actually obey the constraint tags written on it" by
|
|
4
|
+
re-parsing the source and inspecting method bodies. Driven by the
|
|
5
|
+
`tag-conformance` rule type in `.cdec/rules.yaml`, which is what `cdec check`
|
|
6
|
+
runs; this package stays decoupled from `code_constraints.lint`, and the rule
|
|
7
|
+
is a thin adapter over `engine.enforce`.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from code_constraints.enforce.engine import enforce
|
|
13
|
+
from code_constraints.enforce.model import Finding, findings_to_json, format_findings
|
|
14
|
+
|
|
15
|
+
__all__ = ["enforce", "Finding", "format_findings", "findings_to_json"]
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Dispatcher for the implementation-conformance engine (Engine B).
|
|
2
|
+
|
|
3
|
+
This re-parses the current source independently and inspects method bodies. It
|
|
4
|
+
never consults the reference model or the diff metadata (that is Engine A).
|
|
5
|
+
Findings are filtered against the exceptions ledger by the *caller* (the
|
|
6
|
+
`tag-conformance` rule), so the engine itself stays a pure function of the
|
|
7
|
+
source.
|
|
8
|
+
|
|
9
|
+
Per-language body analysis (`no-instantiation`, `factory`, `immutable`) lives in
|
|
10
|
+
the language packages' `conformance` modules. The `sealed` check is structural
|
|
11
|
+
and cross-file, so it's evaluated here against the parsed `Project` model.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from code_constraints.core.model import Project
|
|
19
|
+
from code_constraints.enforce.model import Finding
|
|
20
|
+
|
|
21
|
+
SEALED_RULE = "sealed"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def enforce(root: str | Path, lang: str) -> list[Finding]:
|
|
25
|
+
root_path = Path(root).resolve()
|
|
26
|
+
project = _parse(root_path, lang)
|
|
27
|
+
findings: list[Finding] = []
|
|
28
|
+
findings.extend(_check_sealed(project))
|
|
29
|
+
findings.extend(_analyze_bodies(root_path, lang, project))
|
|
30
|
+
return findings
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _check_sealed(project: Project) -> list[Finding]:
|
|
34
|
+
"""A `@sealed` class may not be subclassed.
|
|
35
|
+
|
|
36
|
+
Tree-sitter / ast give us textual base names (not resolved qnames), so match
|
|
37
|
+
a sealed class by short name against every other class's base list.
|
|
38
|
+
"""
|
|
39
|
+
sealed_names: dict[str, object] = {
|
|
40
|
+
cls.name: cls
|
|
41
|
+
for cls in project.iter_classes()
|
|
42
|
+
if any(r.name == SEALED_RULE for r in cls.rules)
|
|
43
|
+
}
|
|
44
|
+
if not sealed_names:
|
|
45
|
+
return []
|
|
46
|
+
findings: list[Finding] = []
|
|
47
|
+
for cls in project.iter_classes():
|
|
48
|
+
for base in cls.bases:
|
|
49
|
+
short = base.rsplit(".", 1)[-1]
|
|
50
|
+
if short in sealed_names and short != cls.name:
|
|
51
|
+
loc = cls.location
|
|
52
|
+
findings.append(
|
|
53
|
+
Finding(
|
|
54
|
+
rule=SEALED_RULE,
|
|
55
|
+
qualified_name=cls.qualified_name,
|
|
56
|
+
message=(
|
|
57
|
+
f"'{cls.qualified_name}' subclasses sealed class "
|
|
58
|
+
f"'{short}'; sealed types may not be subclassed."
|
|
59
|
+
),
|
|
60
|
+
detail=f"base:{short}",
|
|
61
|
+
file=loc.file if loc else "",
|
|
62
|
+
line=loc.start_line if loc else 0,
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
return findings
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _analyze_bodies(root: Path, lang: str, project: Project) -> list[Finding]:
|
|
69
|
+
if lang == "python":
|
|
70
|
+
from code_constraints.python.conformance import analyze
|
|
71
|
+
|
|
72
|
+
return analyze(root, project)
|
|
73
|
+
if lang == "csharp":
|
|
74
|
+
from code_constraints.csharp.conformance import analyze
|
|
75
|
+
|
|
76
|
+
return analyze(root, project)
|
|
77
|
+
if lang == "odin":
|
|
78
|
+
from code_constraints.odin.conformance import analyze
|
|
79
|
+
|
|
80
|
+
return analyze(root, project)
|
|
81
|
+
if lang == "lua":
|
|
82
|
+
from code_constraints.lua.conformance import analyze
|
|
83
|
+
|
|
84
|
+
return analyze(root, project)
|
|
85
|
+
if lang == "julia":
|
|
86
|
+
from code_constraints.julia.conformance import analyze
|
|
87
|
+
|
|
88
|
+
return analyze(root, project)
|
|
89
|
+
# Other languages have no body analyzer yet.
|
|
90
|
+
return []
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _parse(root: Path, lang: str) -> Project:
|
|
94
|
+
if lang == "python":
|
|
95
|
+
from code_constraints.python import parse_project
|
|
96
|
+
|
|
97
|
+
return parse_project(root)
|
|
98
|
+
if lang == "csharp":
|
|
99
|
+
from code_constraints.csharp import parse_project
|
|
100
|
+
|
|
101
|
+
return parse_project(root)
|
|
102
|
+
if lang == "typescript":
|
|
103
|
+
from code_constraints.typescript import parse_project
|
|
104
|
+
|
|
105
|
+
return parse_project(root)
|
|
106
|
+
if lang == "svelte":
|
|
107
|
+
from code_constraints.svelte import parse_project
|
|
108
|
+
|
|
109
|
+
return parse_project(root)
|
|
110
|
+
if lang == "odin":
|
|
111
|
+
from code_constraints.odin import parse_project
|
|
112
|
+
|
|
113
|
+
return parse_project(root)
|
|
114
|
+
if lang == "lua":
|
|
115
|
+
from code_constraints.lua import parse_project
|
|
116
|
+
|
|
117
|
+
return parse_project(root)
|
|
118
|
+
if lang == "julia":
|
|
119
|
+
from code_constraints.julia import parse_project
|
|
120
|
+
|
|
121
|
+
return parse_project(root)
|
|
122
|
+
raise ValueError(f"unsupported language: {lang}")
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Result types for the conformance engine (Engine B).
|
|
2
|
+
|
|
3
|
+
Deliberately separate from `code_constraints.lint.Violation`: the two engines
|
|
4
|
+
are decoupled by design. Engine A answers "did the architectural intent drift
|
|
5
|
+
over time"; Engine B answers "does the code actually obey the tag right now" by
|
|
6
|
+
inspecting method bodies. They share the rule *catalog* and the review-key
|
|
7
|
+
scheme (`code_constraints.core.keys`) so one exceptions list can cover both —
|
|
8
|
+
nothing else. Both are reached through rule types in `.cdec/rules.yaml`.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
|
|
15
|
+
from code_constraints.core.keys import make_key
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class Finding:
|
|
20
|
+
"""A single conformance violation found by a body analyzer."""
|
|
21
|
+
|
|
22
|
+
rule: str # catalog id, e.g. "no-instantiation"
|
|
23
|
+
qualified_name: str # the element the violation is attributed to
|
|
24
|
+
message: str
|
|
25
|
+
file: str = ""
|
|
26
|
+
line: int = 0
|
|
27
|
+
# Stable discriminator between two findings of the same rule on the same
|
|
28
|
+
# element — e.g. "settle->Invoice" for a construction, "rename.total" for a
|
|
29
|
+
# field reassignment. Deliberately free of line numbers so the review key
|
|
30
|
+
# survives edits elsewhere in the file. Every producer sets it; an empty
|
|
31
|
+
# detail just means the rule can only fire once per element.
|
|
32
|
+
detail: str = ""
|
|
33
|
+
|
|
34
|
+
def fingerprint(self) -> tuple[str, str, str, int]:
|
|
35
|
+
return (self.rule, self.qualified_name, self.file, self.line)
|
|
36
|
+
|
|
37
|
+
def key(self) -> str:
|
|
38
|
+
"""Stable review key (`F-…`) — see `code_constraints.core.keys`."""
|
|
39
|
+
return make_key("enforce", self.rule, self.qualified_name, self.detail)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def format_findings(findings: list[Finding], *, suppressed: int = 0) -> str:
|
|
43
|
+
"""Render findings as a human-readable report.
|
|
44
|
+
|
|
45
|
+
Each line leads with the review key so the output can be saved, marked up,
|
|
46
|
+
and fed back through `cdec exceptions patch`.
|
|
47
|
+
"""
|
|
48
|
+
if not findings:
|
|
49
|
+
text = "no conformance violations.\n"
|
|
50
|
+
if suppressed:
|
|
51
|
+
text += f"({suppressed} finding(s) silenced by an exception.)\n"
|
|
52
|
+
return text
|
|
53
|
+
lines = [f"{len(findings)} conformance violation(s):"]
|
|
54
|
+
for f in sorted(findings, key=lambda x: (x.file, x.line, x.rule)):
|
|
55
|
+
loc = f"{f.file}:{f.line}" if f.file else f.qualified_name
|
|
56
|
+
lines.append(f" - [{f.key()}] [{f.rule}] {loc}: {f.message}")
|
|
57
|
+
if suppressed:
|
|
58
|
+
lines.append(f"({suppressed} finding(s) silenced by an exception.)")
|
|
59
|
+
return "\n".join(lines) + "\n"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def findings_to_json(findings: list[Finding]) -> list[dict]:
|
|
63
|
+
return [
|
|
64
|
+
{
|
|
65
|
+
"key": f.key(),
|
|
66
|
+
"rule": f.rule,
|
|
67
|
+
"qualifiedName": f.qualified_name,
|
|
68
|
+
"detail": f.detail,
|
|
69
|
+
"message": f.message,
|
|
70
|
+
"file": f.file,
|
|
71
|
+
"line": f.line,
|
|
72
|
+
}
|
|
73
|
+
for f in findings
|
|
74
|
+
]
|