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,590 @@
|
|
|
1
|
+
"""Parse a TypeScript project (a directory of .ts / .tsx files) into a
|
|
2
|
+
`code_constraints.core.Project`.
|
|
3
|
+
|
|
4
|
+
Uses `tree_sitter` with the `tree_sitter_typescript` grammar. Like the C# parser
|
|
5
|
+
this is a syntactic parse — there is no type-resolution across files, so an
|
|
6
|
+
inheritance / type reference that goes through an aliased import will appear as
|
|
7
|
+
the local alias rather than the source's qualified name.
|
|
8
|
+
|
|
9
|
+
Packages are derived from the directory layout (mirrors the Python parser);
|
|
10
|
+
file-level `namespace X { ... }` (TypeScript's `internal_module`) further nests
|
|
11
|
+
classes inside the directory-derived package.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
import tree_sitter_typescript
|
|
19
|
+
from tree_sitter import Language, Node, Parser
|
|
20
|
+
|
|
21
|
+
from code_constraints.core.model import (
|
|
22
|
+
Attribute,
|
|
23
|
+
Class,
|
|
24
|
+
ClassKind,
|
|
25
|
+
Operation,
|
|
26
|
+
Package,
|
|
27
|
+
Parameter,
|
|
28
|
+
Project,
|
|
29
|
+
SourceLocation,
|
|
30
|
+
Visibility,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
_LANG_TS = Language(tree_sitter_typescript.language_typescript())
|
|
34
|
+
_LANG_TSX = Language(tree_sitter_typescript.language_tsx())
|
|
35
|
+
_PARSER_TS = Parser(_LANG_TS)
|
|
36
|
+
_PARSER_TSX = Parser(_LANG_TSX)
|
|
37
|
+
|
|
38
|
+
_SKIP_DIR_NAMES = {
|
|
39
|
+
"node_modules", ".git", "dist", "build", "out", ".next", ".svelte-kit",
|
|
40
|
+
".turbo", ".cache", "coverage",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
_TS_SUFFIXES = (".ts", ".tsx", ".mts", ".cts")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def parse_project(root: str | Path) -> Project:
|
|
47
|
+
root_path = Path(root).resolve()
|
|
48
|
+
if not root_path.is_dir():
|
|
49
|
+
raise ValueError(f"not a directory: {root_path}")
|
|
50
|
+
|
|
51
|
+
project = Project(source_language="typescript", root_path=str(root_path))
|
|
52
|
+
package_index: dict[str, Package] = {}
|
|
53
|
+
|
|
54
|
+
for ts_file in sorted(_iter_ts_files(root_path)):
|
|
55
|
+
_parse_file(ts_file, root_path, project, package_index)
|
|
56
|
+
|
|
57
|
+
return project
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _iter_ts_files(root_path: Path):
|
|
61
|
+
for suf in _TS_SUFFIXES:
|
|
62
|
+
for f in root_path.rglob(f"*{suf}"):
|
|
63
|
+
if _should_skip(f):
|
|
64
|
+
continue
|
|
65
|
+
# Skip declaration files: they restate types defined elsewhere and
|
|
66
|
+
# would double-count classes when parsed alongside their .ts pair.
|
|
67
|
+
if f.name.endswith(".d.ts"):
|
|
68
|
+
continue
|
|
69
|
+
yield f
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _should_skip(p: Path) -> bool:
|
|
73
|
+
return any(part in _SKIP_DIR_NAMES for part in p.parts)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _parse_file(
|
|
77
|
+
file: Path,
|
|
78
|
+
root: Path,
|
|
79
|
+
project: Project,
|
|
80
|
+
package_index: dict[str, Package],
|
|
81
|
+
) -> None:
|
|
82
|
+
source_bytes = file.read_bytes()
|
|
83
|
+
parser = _PARSER_TSX if file.suffix == ".tsx" else _PARSER_TS
|
|
84
|
+
tree = parser.parse(source_bytes)
|
|
85
|
+
rel = file.relative_to(root)
|
|
86
|
+
file_str = rel.as_posix()
|
|
87
|
+
package_qn = _qualified_package_name(rel)
|
|
88
|
+
_ensure_package(project, package_qn, package_index)
|
|
89
|
+
|
|
90
|
+
parse_module_body(
|
|
91
|
+
tree.root_node,
|
|
92
|
+
source_bytes,
|
|
93
|
+
package_qn=package_qn,
|
|
94
|
+
project=project,
|
|
95
|
+
package_index=package_index,
|
|
96
|
+
file_str=file_str,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def parse_module_body(
|
|
101
|
+
root: Node,
|
|
102
|
+
source: bytes,
|
|
103
|
+
*,
|
|
104
|
+
package_qn: str,
|
|
105
|
+
project: Project,
|
|
106
|
+
package_index: dict[str, Package],
|
|
107
|
+
file_str: str,
|
|
108
|
+
) -> None:
|
|
109
|
+
"""Walk the top-level of a parsed TS source and ingest its declarations.
|
|
110
|
+
|
|
111
|
+
Factored out of `_parse_file` so the Svelte parser can run it on the
|
|
112
|
+
contents of a `<script>` block (which parse as a `program` with the same
|
|
113
|
+
grammar) without re-implementing the dispatch logic.
|
|
114
|
+
"""
|
|
115
|
+
for child in root.named_children:
|
|
116
|
+
_visit_top_level(child, package_qn, project, package_index, source, file_str)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
_CLASS_LIKE_TYPES = {
|
|
120
|
+
"class_declaration",
|
|
121
|
+
"abstract_class_declaration",
|
|
122
|
+
"interface_declaration",
|
|
123
|
+
"enum_declaration",
|
|
124
|
+
"type_alias_declaration",
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _visit_top_level(
|
|
129
|
+
node: Node,
|
|
130
|
+
package_qn: str,
|
|
131
|
+
project: Project,
|
|
132
|
+
package_index: dict[str, Package],
|
|
133
|
+
source: bytes,
|
|
134
|
+
file_str: str,
|
|
135
|
+
) -> None:
|
|
136
|
+
# Unwrap `export class ...` / `export default class ...`.
|
|
137
|
+
if node.type == "export_statement":
|
|
138
|
+
for c in node.named_children:
|
|
139
|
+
_visit_top_level(c, package_qn, project, package_index, source, file_str)
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
# `expression_statement` shows up around `namespace X { ... }`.
|
|
143
|
+
if node.type == "expression_statement" and node.named_child_count == 1:
|
|
144
|
+
_visit_top_level(
|
|
145
|
+
node.named_children[0], package_qn, project, package_index, source, file_str
|
|
146
|
+
)
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
if node.type == "internal_module":
|
|
150
|
+
_visit_namespace(node, package_qn, project, package_index, source, file_str)
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
if node.type in _CLASS_LIKE_TYPES:
|
|
154
|
+
_ingest_class(node, package_qn, project, package_index, source, file_str)
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _visit_namespace(
|
|
159
|
+
node: Node,
|
|
160
|
+
parent_qn: str,
|
|
161
|
+
project: Project,
|
|
162
|
+
package_index: dict[str, Package],
|
|
163
|
+
source: bytes,
|
|
164
|
+
file_str: str,
|
|
165
|
+
) -> None:
|
|
166
|
+
# The name child is either `identifier` or `nested_identifier`
|
|
167
|
+
# (`namespace Foo.Bar { ... }`).
|
|
168
|
+
name_node = next(
|
|
169
|
+
(c for c in node.named_children
|
|
170
|
+
if c.type in ("identifier", "nested_identifier")),
|
|
171
|
+
None,
|
|
172
|
+
)
|
|
173
|
+
name = _text(name_node, source) if name_node else "anon"
|
|
174
|
+
qn = name if parent_qn in ("", "__root__") else f"{parent_qn}.{name}"
|
|
175
|
+
_ensure_package(project, qn, package_index)
|
|
176
|
+
|
|
177
|
+
body = next(
|
|
178
|
+
(c for c in node.named_children if c.type == "statement_block"), None
|
|
179
|
+
)
|
|
180
|
+
if body is None:
|
|
181
|
+
return
|
|
182
|
+
for child in body.named_children:
|
|
183
|
+
_visit_top_level(child, qn, project, package_index, source, file_str)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _ensure_package(
|
|
187
|
+
project: Project, qualified_name: str, index: dict[str, Package]
|
|
188
|
+
) -> Package:
|
|
189
|
+
if qualified_name in index:
|
|
190
|
+
return index[qualified_name]
|
|
191
|
+
if qualified_name == "__root__":
|
|
192
|
+
pkg = Package(name="__root__", qualified_name="__root__")
|
|
193
|
+
project.packages.append(pkg)
|
|
194
|
+
index[qualified_name] = pkg
|
|
195
|
+
return pkg
|
|
196
|
+
parts = qualified_name.split(".")
|
|
197
|
+
if len(parts) == 1:
|
|
198
|
+
pkg = Package(name=parts[0], qualified_name=qualified_name)
|
|
199
|
+
project.packages.append(pkg)
|
|
200
|
+
else:
|
|
201
|
+
parent = _ensure_package(project, ".".join(parts[:-1]), index)
|
|
202
|
+
pkg = Package(name=parts[-1], qualified_name=qualified_name)
|
|
203
|
+
parent.sub_packages.append(pkg)
|
|
204
|
+
index[qualified_name] = pkg
|
|
205
|
+
return pkg
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _qualified_package_name(rel_file: Path) -> str:
|
|
209
|
+
parts = list(rel_file.parts[:-1])
|
|
210
|
+
if not parts:
|
|
211
|
+
return "__root__"
|
|
212
|
+
return ".".join(parts)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# ---------- class / member extraction ----------
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _ingest_class(
|
|
219
|
+
node: Node,
|
|
220
|
+
package_qn: str,
|
|
221
|
+
project: Project,
|
|
222
|
+
package_index: dict[str, Package],
|
|
223
|
+
source: bytes,
|
|
224
|
+
file_str: str,
|
|
225
|
+
) -> None:
|
|
226
|
+
name_node = node.child_by_field_name("name") or _first_named_of_type(
|
|
227
|
+
node, "type_identifier"
|
|
228
|
+
)
|
|
229
|
+
if name_node is None:
|
|
230
|
+
return
|
|
231
|
+
name = _text(name_node, source)
|
|
232
|
+
qn = (
|
|
233
|
+
f"{package_qn}.{name}"
|
|
234
|
+
if package_qn and package_qn != "__root__"
|
|
235
|
+
else name
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
cls = Class(
|
|
239
|
+
name=name,
|
|
240
|
+
qualified_name=qn,
|
|
241
|
+
kind=_kind_for(node, source),
|
|
242
|
+
bases=_bases(node, source),
|
|
243
|
+
location=SourceLocation(
|
|
244
|
+
file=file_str,
|
|
245
|
+
start_line=node.start_point[0] + 1,
|
|
246
|
+
end_line=node.end_point[0] + 1,
|
|
247
|
+
),
|
|
248
|
+
description=_jsdoc_for(node, source),
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
if node.type == "type_alias_declaration":
|
|
252
|
+
# A type alias has no members; the `value` child is the aliased type
|
|
253
|
+
# expression. We surface the alias text as a synthetic attribute so
|
|
254
|
+
# the viewer shows what the alias resolves to.
|
|
255
|
+
value = node.child_by_field_name("value")
|
|
256
|
+
if value is not None:
|
|
257
|
+
cls.attributes.append(
|
|
258
|
+
Attribute(name="_alias", type=_text(value, source))
|
|
259
|
+
)
|
|
260
|
+
else:
|
|
261
|
+
body = _class_body(node)
|
|
262
|
+
if body is not None:
|
|
263
|
+
for member in body.named_children:
|
|
264
|
+
_ingest_member(member, cls, source)
|
|
265
|
+
|
|
266
|
+
pkg = _ensure_package(project, package_qn, package_index)
|
|
267
|
+
pkg.classes.append(cls)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _class_body(node: Node) -> Node | None:
|
|
271
|
+
for c in node.named_children:
|
|
272
|
+
if c.type in ("class_body", "interface_body", "enum_body", "object_type"):
|
|
273
|
+
return c
|
|
274
|
+
return node.child_by_field_name("body")
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _kind_for(node: Node, source: bytes) -> ClassKind:
|
|
278
|
+
if node.type == "interface_declaration":
|
|
279
|
+
return "interface"
|
|
280
|
+
if node.type == "enum_declaration":
|
|
281
|
+
return "enum"
|
|
282
|
+
if node.type == "type_alias_declaration":
|
|
283
|
+
return "interface" # closest UML equivalent — a named type contract
|
|
284
|
+
if node.type == "abstract_class_declaration":
|
|
285
|
+
return "abstract"
|
|
286
|
+
return "class"
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _bases(node: Node, source: bytes) -> list[str]:
|
|
290
|
+
"""Collect `extends` + `implements` targets.
|
|
291
|
+
|
|
292
|
+
Class heritage on `class_declaration` / `abstract_class_declaration` lives
|
|
293
|
+
inside a `class_heritage` child holding `extends_clause` and/or
|
|
294
|
+
`implements_clause`. Interfaces use an `extends_type_clause` directly on the
|
|
295
|
+
`interface_declaration`.
|
|
296
|
+
"""
|
|
297
|
+
out: list[str] = []
|
|
298
|
+
for child in node.named_children:
|
|
299
|
+
if child.type == "class_heritage":
|
|
300
|
+
for sub in child.named_children:
|
|
301
|
+
if sub.type == "extends_clause":
|
|
302
|
+
out.extend(_clause_types(sub, source))
|
|
303
|
+
elif sub.type == "implements_clause":
|
|
304
|
+
out.extend(_clause_types(sub, source))
|
|
305
|
+
elif child.type in ("extends_clause", "extends_type_clause", "implements_clause"):
|
|
306
|
+
out.extend(_clause_types(child, source))
|
|
307
|
+
return out
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _clause_types(clause: Node, source: bytes) -> list[str]:
|
|
311
|
+
types: list[str] = []
|
|
312
|
+
for c in clause.named_children:
|
|
313
|
+
# Skip the `extends` / `implements` keyword token (unnamed).
|
|
314
|
+
text = _text(c, source).strip()
|
|
315
|
+
if text:
|
|
316
|
+
types.append(text)
|
|
317
|
+
return types
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _ingest_member(node: Node, cls: Class, source: bytes) -> None:
|
|
321
|
+
t = node.type
|
|
322
|
+
if t == "public_field_definition":
|
|
323
|
+
cls.attributes.append(_attribute_from_field(node, source))
|
|
324
|
+
elif t == "property_signature":
|
|
325
|
+
cls.attributes.append(_attribute_from_property_sig(node, source))
|
|
326
|
+
elif t in ("method_definition", "method_signature", "abstract_method_signature"):
|
|
327
|
+
op = _operation_from_method(node, source)
|
|
328
|
+
if op.name == "constructor":
|
|
329
|
+
# TS allows `constructor(public name: string)` parameter properties.
|
|
330
|
+
for p in op.parameters:
|
|
331
|
+
# A parameter property is a constructor parameter that carried
|
|
332
|
+
# an `accessibility_modifier` or `readonly` — encoded in the
|
|
333
|
+
# parsed parameter via the leading marker in its name. We
|
|
334
|
+
# capture this directly from the tree below.
|
|
335
|
+
pass
|
|
336
|
+
# Re-walk the formal parameters for parameter properties.
|
|
337
|
+
params_node = node.child_by_field_name("parameters")
|
|
338
|
+
if params_node is not None:
|
|
339
|
+
for p in params_node.named_children:
|
|
340
|
+
attr = _attribute_from_parameter_property(p, source)
|
|
341
|
+
if attr is not None:
|
|
342
|
+
cls.attributes.append(attr)
|
|
343
|
+
cls.operations.append(op)
|
|
344
|
+
elif t == "enum_assignment" or t == "property_identifier":
|
|
345
|
+
# Enum body entries are bare `property_identifier`s (no value) or
|
|
346
|
+
# `enum_assignment` (with an initializer).
|
|
347
|
+
if t == "property_identifier":
|
|
348
|
+
cls.attributes.append(
|
|
349
|
+
Attribute(
|
|
350
|
+
name=_text(node, source),
|
|
351
|
+
type=cls.name,
|
|
352
|
+
visibility=Visibility.PUBLIC,
|
|
353
|
+
is_static=True,
|
|
354
|
+
)
|
|
355
|
+
)
|
|
356
|
+
else:
|
|
357
|
+
name_node = node.child_by_field_name("name")
|
|
358
|
+
value_node = node.child_by_field_name("value")
|
|
359
|
+
if name_node is not None:
|
|
360
|
+
cls.attributes.append(
|
|
361
|
+
Attribute(
|
|
362
|
+
name=_text(name_node, source),
|
|
363
|
+
type=cls.name,
|
|
364
|
+
visibility=Visibility.PUBLIC,
|
|
365
|
+
is_static=True,
|
|
366
|
+
default=_text(value_node, source) if value_node else None,
|
|
367
|
+
)
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _attribute_from_field(node: Node, source: bytes) -> Attribute:
|
|
372
|
+
name_node = node.child_by_field_name("name") or _first_named_of_type(
|
|
373
|
+
node, "property_identifier"
|
|
374
|
+
)
|
|
375
|
+
name = _text(name_node, source) if name_node else ""
|
|
376
|
+
type_node = _first_named_of_type(node, "type_annotation")
|
|
377
|
+
type_text = _strip_type_annotation(_text(type_node, source)) if type_node else ""
|
|
378
|
+
value_node = node.child_by_field_name("value")
|
|
379
|
+
default = _text(value_node, source) if value_node else None
|
|
380
|
+
|
|
381
|
+
vis = _visibility_for_member(node, name, source)
|
|
382
|
+
is_static = _has_keyword(node, "static")
|
|
383
|
+
is_readonly = _has_keyword(node, "readonly")
|
|
384
|
+
return Attribute(
|
|
385
|
+
name=name,
|
|
386
|
+
type=type_text,
|
|
387
|
+
visibility=vis,
|
|
388
|
+
is_static=is_static,
|
|
389
|
+
is_readonly=is_readonly,
|
|
390
|
+
default=default,
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _attribute_from_property_sig(node: Node, source: bytes) -> Attribute:
|
|
395
|
+
name_node = node.child_by_field_name("name") or _first_named_of_type(
|
|
396
|
+
node, "property_identifier"
|
|
397
|
+
)
|
|
398
|
+
name = _text(name_node, source) if name_node else ""
|
|
399
|
+
type_node = _first_named_of_type(node, "type_annotation")
|
|
400
|
+
type_text = _strip_type_annotation(_text(type_node, source)) if type_node else ""
|
|
401
|
+
return Attribute(
|
|
402
|
+
name=name,
|
|
403
|
+
type=type_text,
|
|
404
|
+
visibility=_visibility_for_member(node, name, source),
|
|
405
|
+
is_readonly=_has_keyword(node, "readonly"),
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _attribute_from_parameter_property(param: Node, source: bytes) -> Attribute | None:
|
|
410
|
+
"""Capture `constructor(public name: string)` parameter properties.
|
|
411
|
+
|
|
412
|
+
Such parameters carry an `accessibility_modifier` or `readonly` keyword;
|
|
413
|
+
everything else is a plain parameter we should NOT mirror as an attribute.
|
|
414
|
+
"""
|
|
415
|
+
has_marker = False
|
|
416
|
+
for c in param.children:
|
|
417
|
+
if c.type == "accessibility_modifier":
|
|
418
|
+
has_marker = True
|
|
419
|
+
if c.type == "readonly":
|
|
420
|
+
has_marker = True
|
|
421
|
+
if not has_marker:
|
|
422
|
+
return None
|
|
423
|
+
if param.type not in ("required_parameter", "optional_parameter"):
|
|
424
|
+
return None
|
|
425
|
+
name_node = next(
|
|
426
|
+
(c for c in param.named_children if c.type in ("identifier", "shorthand_property_identifier_pattern")),
|
|
427
|
+
None,
|
|
428
|
+
)
|
|
429
|
+
name = _text(name_node, source) if name_node else ""
|
|
430
|
+
type_node = _first_named_of_type(param, "type_annotation")
|
|
431
|
+
type_text = _strip_type_annotation(_text(type_node, source)) if type_node else ""
|
|
432
|
+
return Attribute(
|
|
433
|
+
name=name,
|
|
434
|
+
type=type_text,
|
|
435
|
+
visibility=_visibility_for_member(param, name, source),
|
|
436
|
+
is_readonly=_has_keyword(param, "readonly"),
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _operation_from_method(node: Node, source: bytes) -> Operation:
|
|
441
|
+
name_node = node.child_by_field_name("name") or _first_named_of_type(
|
|
442
|
+
node, "property_identifier"
|
|
443
|
+
)
|
|
444
|
+
name = _text(name_node, source) if name_node else ""
|
|
445
|
+
return_node = _first_named_of_type(node, "type_annotation")
|
|
446
|
+
return_type = (
|
|
447
|
+
_strip_type_annotation(_text(return_node, source)) if return_node else ""
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
params: list[Parameter] = []
|
|
451
|
+
params_node = node.child_by_field_name("parameters")
|
|
452
|
+
if params_node is not None:
|
|
453
|
+
for p in params_node.named_children:
|
|
454
|
+
if p.type not in ("required_parameter", "optional_parameter"):
|
|
455
|
+
continue
|
|
456
|
+
params.append(_parameter_from(p, source))
|
|
457
|
+
|
|
458
|
+
return Operation(
|
|
459
|
+
name=name,
|
|
460
|
+
parameters=params,
|
|
461
|
+
return_type=return_type,
|
|
462
|
+
visibility=_visibility_for_member(node, name, source),
|
|
463
|
+
is_static=_has_keyword(node, "static"),
|
|
464
|
+
is_abstract=node.type == "abstract_method_signature" or _has_keyword(node, "abstract"),
|
|
465
|
+
description=_jsdoc_for(node, source),
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _parameter_from(p: Node, source: bytes) -> Parameter:
|
|
470
|
+
name_node = next(
|
|
471
|
+
(c for c in p.named_children
|
|
472
|
+
if c.type in ("identifier", "shorthand_property_identifier_pattern",
|
|
473
|
+
"object_pattern", "array_pattern")),
|
|
474
|
+
None,
|
|
475
|
+
)
|
|
476
|
+
name = _text(name_node, source) if name_node else ""
|
|
477
|
+
type_node = _first_named_of_type(p, "type_annotation")
|
|
478
|
+
type_text = _strip_type_annotation(_text(type_node, source)) if type_node else ""
|
|
479
|
+
# Default value is the named child after the type annotation.
|
|
480
|
+
default = None
|
|
481
|
+
saw_type = type_node is None
|
|
482
|
+
for c in p.named_children:
|
|
483
|
+
if c is type_node:
|
|
484
|
+
saw_type = True
|
|
485
|
+
continue
|
|
486
|
+
if c is name_node:
|
|
487
|
+
continue
|
|
488
|
+
if saw_type and c.type not in ("type_annotation", "accessibility_modifier"):
|
|
489
|
+
default = _text(c, source)
|
|
490
|
+
break
|
|
491
|
+
return Parameter(name=name, type=type_text, default=default)
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
# ---------- modifier helpers ----------
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def _visibility_for_member(node: Node, name: str, source: bytes) -> Visibility:
|
|
498
|
+
for c in node.children:
|
|
499
|
+
if c.type == "accessibility_modifier":
|
|
500
|
+
text = _text(c, source).strip()
|
|
501
|
+
if text == "private":
|
|
502
|
+
return Visibility.PRIVATE
|
|
503
|
+
if text == "protected":
|
|
504
|
+
return Visibility.PROTECTED
|
|
505
|
+
if text == "public":
|
|
506
|
+
return Visibility.PUBLIC
|
|
507
|
+
# TS convention: leading `#` is a private field; leading `_` is "by
|
|
508
|
+
# convention" protected.
|
|
509
|
+
if name.startswith("#"):
|
|
510
|
+
return Visibility.PRIVATE
|
|
511
|
+
if name.startswith("_"):
|
|
512
|
+
return Visibility.PROTECTED
|
|
513
|
+
return Visibility.PUBLIC
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def _has_keyword(node: Node, keyword: str) -> bool:
|
|
517
|
+
"""True if `node` has a direct (unnamed or named) child whose token text
|
|
518
|
+
matches `keyword`."""
|
|
519
|
+
for c in node.children:
|
|
520
|
+
if c.type == keyword:
|
|
521
|
+
return True
|
|
522
|
+
return False
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
# ---------- text helpers ----------
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def _text(node: Node | None, source: bytes) -> str:
|
|
529
|
+
if node is None:
|
|
530
|
+
return ""
|
|
531
|
+
return source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _strip_type_annotation(text: str) -> str:
|
|
535
|
+
"""Turn `: Foo<Bar>` into `Foo<Bar>` and trim whitespace."""
|
|
536
|
+
text = text.strip()
|
|
537
|
+
if text.startswith(":"):
|
|
538
|
+
text = text[1:].strip()
|
|
539
|
+
return text
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def _first_named_of_type(node: Node, type_name: str) -> Node | None:
|
|
543
|
+
for c in node.named_children:
|
|
544
|
+
if c.type == type_name:
|
|
545
|
+
return c
|
|
546
|
+
return None
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def _jsdoc_for(node: Node, source: bytes) -> str | None:
|
|
550
|
+
"""Read a `/** ... */` JSDoc block immediately above `node`.
|
|
551
|
+
|
|
552
|
+
Class / interface declarations are commonly wrapped in an
|
|
553
|
+
`export_statement` whose own `prev_sibling` is the comment we want, so we
|
|
554
|
+
step up to the export wrapper when the immediate previous sibling is the
|
|
555
|
+
`export` keyword.
|
|
556
|
+
"""
|
|
557
|
+
start = node
|
|
558
|
+
if (
|
|
559
|
+
node.parent is not None
|
|
560
|
+
and node.parent.type == "export_statement"
|
|
561
|
+
and node.prev_sibling is not None
|
|
562
|
+
and node.prev_sibling.type == "export"
|
|
563
|
+
):
|
|
564
|
+
start = node.parent
|
|
565
|
+
|
|
566
|
+
sib = start.prev_sibling
|
|
567
|
+
# Skip past unnamed token siblings (semicolons, keywords) to find the
|
|
568
|
+
# nearest comment OR named declaration. We only accept the comment if we
|
|
569
|
+
# encounter it before any other declaration.
|
|
570
|
+
while sib is not None and sib.type != "comment" and not sib.is_named:
|
|
571
|
+
sib = sib.prev_sibling
|
|
572
|
+
if sib is None or sib.type != "comment":
|
|
573
|
+
return None
|
|
574
|
+
raw = _text(sib, source).strip()
|
|
575
|
+
if not raw.startswith("/**"):
|
|
576
|
+
return None
|
|
577
|
+
# Strip /** ... */ and per-line " * " prefixes.
|
|
578
|
+
body = raw[3:]
|
|
579
|
+
if body.endswith("*/"):
|
|
580
|
+
body = body[:-2]
|
|
581
|
+
lines = []
|
|
582
|
+
for line in body.splitlines():
|
|
583
|
+
stripped = line.strip()
|
|
584
|
+
if stripped.startswith("*"):
|
|
585
|
+
stripped = stripped[1:].strip()
|
|
586
|
+
if stripped.startswith("@"):
|
|
587
|
+
break # stop at first @param/@returns tag
|
|
588
|
+
lines.append(stripped)
|
|
589
|
+
joined = " ".join(s for s in lines if s)
|
|
590
|
+
return joined or None
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Waivers — the review loop that lets an enforced codebase keep evolving.
|
|
2
|
+
|
|
3
|
+
Rules that only ever say "no" get switched off. The point of this package is the
|
|
4
|
+
other half of the loop: a violation is reported with a stable key, a human or an
|
|
5
|
+
agent decides it is acceptable, and that decision is recorded in
|
|
6
|
+
the `exceptions:` section of `.cdec/rules.yaml` with a reason — sitting next to
|
|
7
|
+
the rule it exempts, reviewable in the diff, and revocable.
|
|
8
|
+
|
|
9
|
+
cdec check # every issue prints its key
|
|
10
|
+
cdec exceptions review --out r.txt # one line per issue, ready to mark
|
|
11
|
+
…mark lines [ALLOW]…
|
|
12
|
+
cdec exceptions patch --file r.txt # apply exactly those decisions
|
|
13
|
+
cdec exceptions allow V-1A2B3C4D # or name one directly
|
|
14
|
+
|
|
15
|
+
Locks (Engine C) are pointedly excluded — see `model.NotWaivable`.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from code_constraints.core.keys import (
|
|
19
|
+
KEY_RE,
|
|
20
|
+
engine_of,
|
|
21
|
+
find_keys,
|
|
22
|
+
is_key,
|
|
23
|
+
make_key,
|
|
24
|
+
normalize_key,
|
|
25
|
+
)
|
|
26
|
+
from code_constraints.waivers.collect import CollectOptions, Collected, collect_issues
|
|
27
|
+
from code_constraints.waivers.model import Issue, NotWaivable
|
|
28
|
+
from code_constraints.waivers.ops import (
|
|
29
|
+
ApplyResult,
|
|
30
|
+
allow_keys,
|
|
31
|
+
apply_decisions,
|
|
32
|
+
prune,
|
|
33
|
+
remove_keys,
|
|
34
|
+
)
|
|
35
|
+
from code_constraints.waivers.review import (
|
|
36
|
+
Decision,
|
|
37
|
+
ReviewDecisions,
|
|
38
|
+
parse_review,
|
|
39
|
+
render_issue_line,
|
|
40
|
+
render_review,
|
|
41
|
+
)
|
|
42
|
+
from code_constraints.waivers.store import (
|
|
43
|
+
BASELINE_FILENAME,
|
|
44
|
+
EXCEPTIONS_SECTION,
|
|
45
|
+
WAIVABLE_ENGINES,
|
|
46
|
+
Waiver,
|
|
47
|
+
WaiverFileError,
|
|
48
|
+
WaiverStore,
|
|
49
|
+
default_actor,
|
|
50
|
+
ledger_paths,
|
|
51
|
+
load_waivers,
|
|
52
|
+
now_stamp,
|
|
53
|
+
save_waivers,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
__all__ = [
|
|
57
|
+
"ApplyResult",
|
|
58
|
+
"BASELINE_FILENAME",
|
|
59
|
+
"EXCEPTIONS_SECTION",
|
|
60
|
+
"CollectOptions",
|
|
61
|
+
"Collected",
|
|
62
|
+
"Decision",
|
|
63
|
+
"Issue",
|
|
64
|
+
"KEY_RE",
|
|
65
|
+
"NotWaivable",
|
|
66
|
+
"ReviewDecisions",
|
|
67
|
+
"WAIVABLE_ENGINES",
|
|
68
|
+
"Waiver",
|
|
69
|
+
"WaiverFileError",
|
|
70
|
+
"WaiverStore",
|
|
71
|
+
"allow_keys",
|
|
72
|
+
"apply_decisions",
|
|
73
|
+
"collect_issues",
|
|
74
|
+
"default_actor",
|
|
75
|
+
"engine_of",
|
|
76
|
+
"find_keys",
|
|
77
|
+
"is_key",
|
|
78
|
+
"ledger_paths",
|
|
79
|
+
"load_waivers",
|
|
80
|
+
"make_key",
|
|
81
|
+
"normalize_key",
|
|
82
|
+
"now_stamp",
|
|
83
|
+
"parse_review",
|
|
84
|
+
"prune",
|
|
85
|
+
"remove_keys",
|
|
86
|
+
"render_issue_line",
|
|
87
|
+
"render_review",
|
|
88
|
+
"save_waivers",
|
|
89
|
+
]
|