entrygraph 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 (102) hide show
  1. entrygraph/__init__.py +87 -0
  2. entrygraph/__main__.py +8 -0
  3. entrygraph/_version.py +24 -0
  4. entrygraph/api.py +549 -0
  5. entrygraph/cli/__init__.py +0 -0
  6. entrygraph/cli/main.py +387 -0
  7. entrygraph/cli/render.py +136 -0
  8. entrygraph/data/sinks/csharp.toml +106 -0
  9. entrygraph/data/sinks/go.toml +87 -0
  10. entrygraph/data/sinks/java.toml +92 -0
  11. entrygraph/data/sinks/javascript.toml +112 -0
  12. entrygraph/data/sinks/lib_javascript.toml +34 -0
  13. entrygraph/data/sinks/lib_python.toml +39 -0
  14. entrygraph/data/sinks/php.toml +125 -0
  15. entrygraph/data/sinks/python.toml +160 -0
  16. entrygraph/data/sinks/ruby.toml +102 -0
  17. entrygraph/data/sinks/rust.toml +68 -0
  18. entrygraph/db/__init__.py +0 -0
  19. entrygraph/db/engine.py +34 -0
  20. entrygraph/db/meta.py +70 -0
  21. entrygraph/db/models.py +176 -0
  22. entrygraph/db/queries.py +155 -0
  23. entrygraph/detect/__init__.py +0 -0
  24. entrygraph/detect/entrypoints/__init__.py +22 -0
  25. entrygraph/detect/entrypoints/base.py +124 -0
  26. entrygraph/detect/entrypoints/configs.py +139 -0
  27. entrygraph/detect/entrypoints/csharp.py +156 -0
  28. entrygraph/detect/entrypoints/golang.py +158 -0
  29. entrygraph/detect/entrypoints/java.py +187 -0
  30. entrygraph/detect/entrypoints/javascript.py +211 -0
  31. entrygraph/detect/entrypoints/php.py +133 -0
  32. entrygraph/detect/entrypoints/python.py +335 -0
  33. entrygraph/detect/entrypoints/ruby.py +147 -0
  34. entrygraph/detect/entrypoints/rust.py +153 -0
  35. entrygraph/detect/frameworks.py +369 -0
  36. entrygraph/detect/manifests.py +234 -0
  37. entrygraph/detect/taint.py +224 -0
  38. entrygraph/errors.py +27 -0
  39. entrygraph/extract/__init__.py +0 -0
  40. entrygraph/extract/base.py +51 -0
  41. entrygraph/extract/csharp.py +502 -0
  42. entrygraph/extract/golang.py +342 -0
  43. entrygraph/extract/ir.py +105 -0
  44. entrygraph/extract/java.py +329 -0
  45. entrygraph/extract/javascript.py +400 -0
  46. entrygraph/extract/php.py +426 -0
  47. entrygraph/extract/python.py +390 -0
  48. entrygraph/extract/registry.py +43 -0
  49. entrygraph/extract/ruby.py +321 -0
  50. entrygraph/extract/rust.py +482 -0
  51. entrygraph/fs/__init__.py +0 -0
  52. entrygraph/fs/hashing.py +78 -0
  53. entrygraph/fs/lang.py +134 -0
  54. entrygraph/fs/walker.py +167 -0
  55. entrygraph/graph/__init__.py +0 -0
  56. entrygraph/graph/adjacency.py +146 -0
  57. entrygraph/graph/cte.py +123 -0
  58. entrygraph/graph/scoring.py +101 -0
  59. entrygraph/kinds.py +51 -0
  60. entrygraph/parsing/__init__.py +0 -0
  61. entrygraph/parsing/parsers.py +49 -0
  62. entrygraph/parsing/queries.py +39 -0
  63. entrygraph/pipeline/__init__.py +0 -0
  64. entrygraph/pipeline/scanner.py +506 -0
  65. entrygraph/pipeline/worker.py +49 -0
  66. entrygraph/pipeline/writer.py +41 -0
  67. entrygraph/py.typed +0 -0
  68. entrygraph/queries/csharp/calls.scm +4 -0
  69. entrygraph/queries/csharp/definitions.scm +29 -0
  70. entrygraph/queries/csharp/imports.scm +4 -0
  71. entrygraph/queries/go/calls.scm +2 -0
  72. entrygraph/queries/go/definitions.scm +24 -0
  73. entrygraph/queries/go/imports.scm +1 -0
  74. entrygraph/queries/java/calls.scm +2 -0
  75. entrygraph/queries/java/definitions.scm +14 -0
  76. entrygraph/queries/java/imports.scm +2 -0
  77. entrygraph/queries/javascript/calls.scm +4 -0
  78. entrygraph/queries/javascript/definitions.scm +4 -0
  79. entrygraph/queries/javascript/imports.scm +6 -0
  80. entrygraph/queries/php/calls.scm +8 -0
  81. entrygraph/queries/php/definitions.scm +24 -0
  82. entrygraph/queries/php/imports.scm +1 -0
  83. entrygraph/queries/python/calls.scm +2 -0
  84. entrygraph/queries/python/definitions.scm +11 -0
  85. entrygraph/queries/python/imports.scm +2 -0
  86. entrygraph/queries/ruby/calls.scm +4 -0
  87. entrygraph/queries/ruby/definitions.scm +20 -0
  88. entrygraph/queries/ruby/imports.scm +7 -0
  89. entrygraph/queries/rust/calls.scm +5 -0
  90. entrygraph/queries/rust/definitions.scm +26 -0
  91. entrygraph/queries/rust/imports.scm +4 -0
  92. entrygraph/resolve/__init__.py +0 -0
  93. entrygraph/resolve/externals.py +61 -0
  94. entrygraph/resolve/hierarchy.py +152 -0
  95. entrygraph/resolve/resolver.py +275 -0
  96. entrygraph/resolve/symbol_table.py +48 -0
  97. entrygraph/results.py +138 -0
  98. entrygraph-0.1.0.dist-info/METADATA +204 -0
  99. entrygraph-0.1.0.dist-info/RECORD +102 -0
  100. entrygraph-0.1.0.dist-info/WHEEL +4 -0
  101. entrygraph-0.1.0.dist-info/entry_points.txt +2 -0
  102. entrygraph-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,502 @@
1
+ """C# extractor: .scm queries harvest nodes, this shaper builds the IR.
2
+
3
+ Qualified names come from the enclosing **namespace**, not the file path — C#
4
+ decouples the two. Both classic block namespaces (``namespace Foo { ... }``)
5
+ and C# 10 file-scoped namespaces (``namespace Foo;``) are honoured by walking
6
+ ancestors for ``namespace_declaration`` / ``file_scoped_namespace_declaration``.
7
+ So ``MyApp.Controllers.UsersController.Get`` regardless of where the file lives.
8
+ Files with no namespace (top-level statements) fall back to a directory-derived
9
+ module path.
10
+
11
+ ``module_path_for`` returns ``(dotted_dir_module, False)`` — it cannot see the
12
+ namespace (no parse), so it is only a fallback; ``extract`` overrides the
13
+ per-symbol qname prefix with the real namespace when one is present.
14
+
15
+ Attributes (``[HttpGet("/x")]``) are captured on ``RawSymbol.decorators`` as raw
16
+ source text and also emitted as ``RawReference(kind="decorator")`` (mirrors the
17
+ Java annotation handling). Base types / interfaces become
18
+ ``RawReference(kind="inherit")`` — C# does not syntactically distinguish a base
19
+ class from implemented interfaces, so every entry in the ``base_list`` is emitted
20
+ as an ``inherit`` ref and stored on ``bases``. ``is_exported`` is ``"public" in
21
+ modifiers``.
22
+
23
+ Partial classes (``partial class Foo`` split across files/blocks) produce the
24
+ same qualified name more than once; the DB is last-writer-wins, which is
25
+ acceptable — members still qualify under the shared type qname.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ from typing import TYPE_CHECKING, ClassVar
31
+
32
+ from entrygraph.extract.base import FileContext, node_text, span_of, truncate
33
+ from entrygraph.extract.ir import FileExtraction, RawImport, RawReference, RawSymbol
34
+ from entrygraph.kinds import SymbolKind
35
+ from entrygraph.parsing.queries import captures, load_query
36
+
37
+ if TYPE_CHECKING: # pragma: no cover
38
+ from tree_sitter import Node, Tree
39
+
40
+ _NAMESPACE_TYPES = frozenset(
41
+ {"namespace_declaration", "file_scoped_namespace_declaration"}
42
+ )
43
+ _TYPE_SCOPES = frozenset(
44
+ {
45
+ "class_declaration",
46
+ "interface_declaration",
47
+ "struct_declaration",
48
+ "record_declaration",
49
+ }
50
+ )
51
+ _CALLABLE_SCOPES = frozenset(
52
+ {
53
+ "method_declaration",
54
+ "constructor_declaration",
55
+ "local_function_statement",
56
+ "property_declaration",
57
+ }
58
+ )
59
+
60
+
61
+ def _looks_like_type_ref(receiver: str) -> bool:
62
+ """Heuristic: is a member-access receiver a *type* (static call) rather than
63
+ an instance/variable? A type reference is a dotted chain of identifiers whose
64
+ every segment starts with an uppercase letter (``Process``,
65
+ ``System.Diagnostics.Process``). Instance receivers are conventionally
66
+ lowercase (``service``, ``cmd``, ``_reports``) or use ``this``/``base``.
67
+ Anything with call/index syntax is not a plain type reference.
68
+ """
69
+ if not receiver or any(c in receiver for c in "()[]?"):
70
+ return False
71
+ segments = receiver.split(".")
72
+ return all(seg[:1].isupper() and seg[:1].isalpha() for seg in segments)
73
+
74
+
75
+ class CSharpExtractor:
76
+ language_ids: ClassVar[tuple[str, ...]] = ("csharp",)
77
+
78
+ def module_path_for(self, repo_relative_path: str) -> tuple[str, bool]:
79
+ # Fallback only: the real qname prefix is the namespace, resolved per
80
+ # symbol in extract(). Directory-derived dotted path, ".cs" dropped.
81
+ path = repo_relative_path.removesuffix(".cs")
82
+ dotted = ".".join(p for p in path.split("/") if p)
83
+ return dotted or "_root", False
84
+
85
+ def extract(self, tree: "Tree", ctx: FileContext) -> FileExtraction:
86
+ root = tree.root_node
87
+ out = FileExtraction(
88
+ path=ctx.path,
89
+ language=ctx.language,
90
+ module_path=ctx.module_path,
91
+ parse_ok=not root.has_error,
92
+ error_count=1 if root.has_error else 0,
93
+ )
94
+ self._definitions(root, ctx, out)
95
+ self._imports(root, ctx, out)
96
+ self._calls(root, ctx, out)
97
+ return out
98
+
99
+ # ---------------- definitions ----------------
100
+
101
+ def _definitions(self, root: "Node", ctx: FileContext, out: FileExtraction) -> None:
102
+ caps = captures(load_query("csharp", "definitions"), root)
103
+
104
+ for node in caps.get("def.class", []):
105
+ self._add_type(node, ctx, out, SymbolKind.CLASS)
106
+ for node in caps.get("def.interface", []):
107
+ self._add_type(node, ctx, out, SymbolKind.INTERFACE)
108
+ for node in caps.get("def.struct", []):
109
+ self._add_type(node, ctx, out, SymbolKind.STRUCT)
110
+ for node in caps.get("def.record", []):
111
+ # C# has no dedicated record kind in the IR; treat as a class.
112
+ self._add_type(node, ctx, out, SymbolKind.CLASS)
113
+
114
+ for kind_key, sym_kind in (
115
+ ("def.method", SymbolKind.METHOD),
116
+ ("def.constructor", SymbolKind.METHOD),
117
+ ("def.local_function", SymbolKind.FUNCTION),
118
+ ):
119
+ for node in caps.get(kind_key, []):
120
+ self._add_callable(node, ctx, out, sym_kind)
121
+
122
+ for node in caps.get("def.property", []):
123
+ self._add_member(node, ctx, out, SymbolKind.PROPERTY)
124
+
125
+ for node in caps.get("def.field", []):
126
+ self._add_field(node, ctx, out)
127
+
128
+ def _add_type(
129
+ self, node: "Node", ctx: FileContext, out: FileExtraction, kind: SymbolKind
130
+ ) -> None:
131
+ name_node = node.child_by_field_name("name")
132
+ if name_node is None:
133
+ return
134
+ name = node_text(name_node)
135
+ qname, parent_q = self._qualify(node, name, ctx)
136
+ bases = self._base_types(node)
137
+ modifiers = self._modifiers(node)
138
+ out.symbols.append(
139
+ RawSymbol(
140
+ kind=kind,
141
+ name=name,
142
+ qualified_name=qname,
143
+ span=span_of(node),
144
+ parent_qualified_name=parent_q,
145
+ signature=self._signature(node),
146
+ decorators=self._attributes(node),
147
+ bases=bases,
148
+ modifiers=modifiers,
149
+ is_exported="public" in modifiers,
150
+ )
151
+ )
152
+ for base in bases:
153
+ out.references.append(
154
+ RawReference(
155
+ kind="inherit",
156
+ callee_text=base,
157
+ callee_name=base.rsplit(".", 1)[-1],
158
+ receiver_text=None,
159
+ span=span_of(node),
160
+ caller_qualified_name=qname,
161
+ )
162
+ )
163
+ self._emit_attribute_refs(node, qname, out)
164
+
165
+ def _add_callable(
166
+ self, node: "Node", ctx: FileContext, out: FileExtraction, kind: SymbolKind
167
+ ) -> None:
168
+ name_node = node.child_by_field_name("name")
169
+ if name_node is None:
170
+ return
171
+ name = node_text(name_node)
172
+ qname, parent_q = self._qualify(node, name, ctx)
173
+ modifiers = self._modifiers(node)
174
+ out.symbols.append(
175
+ RawSymbol(
176
+ kind=kind,
177
+ name=name,
178
+ qualified_name=qname,
179
+ span=span_of(node),
180
+ parent_qualified_name=parent_q,
181
+ signature=self._signature(node),
182
+ decorators=self._attributes(node),
183
+ modifiers=modifiers,
184
+ is_exported="public" in modifiers,
185
+ )
186
+ )
187
+ self._emit_attribute_refs(node, qname, out)
188
+
189
+ def _add_member(
190
+ self, node: "Node", ctx: FileContext, out: FileExtraction, kind: SymbolKind
191
+ ) -> None:
192
+ if self._nearest_type_scope(node) is None:
193
+ return # only type members are symbols
194
+ name_node = node.child_by_field_name("name")
195
+ if name_node is None:
196
+ return
197
+ name = node_text(name_node)
198
+ qname, parent_q = self._qualify(node, name, ctx)
199
+ modifiers = self._modifiers(node)
200
+ out.symbols.append(
201
+ RawSymbol(
202
+ kind=kind,
203
+ name=name,
204
+ qualified_name=qname,
205
+ span=span_of(node),
206
+ parent_qualified_name=parent_q,
207
+ signature=truncate(node_text(node), 120),
208
+ decorators=self._attributes(node),
209
+ modifiers=modifiers,
210
+ is_exported="public" in modifiers,
211
+ )
212
+ )
213
+ self._emit_attribute_refs(node, qname, out)
214
+
215
+ def _add_field(self, node: "Node", ctx: FileContext, out: FileExtraction) -> None:
216
+ if self._nearest_type_scope(node) is None:
217
+ return
218
+ declaration = next(
219
+ (c for c in node.named_children if c.type == "variable_declaration"), None
220
+ )
221
+ if declaration is None:
222
+ return
223
+ declarator = next(
224
+ (c for c in declaration.named_children if c.type == "variable_declarator"),
225
+ None,
226
+ )
227
+ if declarator is None:
228
+ return
229
+ name_node = declarator.child_by_field_name("name") or next(
230
+ (c for c in declarator.named_children if c.type == "identifier"), None
231
+ )
232
+ if name_node is None:
233
+ return
234
+ name = node_text(name_node)
235
+ qname, parent_q = self._qualify(node, name, ctx)
236
+ modifiers = self._modifiers(node)
237
+ is_const = "const" in modifiers or (
238
+ "static" in modifiers and "readonly" in modifiers and name.isupper()
239
+ )
240
+ out.symbols.append(
241
+ RawSymbol(
242
+ kind=SymbolKind.CONSTANT if is_const else SymbolKind.FIELD,
243
+ name=name,
244
+ qualified_name=qname,
245
+ span=span_of(node),
246
+ parent_qualified_name=parent_q,
247
+ signature=truncate(node_text(node), 120),
248
+ decorators=self._attributes(node),
249
+ modifiers=modifiers,
250
+ is_exported="public" in modifiers,
251
+ )
252
+ )
253
+
254
+ # ---------------- imports ----------------
255
+
256
+ def _imports(self, root: "Node", ctx: FileContext, out: FileExtraction) -> None:
257
+ caps = captures(load_query("csharp", "imports"), root)
258
+ for node in caps.get("import", []):
259
+ self._add_using(node, out)
260
+ for imp in out.imports:
261
+ if imp.module:
262
+ out.framework_signals.append(("import", imp.module))
263
+
264
+ def _add_using(self, node: "Node", out: FileExtraction) -> None:
265
+ # Named children in order; an `=` child marks an alias directive, a
266
+ # `static` keyword marks `using static`.
267
+ children = node.children
268
+ has_alias = any(c.type == "=" for c in children)
269
+ # The dotted namespace / type is the qualified_name or identifier child.
270
+ name_nodes = [
271
+ c
272
+ for c in node.named_children
273
+ if c.type in ("qualified_name", "identifier")
274
+ ]
275
+ if not name_nodes:
276
+ return
277
+ if has_alias and len(name_nodes) >= 2:
278
+ alias = node_text(name_nodes[0])
279
+ module = node_text(name_nodes[1])
280
+ imported = module.rsplit(".", 1)[-1]
281
+ out.imports.append(
282
+ RawImport(
283
+ module=module,
284
+ imported_name=imported,
285
+ alias=alias,
286
+ span=span_of(node),
287
+ )
288
+ )
289
+ return
290
+ module = node_text(name_nodes[-1])
291
+ # Plain / static using: a namespace wildcard import (any type in it is
292
+ # brought into scope), mirror Java's star import.
293
+ out.imports.append(
294
+ RawImport(
295
+ module=module,
296
+ imported_name="*",
297
+ alias="*",
298
+ span=span_of(node),
299
+ )
300
+ )
301
+
302
+ # ---------------- calls ----------------
303
+
304
+ def _calls(self, root: "Node", ctx: FileContext, out: FileExtraction) -> None:
305
+ caps = captures(load_query("csharp", "calls"), root)
306
+
307
+ for node in caps.get("call", []):
308
+ self._add_invocation(node, ctx, out)
309
+ for node in caps.get("new", []):
310
+ self._add_object_creation(node, ctx, out)
311
+
312
+ def _add_invocation(self, node: "Node", ctx: FileContext, out: FileExtraction) -> None:
313
+ fn = node.child_by_field_name("function")
314
+ if fn is None:
315
+ return
316
+ if fn.type == "member_access_expression":
317
+ name_node = fn.child_by_field_name("name")
318
+ obj = fn.child_by_field_name("expression")
319
+ if name_node is None:
320
+ return
321
+ callee_name = node_text(name_node)
322
+ receiver = node_text(obj) if obj is not None else None
323
+ callee_text = node_text(fn)
324
+ # Static-type calls (`Process.Start`, `System.Diagnostics.Process.Start`)
325
+ # cannot be told from instance calls by the grammar, but the receiver
326
+ # of a static call is a *type* — a dotted chain of PascalCase
327
+ # identifiers. Treat those as bare calls so the resolver keeps the
328
+ # full dotted callee (`cs:Process.Start`) instead of collapsing to
329
+ # `cs:*.Start`; instance receivers (`cmd`, `service`) stay set so
330
+ # `cs:*.Method` sink matching still fires.
331
+ if receiver is not None and _looks_like_type_ref(receiver):
332
+ receiver = None
333
+ else:
334
+ callee_name = node_text(fn).rsplit(".", 1)[-1]
335
+ receiver = None
336
+ callee_text = node_text(fn)
337
+ args = node.child_by_field_name("arguments")
338
+ out.references.append(
339
+ RawReference(
340
+ kind="call",
341
+ callee_text=callee_text,
342
+ callee_name=callee_name,
343
+ receiver_text=receiver,
344
+ span=span_of(node),
345
+ caller_qualified_name=self._caller(node, ctx),
346
+ arg_count=len(args.named_children) if args is not None else 0,
347
+ arg_preview=truncate(node_text(args)) if args is not None else None,
348
+ )
349
+ )
350
+
351
+ def _add_object_creation(
352
+ self, node: "Node", ctx: FileContext, out: FileExtraction
353
+ ) -> None:
354
+ type_node = node.child_by_field_name("type")
355
+ if type_node is None:
356
+ return
357
+ type_text = node_text(type_node).split("<", 1)[0].strip()
358
+ callee_name = type_text.rsplit(".", 1)[-1]
359
+ args = node.child_by_field_name("arguments")
360
+ out.references.append(
361
+ RawReference(
362
+ kind="call",
363
+ callee_text=type_text,
364
+ callee_name=callee_name,
365
+ receiver_text=None,
366
+ span=span_of(node),
367
+ caller_qualified_name=self._caller(node, ctx),
368
+ arg_count=len(args.named_children) if args is not None else 0,
369
+ arg_preview=truncate(node_text(args)) if args is not None else None,
370
+ )
371
+ )
372
+
373
+ # ---------------- helpers ----------------
374
+
375
+ def _namespace(self, node: "Node") -> str | None:
376
+ # Block namespaces enclose their members; walk ancestors.
377
+ current = node.parent
378
+ while current is not None:
379
+ if current.type == "namespace_declaration":
380
+ name = current.child_by_field_name("name")
381
+ if name is not None:
382
+ return node_text(name)
383
+ current = current.parent
384
+ # File-scoped namespaces (C# 10, `namespace Foo;`) do NOT nest their
385
+ # members: the declaration is a sibling at compilation_unit level and
386
+ # applies to everything after it. Find it at the root.
387
+ root = node
388
+ while root.parent is not None:
389
+ root = root.parent
390
+ for child in root.children:
391
+ if child.type == "file_scoped_namespace_declaration":
392
+ name = child.child_by_field_name("name")
393
+ if name is not None:
394
+ return node_text(name)
395
+ return None
396
+
397
+ def _type_chain(self, node: "Node") -> list[str]:
398
+ parts, current = [], node.parent
399
+ while current is not None:
400
+ if current.type in _TYPE_SCOPES:
401
+ name = current.child_by_field_name("name")
402
+ if name is not None:
403
+ parts.append(node_text(name))
404
+ current = current.parent
405
+ parts.reverse()
406
+ return parts
407
+
408
+ def _base_prefix(self, node: "Node", ctx: FileContext) -> str:
409
+ ns = self._namespace(node)
410
+ return ns if ns is not None else ctx.module_path
411
+
412
+ def _qualify(
413
+ self, node: "Node", name: str, ctx: FileContext
414
+ ) -> tuple[str, str | None]:
415
+ prefix = self._base_prefix(node, ctx)
416
+ chain = self._type_chain(node)
417
+ parent_parts = [prefix, *chain]
418
+ parent_q = ".".join(p for p in parent_parts if p) or None
419
+ qname = ".".join(p for p in [*parent_parts, name] if p)
420
+ return qname, parent_q
421
+
422
+ def _nearest_type_scope(self, node: "Node") -> "Node | None":
423
+ current = node.parent
424
+ while current is not None:
425
+ if current.type in _TYPE_SCOPES:
426
+ return current
427
+ current = current.parent
428
+ return None
429
+
430
+ def _caller(self, node: "Node", ctx: FileContext) -> str | None:
431
+ current = node.parent
432
+ while current is not None:
433
+ if current.type in _CALLABLE_SCOPES:
434
+ name = current.child_by_field_name("name")
435
+ if name is not None:
436
+ return self._qualify(current, node_text(name), ctx)[0]
437
+ current = current.parent
438
+ return None
439
+
440
+ def _signature(self, node: "Node") -> str:
441
+ text = node_text(node)
442
+ for stop in ("{", "=>", ";"):
443
+ text = text.split(stop, 1)[0]
444
+ return truncate(text.strip(), 120)
445
+
446
+ def _modifiers(self, node: "Node") -> list[str]:
447
+ return [node_text(c) for c in node.children if c.type == "modifier"]
448
+
449
+ def _attribute_lists(self, node: "Node") -> list["Node"]:
450
+ return [c for c in node.children if c.type == "attribute_list"]
451
+
452
+ def _attributes(self, node: "Node") -> list[str]:
453
+ return [
454
+ "[" + node_text(attr) + "]"
455
+ for lst in self._attribute_lists(node)
456
+ for attr in lst.named_children
457
+ if attr.type == "attribute"
458
+ ]
459
+
460
+ def _emit_attribute_refs(
461
+ self, node: "Node", owner_qname: str, out: FileExtraction
462
+ ) -> None:
463
+ for lst in self._attribute_lists(node):
464
+ for attr in lst.named_children:
465
+ if attr.type != "attribute":
466
+ continue
467
+ name_node = attr.child_by_field_name("name") or next(
468
+ (
469
+ c
470
+ for c in attr.named_children
471
+ if c.type in ("identifier", "qualified_name")
472
+ ),
473
+ None,
474
+ )
475
+ if name_node is None:
476
+ continue
477
+ callee_text = node_text(name_node)
478
+ out.references.append(
479
+ RawReference(
480
+ kind="decorator",
481
+ callee_text=callee_text,
482
+ callee_name=callee_text.rsplit(".", 1)[-1],
483
+ receiver_text=callee_text.rsplit(".", 1)[0]
484
+ if "." in callee_text
485
+ else None,
486
+ span=span_of(attr),
487
+ caller_qualified_name=owner_qname,
488
+ )
489
+ )
490
+
491
+ def _base_types(self, node: "Node") -> list[str]:
492
+ base_list = next(
493
+ (c for c in node.named_children if c.type == "base_list"), None
494
+ )
495
+ if base_list is None:
496
+ return []
497
+ bases: list[str] = []
498
+ for child in base_list.named_children:
499
+ text = node_text(child).split("<", 1)[0].strip()
500
+ if text:
501
+ bases.append(text)
502
+ return bases