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,244 @@
|
|
|
1
|
+
"""Body-level conformance analysis for Odin (`cdec enforce`, Engine B).
|
|
2
|
+
|
|
3
|
+
Re-parses Odin source with tree-sitter and inspects procedure bodies for
|
|
4
|
+
violations of architectural-rule tags. Reuses `odin.rules_extract` so "what is a
|
|
5
|
+
rule" stays identical to the UML parser, and `core.receivers.resolve_owner` so a
|
|
6
|
+
procedure is attributed to the same struct the model shows.
|
|
7
|
+
|
|
8
|
+
Detection is precise rather than heuristic, because Odin's grammar distinguishes
|
|
9
|
+
the constructs outright:
|
|
10
|
+
|
|
11
|
+
* **Construction** is a composite literal — grammar node type `struct`, as in
|
|
12
|
+
`Money{amount = 1}` — plus `new(T)` / `make(T)` allocations. There is no need
|
|
13
|
+
to guess from a callee's capitalisation the way the Python analyzer must.
|
|
14
|
+
* **Field reassignment** is an `assignment_statement` whose target is a
|
|
15
|
+
`member_expression` rooted at the receiver parameter, so `inv.total = …` is
|
|
16
|
+
caught while an unrelated `other.total = …` is not.
|
|
17
|
+
|
|
18
|
+
Like the other analyzers this only reports; waiver filtering happens in the
|
|
19
|
+
caller.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
import tree_sitter_odin
|
|
27
|
+
from tree_sitter import Language, Node, Parser
|
|
28
|
+
|
|
29
|
+
from code_constraints.core.annotations import literal_set
|
|
30
|
+
from code_constraints.core.model import Project, RuleAnnotation
|
|
31
|
+
from code_constraints.core.receivers import resolve_owner
|
|
32
|
+
from code_constraints.enforce.model import Finding
|
|
33
|
+
from code_constraints.odin.parser import (
|
|
34
|
+
_TYPE_DECL_TYPES,
|
|
35
|
+
_package_name,
|
|
36
|
+
_parameters,
|
|
37
|
+
_receiver_name,
|
|
38
|
+
_should_skip,
|
|
39
|
+
_text,
|
|
40
|
+
)
|
|
41
|
+
from code_constraints.odin.rules_extract import extract_rules
|
|
42
|
+
|
|
43
|
+
_LANG = Language(tree_sitter_odin.language())
|
|
44
|
+
_PARSER = Parser(_LANG)
|
|
45
|
+
|
|
46
|
+
CTOR_RULE = "no-instantiation"
|
|
47
|
+
FACTORY_RULE = "factory"
|
|
48
|
+
IMMUTABLE_RULE = "immutable"
|
|
49
|
+
|
|
50
|
+
# Odin's built-in allocators; the type is their first argument.
|
|
51
|
+
_ALLOCATORS = frozenset({"new", "make", "new_clone"})
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def analyze(root: Path, project: Project) -> list[Finding]:
|
|
55
|
+
project_classes = {cls.name for cls in project.iter_classes()}
|
|
56
|
+
factory_index = _factory_index(project)
|
|
57
|
+
class_rules = {
|
|
58
|
+
cls.qualified_name: {r.name: r for r in cls.rules}
|
|
59
|
+
for cls in project.iter_classes()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
files: list[tuple[Path, bytes, Node, str]] = []
|
|
63
|
+
struct_index: dict[tuple[str, str], str] = {}
|
|
64
|
+
|
|
65
|
+
for odin_file in sorted(root.rglob("*.odin")):
|
|
66
|
+
if _should_skip(odin_file):
|
|
67
|
+
continue
|
|
68
|
+
try:
|
|
69
|
+
source = odin_file.read_bytes()
|
|
70
|
+
except OSError:
|
|
71
|
+
continue
|
|
72
|
+
tree = _PARSER.parse(source)
|
|
73
|
+
rel = odin_file.relative_to(root)
|
|
74
|
+
package_qn = _package_name(tree.root_node, source, rel)
|
|
75
|
+
files.append((rel, source, tree.root_node, package_qn))
|
|
76
|
+
for child in tree.root_node.named_children:
|
|
77
|
+
if child.type not in _TYPE_DECL_TYPES:
|
|
78
|
+
continue
|
|
79
|
+
name_node = next((c for c in child.children if c.type == "identifier"), None)
|
|
80
|
+
if name_node is None:
|
|
81
|
+
continue
|
|
82
|
+
name = _text(name_node, source)
|
|
83
|
+
struct_index[(package_qn, name)] = (
|
|
84
|
+
f"{package_qn}.{name}" if package_qn != "__root__" else name
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
findings: list[Finding] = []
|
|
88
|
+
for rel, source, root_node, package_qn in files:
|
|
89
|
+
for child in root_node.named_children:
|
|
90
|
+
if child.type != "procedure_declaration":
|
|
91
|
+
continue
|
|
92
|
+
_analyze_procedure(
|
|
93
|
+
child, source, package_qn, rel.as_posix(), struct_index,
|
|
94
|
+
class_rules, project_classes, factory_index, findings,
|
|
95
|
+
)
|
|
96
|
+
return findings
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _analyze_procedure(
|
|
100
|
+
decl: Node,
|
|
101
|
+
source: bytes,
|
|
102
|
+
package_qn: str,
|
|
103
|
+
file: str,
|
|
104
|
+
struct_index: dict[tuple[str, str], str],
|
|
105
|
+
class_rules: dict[str, dict[str, RuleAnnotation]],
|
|
106
|
+
project_classes: set[str],
|
|
107
|
+
factory_index: dict[str, set[str]],
|
|
108
|
+
out: list[Finding],
|
|
109
|
+
) -> None:
|
|
110
|
+
name_node = next((c for c in decl.children if c.type == "identifier"), None)
|
|
111
|
+
proc_node = next((c for c in decl.children if c.type == "procedure"), None)
|
|
112
|
+
if name_node is None or proc_node is None:
|
|
113
|
+
return
|
|
114
|
+
block = next((c for c in proc_node.children if c.type == "block"), None)
|
|
115
|
+
if block is None:
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
name = _text(name_node, source)
|
|
119
|
+
params = _parameters(proc_node, source)
|
|
120
|
+
owner_qn = resolve_owner(_receiver_name(params), package_qn, struct_index)
|
|
121
|
+
if owner_qn is None:
|
|
122
|
+
stem = Path(file).stem
|
|
123
|
+
owner_qn = f"{package_qn}.{stem}" if package_qn != "__root__" else stem
|
|
124
|
+
owner_name = stem
|
|
125
|
+
receiver = ""
|
|
126
|
+
else:
|
|
127
|
+
owner_name = owner_qn.rsplit(".", 1)[-1]
|
|
128
|
+
receiver = params[0].name if params else ""
|
|
129
|
+
|
|
130
|
+
proc_rules = {r.name: r for r in extract_rules(decl, source)}
|
|
131
|
+
owner_rules = class_rules.get(owner_qn, {})
|
|
132
|
+
|
|
133
|
+
noinst = proc_rules.get(CTOR_RULE, owner_rules.get(CTOR_RULE))
|
|
134
|
+
allow = literal_set(noinst.kwargs.get("allow", "")) if noinst is not None else None
|
|
135
|
+
|
|
136
|
+
for constructed, line in _constructions(block, source):
|
|
137
|
+
if constructed == owner_name:
|
|
138
|
+
# A type constructing itself is never a violation: it is how the
|
|
139
|
+
# `T.new(...)` / inner-constructor idiom is written, and neither
|
|
140
|
+
# `no_instantiation` nor `factory` is aimed at a type's own
|
|
141
|
+
# constructor — they guard construction by *other* code.
|
|
142
|
+
continue
|
|
143
|
+
if allow is not None and constructed not in allow:
|
|
144
|
+
out.append(
|
|
145
|
+
Finding(
|
|
146
|
+
rule=CTOR_RULE,
|
|
147
|
+
qualified_name=owner_qn,
|
|
148
|
+
message=(
|
|
149
|
+
f"'{owner_qn}.{name}' is tagged @cdec no_instantiation but "
|
|
150
|
+
f"constructs '{constructed}'."
|
|
151
|
+
),
|
|
152
|
+
detail=f"{name}->{constructed}",
|
|
153
|
+
file=file,
|
|
154
|
+
line=line,
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
designated = factory_index.get(constructed)
|
|
158
|
+
if designated is not None and owner_name not in designated:
|
|
159
|
+
allowed = ", ".join(sorted(designated)) or "(none)"
|
|
160
|
+
out.append(
|
|
161
|
+
Finding(
|
|
162
|
+
rule=FACTORY_RULE,
|
|
163
|
+
qualified_name=owner_qn,
|
|
164
|
+
message=(
|
|
165
|
+
f"'{owner_qn}.{name}' constructs '{constructed}' outside its "
|
|
166
|
+
f"designated factory ({allowed})."
|
|
167
|
+
),
|
|
168
|
+
detail=f"{name}->{constructed}",
|
|
169
|
+
file=file,
|
|
170
|
+
line=line,
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
if IMMUTABLE_RULE in owner_rules and receiver:
|
|
175
|
+
for field, line in _receiver_assignments(block, receiver, source):
|
|
176
|
+
out.append(
|
|
177
|
+
Finding(
|
|
178
|
+
rule=IMMUTABLE_RULE,
|
|
179
|
+
qualified_name=owner_qn,
|
|
180
|
+
message=(
|
|
181
|
+
f"'{owner_qn}' is tagged @cdec immutable but '{name}' "
|
|
182
|
+
f"reassigns field '{field}'."
|
|
183
|
+
),
|
|
184
|
+
detail=f"{name}.{field}",
|
|
185
|
+
file=file,
|
|
186
|
+
line=line,
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _constructions(block: Node, source: bytes) -> list[tuple[str, int]]:
|
|
192
|
+
"""Types constructed in a procedure body, with their line numbers."""
|
|
193
|
+
out: list[tuple[str, int]] = []
|
|
194
|
+
stack = [block]
|
|
195
|
+
while stack:
|
|
196
|
+
node = stack.pop()
|
|
197
|
+
stack.extend(node.children)
|
|
198
|
+
line = node.start_point[0] + 1
|
|
199
|
+
if node.type == "struct":
|
|
200
|
+
# Composite literal `Money{...}`: the type is the leading identifier.
|
|
201
|
+
ident = next((c for c in node.children if c.type == "identifier"), None)
|
|
202
|
+
if ident is not None:
|
|
203
|
+
out.append((_text(ident, source), line))
|
|
204
|
+
elif node.type == "call_expression":
|
|
205
|
+
callee = next((c for c in node.children if c.type == "identifier"), None)
|
|
206
|
+
if callee is None or _text(callee, source) not in _ALLOCATORS:
|
|
207
|
+
continue
|
|
208
|
+
args = next((c for c in node.children if c.type == "argument_list"), None)
|
|
209
|
+
first = args.named_children[0] if args and args.named_children else None
|
|
210
|
+
if first is not None:
|
|
211
|
+
out.append((_text(first, source).lstrip("^"), line))
|
|
212
|
+
return out
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _receiver_assignments(
|
|
216
|
+
block: Node, receiver: str, source: bytes
|
|
217
|
+
) -> list[tuple[str, int]]:
|
|
218
|
+
"""`recv.field = …` assignments inside the body."""
|
|
219
|
+
out: list[tuple[str, int]] = []
|
|
220
|
+
stack = [block]
|
|
221
|
+
while stack:
|
|
222
|
+
node = stack.pop()
|
|
223
|
+
stack.extend(node.children)
|
|
224
|
+
if node.type != "assignment_statement":
|
|
225
|
+
continue
|
|
226
|
+
target = node.named_children[0] if node.named_children else None
|
|
227
|
+
if target is None or target.type != "member_expression":
|
|
228
|
+
continue
|
|
229
|
+
parts = [c for c in target.children if c.type == "identifier"]
|
|
230
|
+
if len(parts) >= 2 and _text(parts[0], source) == receiver:
|
|
231
|
+
out.append((_text(parts[-1], source), node.start_point[0] + 1))
|
|
232
|
+
return out
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _factory_index(project: Project) -> dict[str, set[str]]:
|
|
236
|
+
"""Created-type name -> set of class names designated to construct it."""
|
|
237
|
+
idx: dict[str, set[str]] = {}
|
|
238
|
+
for cls in project.iter_classes():
|
|
239
|
+
for rule in list(cls.rules) + [r for op in cls.operations for r in op.rules]:
|
|
240
|
+
if rule.name != FACTORY_RULE:
|
|
241
|
+
continue
|
|
242
|
+
for created in literal_set(rule.kwargs.get("creates", "")):
|
|
243
|
+
idx.setdefault(created, set()).add(cls.name)
|
|
244
|
+
return idx
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""AST fingerprinting for Odin (`cdec lock`, Engine C).
|
|
2
|
+
|
|
3
|
+
Digests come from the tree-sitter tree via `core.ts_fingerprint`, so a locked
|
|
4
|
+
element survives reformatting and relocation within its file.
|
|
5
|
+
|
|
6
|
+
Odin-specific concerns:
|
|
7
|
+
|
|
8
|
+
* **Tags are comments**, so the `//@cdec locked` line is dropped from the digest
|
|
9
|
+
explicitly rather than incidentally — otherwise `include_docstrings` would let
|
|
10
|
+
applying a lock change the very digest it records.
|
|
11
|
+
* **Target names must match the UML model**, because a target an agent reads
|
|
12
|
+
from `cdec lock list` has to be the target `cdec lock set` accepts. Procedures
|
|
13
|
+
are therefore resolved to their receiver struct exactly as the parser does,
|
|
14
|
+
which needs the same two-phase walk (structs first, then procedures) and the
|
|
15
|
+
same helpers — imported from `odin.parser` rather than re-derived, so the two
|
|
16
|
+
can't drift apart.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
import tree_sitter_odin
|
|
24
|
+
from tree_sitter import Language, Node, Parser
|
|
25
|
+
|
|
26
|
+
from code_constraints.core.ts_fingerprint import digest_members
|
|
27
|
+
from code_constraints.lock.model import LOCK_RULE, LockTarget
|
|
28
|
+
from code_constraints.odin.parser import (
|
|
29
|
+
_TYPE_DECL_TYPES,
|
|
30
|
+
_package_name,
|
|
31
|
+
_parameters,
|
|
32
|
+
_receiver_name,
|
|
33
|
+
_should_skip,
|
|
34
|
+
_text,
|
|
35
|
+
)
|
|
36
|
+
from code_constraints.odin.rules_extract import comment_is_rule_tag, extract_rules
|
|
37
|
+
|
|
38
|
+
DIGEST_ALGO = "odin-ts/1"
|
|
39
|
+
|
|
40
|
+
_LANG = Language(tree_sitter_odin.language())
|
|
41
|
+
_PARSER = Parser(_LANG)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def collect_lockables(
|
|
45
|
+
root: str | Path, *, include_docstrings: bool = False
|
|
46
|
+
) -> list[LockTarget]:
|
|
47
|
+
root_path = Path(root).resolve()
|
|
48
|
+
files: list[tuple[Path, bytes, Node, str]] = []
|
|
49
|
+
# (package_qn, struct name) -> qualified name, for receiver resolution.
|
|
50
|
+
struct_index: dict[tuple[str, str], str] = {}
|
|
51
|
+
out: list[LockTarget] = []
|
|
52
|
+
|
|
53
|
+
# Phase 1: every type declaration, indexed for phase 2.
|
|
54
|
+
for odin_file in sorted(root_path.rglob("*.odin")):
|
|
55
|
+
if _should_skip(odin_file):
|
|
56
|
+
continue
|
|
57
|
+
try:
|
|
58
|
+
source = odin_file.read_bytes()
|
|
59
|
+
except OSError:
|
|
60
|
+
continue
|
|
61
|
+
tree = _PARSER.parse(source)
|
|
62
|
+
rel = odin_file.relative_to(root_path)
|
|
63
|
+
package_qn = _package_name(tree.root_node, source, rel)
|
|
64
|
+
files.append((rel, source, tree.root_node, package_qn))
|
|
65
|
+
|
|
66
|
+
for child in tree.root_node.named_children:
|
|
67
|
+
if child.type not in _TYPE_DECL_TYPES:
|
|
68
|
+
continue
|
|
69
|
+
name_node = next((c for c in child.children if c.type == "identifier"), None)
|
|
70
|
+
if name_node is None:
|
|
71
|
+
continue
|
|
72
|
+
name = _text(name_node, source)
|
|
73
|
+
qn = f"{package_qn}.{name}" if package_qn != "__root__" else name
|
|
74
|
+
struct_index[(package_qn, name)] = qn
|
|
75
|
+
out.append(
|
|
76
|
+
_target(
|
|
77
|
+
[(child, source)], qn, "class", rel.as_posix(), include_docstrings
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Phase 2: procedures, grouped by owner so same-named siblings share a target.
|
|
82
|
+
groups: dict[str, list[tuple[Node, bytes]]] = {}
|
|
83
|
+
locations: dict[str, str] = {}
|
|
84
|
+
for rel, source, root_node, package_qn in files:
|
|
85
|
+
for child in root_node.named_children:
|
|
86
|
+
if child.type != "procedure_declaration":
|
|
87
|
+
continue
|
|
88
|
+
name_node = next((c for c in child.children if c.type == "identifier"), None)
|
|
89
|
+
proc_node = next((c for c in child.children if c.type == "procedure"), None)
|
|
90
|
+
if name_node is None or proc_node is None:
|
|
91
|
+
continue
|
|
92
|
+
owner = _owner_qn(proc_node, source, package_qn, struct_index, rel)
|
|
93
|
+
target = f"{owner}.{_text(name_node, source)}"
|
|
94
|
+
groups.setdefault(target, []).append((child, source))
|
|
95
|
+
locations.setdefault(target, rel.as_posix())
|
|
96
|
+
|
|
97
|
+
for target, members in groups.items():
|
|
98
|
+
out.append(
|
|
99
|
+
_target(members, target, "method", locations[target], include_docstrings)
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
return out
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _owner_qn(
|
|
106
|
+
proc_node: Node,
|
|
107
|
+
source: bytes,
|
|
108
|
+
package_qn: str,
|
|
109
|
+
struct_index: dict[tuple[str, str], str],
|
|
110
|
+
rel: Path,
|
|
111
|
+
) -> str:
|
|
112
|
+
"""The receiver struct's qualified name, or the file's synthetic module class."""
|
|
113
|
+
params = _parameters(proc_node, source)
|
|
114
|
+
name = _receiver_name(params)
|
|
115
|
+
if name:
|
|
116
|
+
same_package = struct_index.get((package_qn, name))
|
|
117
|
+
if same_package is not None:
|
|
118
|
+
return same_package
|
|
119
|
+
matches = [qn for (_pkg, n), qn in struct_index.items() if n == name]
|
|
120
|
+
if len(matches) == 1:
|
|
121
|
+
return matches[0]
|
|
122
|
+
return f"{package_qn}.{rel.stem}" if package_qn != "__root__" else rel.stem
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _target(
|
|
126
|
+
members: list[tuple[Node, bytes]],
|
|
127
|
+
target: str,
|
|
128
|
+
kind: str,
|
|
129
|
+
file: str,
|
|
130
|
+
include_docstrings: bool,
|
|
131
|
+
) -> LockTarget:
|
|
132
|
+
def drop(node: Node, source: bytes) -> bool:
|
|
133
|
+
if node.type != "comment":
|
|
134
|
+
return False
|
|
135
|
+
# A rule tag is never part of the implementation, whatever the
|
|
136
|
+
# docstring setting: applying or removing `//@cdec locked` must leave
|
|
137
|
+
# the digest of the body it guards untouched.
|
|
138
|
+
if comment_is_rule_tag(node, source):
|
|
139
|
+
return True
|
|
140
|
+
return not include_docstrings
|
|
141
|
+
|
|
142
|
+
declared = False
|
|
143
|
+
params: dict[str, str] = {}
|
|
144
|
+
for node, source in members:
|
|
145
|
+
for rule in extract_rules(node, source):
|
|
146
|
+
if rule.name == LOCK_RULE:
|
|
147
|
+
declared = True
|
|
148
|
+
params = {**rule.kwargs, **params} if params else dict(rule.kwargs)
|
|
149
|
+
|
|
150
|
+
return LockTarget(
|
|
151
|
+
target=target,
|
|
152
|
+
kind=kind, # type: ignore[arg-type]
|
|
153
|
+
digest=digest_members(members, drop),
|
|
154
|
+
algo=DIGEST_ALGO,
|
|
155
|
+
file=file,
|
|
156
|
+
line=members[0][0].start_point[0] + 1,
|
|
157
|
+
declared=declared,
|
|
158
|
+
params=params,
|
|
159
|
+
)
|