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.
Files changed (116) hide show
  1. code_constraints/__init__.py +1 -0
  2. code_constraints/cli/__init__.py +0 -0
  3. code_constraints/cli/__main__.py +1555 -0
  4. code_constraints/cli/_assets/agents/cdec-architect.md +468 -0
  5. code_constraints/cli/_assets/agents/oop-refactor-architect.md +317 -0
  6. code_constraints/cli/_assets/shims/csharp/CodeConstraintsRules.cs +94 -0
  7. code_constraints/cli/_assets/shims/julia/CdecRules.jl +129 -0
  8. code_constraints/cli/_assets/shims/lua/cdec_rules.lua +92 -0
  9. code_constraints/cli/_assets/shims/odin/cdec_rules.odin +67 -0
  10. code_constraints/cli/_assets/shims/python/cdec_rules.py +94 -0
  11. code_constraints/cli/_assets/skills/cdec-architecture-loop/SKILL.md +152 -0
  12. code_constraints/cli/depstamp.py +118 -0
  13. code_constraints/cli/detect.py +77 -0
  14. code_constraints/cli/interactive.py +304 -0
  15. code_constraints/cli/scaffold.py +602 -0
  16. code_constraints/cli/update.py +157 -0
  17. code_constraints/core/__init__.py +41 -0
  18. code_constraints/core/annotations.py +217 -0
  19. code_constraints/core/associations.py +134 -0
  20. code_constraints/core/diff.py +302 -0
  21. code_constraints/core/editor_io.py +280 -0
  22. code_constraints/core/graph_model.py +681 -0
  23. code_constraints/core/keys.py +105 -0
  24. code_constraints/core/model.py +294 -0
  25. code_constraints/core/model_io.py +65 -0
  26. code_constraints/core/receivers.py +34 -0
  27. code_constraints/core/rules.py +177 -0
  28. code_constraints/core/rulesdoc.py +208 -0
  29. code_constraints/core/tags.py +114 -0
  30. code_constraints/core/ts_fingerprint.py +88 -0
  31. code_constraints/core/xmi_reader.py +358 -0
  32. code_constraints/core/xmi_writer.py +373 -0
  33. code_constraints/csharp/__init__.py +3 -0
  34. code_constraints/csharp/activity.py +250 -0
  35. code_constraints/csharp/conformance.py +331 -0
  36. code_constraints/csharp/fingerprint.py +274 -0
  37. code_constraints/csharp/parser.py +436 -0
  38. code_constraints/csharp/rules_extract.py +78 -0
  39. code_constraints/csharp/sequence.py +295 -0
  40. code_constraints/enforce/__init__.py +15 -0
  41. code_constraints/enforce/engine.py +122 -0
  42. code_constraints/enforce/model.py +74 -0
  43. code_constraints/julia/__init__.py +5 -0
  44. code_constraints/julia/conformance.py +282 -0
  45. code_constraints/julia/fingerprint.py +226 -0
  46. code_constraints/julia/parser.py +523 -0
  47. code_constraints/julia/rules_extract.py +216 -0
  48. code_constraints/lint/__init__.py +10 -0
  49. code_constraints/lint/baseline.py +96 -0
  50. code_constraints/lint/config.py +239 -0
  51. code_constraints/lint/engine.py +179 -0
  52. code_constraints/lint/pipeline.py +108 -0
  53. code_constraints/lint/report.py +151 -0
  54. code_constraints/lint/rules/__init__.py +50 -0
  55. code_constraints/lint/rules/base.py +200 -0
  56. code_constraints/lint/rules/cyclic_package_dependencies.py +69 -0
  57. code_constraints/lint/rules/dangling_classes.py +98 -0
  58. code_constraints/lint/rules/forbidden_package_references.py +47 -0
  59. code_constraints/lint/rules/forbidden_references.py +48 -0
  60. code_constraints/lint/rules/frozen_members.py +67 -0
  61. code_constraints/lint/rules/frozen_rules.py +105 -0
  62. code_constraints/lint/rules/implementation_locks.py +156 -0
  63. code_constraints/lint/rules/layer_dependencies.py +92 -0
  64. code_constraints/lint/rules/max_class_fanout.py +41 -0
  65. code_constraints/lint/rules/no_new_classes.py +27 -0
  66. code_constraints/lint/rules/no_removed_classes.py +27 -0
  67. code_constraints/lint/rules/reference_architecture.py +111 -0
  68. code_constraints/lint/rules/subclass_naming.py +71 -0
  69. code_constraints/lint/rules/tag_conformance.py +76 -0
  70. code_constraints/lock/__init__.py +73 -0
  71. code_constraints/lock/engine.py +395 -0
  72. code_constraints/lock/model.py +235 -0
  73. code_constraints/lock/store.py +144 -0
  74. code_constraints/lua/__init__.py +5 -0
  75. code_constraints/lua/conformance.py +239 -0
  76. code_constraints/lua/fingerprint.py +252 -0
  77. code_constraints/lua/parser.py +500 -0
  78. code_constraints/lua/rules_extract.py +55 -0
  79. code_constraints/mcp/__init__.py +20 -0
  80. code_constraints/mcp/__main__.py +73 -0
  81. code_constraints/mcp/server.py +1203 -0
  82. code_constraints/odin/__init__.py +5 -0
  83. code_constraints/odin/conformance.py +244 -0
  84. code_constraints/odin/fingerprint.py +159 -0
  85. code_constraints/odin/parser.py +471 -0
  86. code_constraints/odin/rules_extract.py +38 -0
  87. code_constraints/python/__init__.py +3 -0
  88. code_constraints/python/activity.py +278 -0
  89. code_constraints/python/conformance.py +249 -0
  90. code_constraints/python/fingerprint.py +231 -0
  91. code_constraints/python/parser.py +330 -0
  92. code_constraints/python/rules_extract.py +83 -0
  93. code_constraints/python/sequence.py +257 -0
  94. code_constraints/reference/__init__.py +15 -0
  95. code_constraints/reference/compare.py +356 -0
  96. code_constraints/reference/report.py +38 -0
  97. code_constraints/svelte/__init__.py +3 -0
  98. code_constraints/svelte/parser.py +523 -0
  99. code_constraints/typescript/__init__.py +3 -0
  100. code_constraints/typescript/parser.py +590 -0
  101. code_constraints/waivers/__init__.py +89 -0
  102. code_constraints/waivers/collect.py +167 -0
  103. code_constraints/waivers/model.py +90 -0
  104. code_constraints/waivers/ops.py +150 -0
  105. code_constraints/waivers/review.py +156 -0
  106. code_constraints/waivers/store.py +300 -0
  107. code_constraints/web/__init__.py +0 -0
  108. code_constraints/web/_static/assets/index-3ivBsYY4.css +1 -0
  109. code_constraints/web/_static/assets/index-BTzTqGFp.js +9 -0
  110. code_constraints/web/_static/index.html +13 -0
  111. code_constraints/web/app.py +1076 -0
  112. code_constraints-0.1.0.dist-info/METADATA +663 -0
  113. code_constraints-0.1.0.dist-info/RECORD +116 -0
  114. code_constraints-0.1.0.dist-info/WHEEL +4 -0
  115. code_constraints-0.1.0.dist-info/entry_points.txt +3 -0
  116. code_constraints-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,436 @@
1
+ """Parse a C# project (directory of .cs files) into a `code_constraints.core.Project`.
2
+
3
+ Uses `tree_sitter` with the `tree_sitter_c_sharp` grammar. This is a syntactic
4
+ parse only — there is no semantic / type resolution, so inheritance lines may
5
+ appear as their textual form rather than as fully qualified names. That is a
6
+ documented limitation accepted for v1.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import re
12
+ from pathlib import Path
13
+
14
+ import tree_sitter_c_sharp
15
+ from tree_sitter import Language, Node, Parser
16
+
17
+ from code_constraints.core.model import (
18
+ Attribute,
19
+ Class,
20
+ ClassKind,
21
+ Operation,
22
+ Package,
23
+ Parameter,
24
+ Project,
25
+ SourceLocation,
26
+ Visibility,
27
+ )
28
+ from code_constraints.core.tags import find_tags
29
+ from code_constraints.csharp.activity import build_activity_from_tag
30
+ from code_constraints.csharp.rules_extract import extract_rules, using_has_shim
31
+ from code_constraints.csharp.sequence import build_sequence_from_tag
32
+
33
+ _LANG = Language(tree_sitter_c_sharp.language())
34
+ _PARSER = Parser(_LANG)
35
+
36
+
37
+ def parse_project(root: str | Path) -> Project:
38
+ root_path = Path(root).resolve()
39
+ if not root_path.is_dir():
40
+ raise ValueError(f"not a directory: {root_path}")
41
+
42
+ project = Project(source_language="csharp", root_path=str(root_path))
43
+ package_index: dict[str, Package] = {}
44
+
45
+ for cs_file in sorted(root_path.rglob("*.cs")):
46
+ if _should_skip(cs_file):
47
+ continue
48
+ _parse_file(cs_file, root_path, project, package_index)
49
+
50
+ return project
51
+
52
+
53
+ _SKIP_DIR_NAMES = {"bin", "obj", ".git", "packages", "TestResults"}
54
+
55
+
56
+ def _should_skip(p: Path) -> bool:
57
+ return any(part in _SKIP_DIR_NAMES for part in p.parts)
58
+
59
+
60
+ def _parse_file(
61
+ file: Path,
62
+ root: Path,
63
+ project: Project,
64
+ package_index: dict[str, Package],
65
+ ) -> None:
66
+ source_bytes = file.read_bytes()
67
+ tree = _PARSER.parse(source_bytes)
68
+ file_str = str(file.relative_to(root).as_posix())
69
+ source_text = source_bytes.decode("utf-8", errors="replace")
70
+
71
+ # Find every namespace_declaration (including file-scoped) and class-like
72
+ # declarations directly inside the file (which may belong to an implicit
73
+ # global namespace).
74
+ cu = tree.root_node
75
+ shim_in_scope = using_has_shim(cu, source_bytes)
76
+ # File-scoped namespace claims every subsequent top-level declaration.
77
+ file_scoped_ns: str | None = None
78
+ for child in cu.named_children:
79
+ if child.type == "namespace_declaration":
80
+ _visit_namespace(
81
+ child, "", project, package_index, source_bytes, file_str, shim_in_scope
82
+ )
83
+ elif child.type == "file_scoped_namespace_declaration":
84
+ name_node = child.child_by_field_name("name")
85
+ file_scoped_ns = (
86
+ _text(name_node, source_bytes) if name_node else "anon"
87
+ )
88
+ _ensure_package(project, file_scoped_ns, package_index)
89
+ elif child.type in _CLASS_LIKE_TYPES:
90
+ target_ns = file_scoped_ns or "__root__"
91
+ _ingest_class(
92
+ child, target_ns, project, package_index, source_bytes, file_str, shim_in_scope
93
+ )
94
+
95
+ # Tag-driven activities & sequences
96
+ tags = find_tags(source_text, comment_prefix="//")
97
+ for tag in tags:
98
+ if tag.kind == "uml-activity" and not tag.self_closed:
99
+ act = build_activity_from_tag(tag, tree, source_bytes, file=file_str)
100
+ if act is not None:
101
+ project.activities.append(act)
102
+ elif tag.kind == "uml-sequence" and not tag.self_closed:
103
+ seq = build_sequence_from_tag(tag, tree, source_bytes, file=file_str)
104
+ if seq is not None:
105
+ project.sequences.append(seq)
106
+
107
+
108
+ _CLASS_LIKE_TYPES = {
109
+ "class_declaration",
110
+ "interface_declaration",
111
+ "struct_declaration",
112
+ "record_declaration",
113
+ "record_struct_declaration",
114
+ "enum_declaration",
115
+ }
116
+
117
+
118
+ def _visit_namespace(
119
+ node: Node,
120
+ parent_qn: str,
121
+ project: Project,
122
+ package_index: dict[str, Package],
123
+ source: bytes,
124
+ file_str: str,
125
+ shim_in_scope: bool,
126
+ ) -> None:
127
+ name_node = node.child_by_field_name("name")
128
+ name = _text(name_node, source) if name_node else "anon"
129
+ qn = name if not parent_qn else f"{parent_qn}.{name}"
130
+ pkg = _ensure_package(project, qn, package_index)
131
+
132
+ body = node.child_by_field_name("body")
133
+ if body is None:
134
+ return
135
+ for child in body.named_children:
136
+ if child.type == "namespace_declaration":
137
+ _visit_namespace(child, qn, project, package_index, source, file_str, shim_in_scope)
138
+ elif child.type in _CLASS_LIKE_TYPES:
139
+ _ingest_class(child, qn, project, package_index, source, file_str, shim_in_scope)
140
+
141
+
142
+ def _ensure_package(
143
+ project: Project, qualified_name: str, index: dict[str, Package]
144
+ ) -> Package:
145
+ if qualified_name in index:
146
+ return index[qualified_name]
147
+ if qualified_name == "__root__":
148
+ pkg = Package(name="__root__", qualified_name="__root__")
149
+ project.packages.append(pkg)
150
+ index[qualified_name] = pkg
151
+ return pkg
152
+ parts = qualified_name.split(".")
153
+ if len(parts) == 1:
154
+ pkg = Package(name=parts[0], qualified_name=qualified_name)
155
+ project.packages.append(pkg)
156
+ else:
157
+ parent = _ensure_package(project, ".".join(parts[:-1]), index)
158
+ pkg = Package(name=parts[-1], qualified_name=qualified_name)
159
+ parent.sub_packages.append(pkg)
160
+ index[qualified_name] = pkg
161
+ return pkg
162
+
163
+
164
+ def _ingest_class(
165
+ node: Node,
166
+ package_qn: str,
167
+ project: Project,
168
+ package_index: dict[str, Package],
169
+ source: bytes,
170
+ file_str: str,
171
+ shim_in_scope: bool,
172
+ ) -> None:
173
+ name_node = node.child_by_field_name("name")
174
+ if name_node is None:
175
+ return
176
+ name = _text(name_node, source)
177
+ qn = f"{package_qn}.{name}" if package_qn and package_qn != "__root__" else name
178
+
179
+ cls = Class(
180
+ name=name,
181
+ qualified_name=qn,
182
+ kind=_kind_for(node, source),
183
+ bases=_bases(node, source),
184
+ location=SourceLocation(
185
+ file=file_str,
186
+ start_line=node.start_point[0] + 1,
187
+ end_line=node.end_point[0] + 1,
188
+ ),
189
+ description=_xml_doc_for(node, source),
190
+ rules=extract_rules(node, source, shim_in_scope),
191
+ )
192
+
193
+ body = node.child_by_field_name("body")
194
+ if body is not None:
195
+ for member in body.named_children:
196
+ _ingest_member(member, cls, source, shim_in_scope)
197
+
198
+ cls.dependencies = _dedupe_refs(cls.dependencies, drop=cls.name)
199
+
200
+ pkg = _ensure_package(project, package_qn, package_index)
201
+ pkg.classes.append(cls)
202
+
203
+
204
+ def _kind_for(node: Node, source: bytes) -> ClassKind:
205
+ if node.type == "interface_declaration":
206
+ return "interface"
207
+ if node.type == "struct_declaration":
208
+ return "struct"
209
+ if node.type in ("record_declaration", "record_struct_declaration"):
210
+ return "record"
211
+ if node.type == "enum_declaration":
212
+ return "enum"
213
+ modifiers = _modifiers(node, source)
214
+ if "static" in modifiers:
215
+ return "static"
216
+ if "abstract" in modifiers:
217
+ return "abstract"
218
+ return "class"
219
+
220
+
221
+ def _modifiers(node: Node, source: bytes) -> list[str]:
222
+ mods: list[str] = []
223
+ for c in node.children:
224
+ if c.type == "modifier":
225
+ mods.append(_text(c, source))
226
+ return mods
227
+
228
+
229
+ def _bases(node: Node, source: bytes) -> list[str]:
230
+ base_list = next(
231
+ (c for c in node.children if c.type == "base_list"), None
232
+ )
233
+ if base_list is None:
234
+ return []
235
+ bases: list[str] = []
236
+ for child in base_list.named_children:
237
+ bases.append(_text(child, source))
238
+ return bases
239
+
240
+
241
+ _PASCAL_IDENT_RE = re.compile(r"^[A-Z][A-Za-z0-9_]*$")
242
+
243
+
244
+ def _body_type_refs(node: Node, source: bytes) -> list[str]:
245
+ """Collect PascalCase identifier tokens used inside a method/ctor body.
246
+
247
+ Heuristic (no type resolution): a PascalCase identifier in the body is a
248
+ candidate type reference — static-call receivers (`ControlUtils.Foo()`),
249
+ object creations (`new Foo()`), local declarations, generic args, etc.
250
+ Downstream `resolve_association` keeps only the ones that map to a project
251
+ class; Unity types, locals and method names simply drop out. Only the `body`
252
+ field is walked (the block, or the arrow-expression clause for expression-
253
+ bodied members), so `[Attribute(...)]` annotations and parameter names in
254
+ the signature never leak in.
255
+ """
256
+ body = node.child_by_field_name("body")
257
+ if body is None:
258
+ return []
259
+ out: list[str] = []
260
+ seen: set[str] = set()
261
+ stack = [body]
262
+ while stack:
263
+ n = stack.pop()
264
+ stack.extend(n.children)
265
+ if n.type == "identifier":
266
+ txt = _text(n, source)
267
+ if txt and txt not in seen and _PASCAL_IDENT_RE.match(txt):
268
+ seen.add(txt)
269
+ out.append(txt)
270
+ return out
271
+
272
+
273
+ def _dedupe_refs(refs: list[str], *, drop: str = "") -> list[str]:
274
+ """Order-preserving dedupe, dropping a given name (the owning class)."""
275
+ out: list[str] = []
276
+ seen: set[str] = set()
277
+ for r in refs:
278
+ if r == drop or r in seen:
279
+ continue
280
+ seen.add(r)
281
+ out.append(r)
282
+ return out
283
+
284
+
285
+ def _ingest_member(node: Node, cls: Class, source: bytes, shim_in_scope: bool) -> None:
286
+ if node.type == "field_declaration":
287
+ cls.attributes.extend(_attributes_from_field(node, source))
288
+ elif node.type == "property_declaration":
289
+ cls.attributes.append(_attribute_from_property(node, source))
290
+ elif node.type in ("method_declaration", "constructor_declaration"):
291
+ cls.operations.append(_operation_from_method(node, source, shim_in_scope))
292
+ cls.dependencies.extend(_body_type_refs(node, source))
293
+ elif node.type == "enum_member_declaration":
294
+ name_node = node.child_by_field_name("name")
295
+ if name_node is not None:
296
+ cls.attributes.append(
297
+ Attribute(
298
+ name=_text(name_node, source),
299
+ type=cls.name,
300
+ visibility=Visibility.PUBLIC,
301
+ is_static=True,
302
+ )
303
+ )
304
+
305
+
306
+ def _attributes_from_field(node: Node, source: bytes) -> list[Attribute]:
307
+ # tree-sitter-c-sharp puts the type on the inner `variable_declaration`,
308
+ # NOT on the outer `field_declaration`. Calling child_by_field_name("type")
309
+ # on the field_declaration returns None, which silently drops the type.
310
+ mods = _modifiers(node, source)
311
+ vis = _visibility_from_modifiers(mods)
312
+ is_static = "static" in mods
313
+ is_readonly = "readonly" in mods or "const" in mods
314
+
315
+ decl = next((c for c in node.named_children if c.type == "variable_declaration"), None)
316
+ if decl is None:
317
+ return []
318
+ type_node = decl.child_by_field_name("type")
319
+ type_text = _text(type_node, source) if type_node else ""
320
+
321
+ out: list[Attribute] = []
322
+ for declarator in decl.named_children:
323
+ if declarator.type != "variable_declarator":
324
+ continue
325
+ name_node = declarator.child_by_field_name("name")
326
+ if name_node is None:
327
+ continue
328
+ out.append(
329
+ Attribute(
330
+ name=_text(name_node, source),
331
+ type=type_text,
332
+ visibility=vis,
333
+ is_static=is_static,
334
+ is_readonly=is_readonly,
335
+ )
336
+ )
337
+ return out
338
+
339
+
340
+ def _attribute_from_property(node: Node, source: bytes) -> Attribute:
341
+ name_node = node.child_by_field_name("name")
342
+ type_node = node.child_by_field_name("type")
343
+ mods = _modifiers(node, source)
344
+ return Attribute(
345
+ name=_text(name_node, source) if name_node else "",
346
+ type=_text(type_node, source) if type_node else "",
347
+ visibility=_visibility_from_modifiers(mods),
348
+ is_static="static" in mods,
349
+ is_readonly="readonly" in mods,
350
+ )
351
+
352
+
353
+ def _operation_from_method(node: Node, source: bytes, shim_in_scope: bool = False) -> Operation:
354
+ mods = _modifiers(node, source)
355
+ name_node = node.child_by_field_name("name")
356
+ name = _text(name_node, source) if name_node else "<ctor>"
357
+ return_node = node.child_by_field_name("returns") or node.child_by_field_name("type")
358
+ return_type = _text(return_node, source) if return_node else ""
359
+
360
+ params: list[Parameter] = []
361
+ param_list = node.child_by_field_name("parameters")
362
+ if param_list is not None:
363
+ for p in param_list.named_children:
364
+ if p.type != "parameter":
365
+ continue
366
+ p_name = p.child_by_field_name("name")
367
+ p_type = p.child_by_field_name("type")
368
+ params.append(
369
+ Parameter(
370
+ name=_text(p_name, source) if p_name else "",
371
+ type=_text(p_type, source) if p_type else "",
372
+ )
373
+ )
374
+
375
+ return Operation(
376
+ name=name,
377
+ parameters=params,
378
+ return_type=return_type,
379
+ visibility=_visibility_from_modifiers(mods),
380
+ is_static="static" in mods,
381
+ is_abstract="abstract" in mods,
382
+ description=_xml_doc_for(node, source),
383
+ rules=extract_rules(node, source, shim_in_scope),
384
+ )
385
+
386
+
387
+ def _visibility_from_modifiers(mods: list[str]) -> Visibility:
388
+ if "private" in mods:
389
+ return Visibility.PRIVATE
390
+ if "protected" in mods:
391
+ return Visibility.PROTECTED
392
+ if "internal" in mods and "protected" not in mods:
393
+ return Visibility.PACKAGE
394
+ if "public" in mods:
395
+ return Visibility.PUBLIC
396
+ return Visibility.PRIVATE # C# default for class members
397
+
398
+
399
+ def _text(node: Node | None, source: bytes) -> str:
400
+ if node is None:
401
+ return ""
402
+ return source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
403
+
404
+
405
+ def _xml_doc_for(node: Node, source: bytes) -> str | None:
406
+ """Collect `///` XML-doc lines immediately preceding `node`.
407
+
408
+ Returns the text inside `<summary>` if present, otherwise the tag-stripped
409
+ concatenation. Returns None when there's no `///` block above the node.
410
+ """
411
+ lines: list[str] = []
412
+ sib = node.prev_sibling
413
+ while sib is not None and sib.type == "comment":
414
+ raw = _text(sib, source).strip()
415
+ if not raw.startswith("///"):
416
+ break
417
+ content = raw[3:]
418
+ if content.startswith(" "):
419
+ content = content[1:]
420
+ lines.insert(0, content)
421
+ sib = sib.prev_sibling
422
+ if not lines:
423
+ return None
424
+ joined = "\n".join(lines).strip()
425
+ if not joined:
426
+ return None
427
+ m = re.search(r"<summary\b[^>]*>(.*?)</summary>", joined, re.DOTALL)
428
+ if m:
429
+ # Strip inner XML-doc tags (<see/>, <paramref/>, <c>...</c>, etc.) so
430
+ # only the prose survives. The crefs would otherwise show up as raw
431
+ # markup in the viewer.
432
+ stripped = re.sub(r"<[^>]+>", "", m.group(1))
433
+ summary = " ".join(stripped.split())
434
+ return summary or None
435
+ fallback = " ".join(re.sub(r"<[^>]+>", "", joined).split())
436
+ return fallback or None
@@ -0,0 +1,78 @@
1
+ """Recognise architectural-rule attributes on C# declarations.
2
+
3
+ Shared by the UML parser and the `cdec enforce` conformance analyzer. An
4
+ attribute counts as a rule only when the file has `using CodeConstraints.Rules;`
5
+ (or the attribute is written fully qualified) — so a same-named user attribute
6
+ is never misread as a rule.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from tree_sitter import Node
12
+
13
+ from code_constraints.core.model import RuleAnnotation
14
+ from code_constraints.core.rules import CSHARP_SHIM_NAMESPACE, by_csharp_name
15
+
16
+
17
+ def using_has_shim(root: Node, source: bytes) -> bool:
18
+ """True when the compilation unit imports the shim namespace."""
19
+ stack = [root]
20
+ while stack:
21
+ node = stack.pop()
22
+ if node.type == "using_directive" and CSHARP_SHIM_NAMESPACE in _text(node, source):
23
+ return True
24
+ stack.extend(node.children)
25
+ return False
26
+
27
+
28
+ def extract_rules(decl_node: Node, source: bytes, shim_in_scope: bool) -> list[RuleAnnotation]:
29
+ rules: list[RuleAnnotation] = []
30
+ for child in decl_node.children:
31
+ if child.type != "attribute_list":
32
+ continue
33
+ for attr in child.named_children:
34
+ if attr.type != "attribute":
35
+ continue
36
+ name_node = attr.child_by_field_name("name")
37
+ raw_name = _text(name_node, source) if name_node else ""
38
+ if not raw_name:
39
+ continue
40
+ qualified_shim = raw_name.startswith(CSHARP_SHIM_NAMESPACE + ".")
41
+ if not (shim_in_scope or qualified_shim):
42
+ continue
43
+ spec = by_csharp_name(raw_name.rsplit(".", 1)[-1])
44
+ if spec is None:
45
+ continue
46
+ args, kwargs = _attr_args(attr, source)
47
+ rules.append(RuleAnnotation(name=spec.id, args=args, kwargs=kwargs))
48
+ return rules
49
+
50
+
51
+ def _attr_args(attr: Node, source: bytes) -> tuple[list[str], dict[str, str]]:
52
+ args: list[str] = []
53
+ kwargs: dict[str, str] = {}
54
+ arglist = next(
55
+ (c for c in attr.children if c.type == "attribute_argument_list"), None
56
+ )
57
+ if arglist is None:
58
+ return args, kwargs
59
+ for a in arglist.children:
60
+ if a.type != "attribute_argument":
61
+ continue
62
+ kids = a.children
63
+ # Named arg: `identifier = value` (or `identifier : value`).
64
+ sep = next((i for i, c in enumerate(kids) if c.type in ("=", ":")), None)
65
+ if sep is not None and sep >= 1 and kids[sep - 1].type == "identifier":
66
+ name = _text(kids[sep - 1], source)
67
+ value = next((c for c in kids[sep + 1 :] if c.is_named), None)
68
+ kwargs[name] = _text(value, source) if value else ""
69
+ else:
70
+ value = next((c for c in kids if c.is_named), None)
71
+ args.append(_text(value, source) if value else "")
72
+ return args, kwargs
73
+
74
+
75
+ def _text(node: Node | None, source: bytes) -> str:
76
+ if node is None:
77
+ return ""
78
+ return source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")