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.
- entrygraph/__init__.py +87 -0
- entrygraph/__main__.py +8 -0
- entrygraph/_version.py +24 -0
- entrygraph/api.py +549 -0
- entrygraph/cli/__init__.py +0 -0
- entrygraph/cli/main.py +387 -0
- entrygraph/cli/render.py +136 -0
- entrygraph/data/sinks/csharp.toml +106 -0
- entrygraph/data/sinks/go.toml +87 -0
- entrygraph/data/sinks/java.toml +92 -0
- entrygraph/data/sinks/javascript.toml +112 -0
- entrygraph/data/sinks/lib_javascript.toml +34 -0
- entrygraph/data/sinks/lib_python.toml +39 -0
- entrygraph/data/sinks/php.toml +125 -0
- entrygraph/data/sinks/python.toml +160 -0
- entrygraph/data/sinks/ruby.toml +102 -0
- entrygraph/data/sinks/rust.toml +68 -0
- entrygraph/db/__init__.py +0 -0
- entrygraph/db/engine.py +34 -0
- entrygraph/db/meta.py +70 -0
- entrygraph/db/models.py +176 -0
- entrygraph/db/queries.py +155 -0
- entrygraph/detect/__init__.py +0 -0
- entrygraph/detect/entrypoints/__init__.py +22 -0
- entrygraph/detect/entrypoints/base.py +124 -0
- entrygraph/detect/entrypoints/configs.py +139 -0
- entrygraph/detect/entrypoints/csharp.py +156 -0
- entrygraph/detect/entrypoints/golang.py +158 -0
- entrygraph/detect/entrypoints/java.py +187 -0
- entrygraph/detect/entrypoints/javascript.py +211 -0
- entrygraph/detect/entrypoints/php.py +133 -0
- entrygraph/detect/entrypoints/python.py +335 -0
- entrygraph/detect/entrypoints/ruby.py +147 -0
- entrygraph/detect/entrypoints/rust.py +153 -0
- entrygraph/detect/frameworks.py +369 -0
- entrygraph/detect/manifests.py +234 -0
- entrygraph/detect/taint.py +224 -0
- entrygraph/errors.py +27 -0
- entrygraph/extract/__init__.py +0 -0
- entrygraph/extract/base.py +51 -0
- entrygraph/extract/csharp.py +502 -0
- entrygraph/extract/golang.py +342 -0
- entrygraph/extract/ir.py +105 -0
- entrygraph/extract/java.py +329 -0
- entrygraph/extract/javascript.py +400 -0
- entrygraph/extract/php.py +426 -0
- entrygraph/extract/python.py +390 -0
- entrygraph/extract/registry.py +43 -0
- entrygraph/extract/ruby.py +321 -0
- entrygraph/extract/rust.py +482 -0
- entrygraph/fs/__init__.py +0 -0
- entrygraph/fs/hashing.py +78 -0
- entrygraph/fs/lang.py +134 -0
- entrygraph/fs/walker.py +167 -0
- entrygraph/graph/__init__.py +0 -0
- entrygraph/graph/adjacency.py +146 -0
- entrygraph/graph/cte.py +123 -0
- entrygraph/graph/scoring.py +101 -0
- entrygraph/kinds.py +51 -0
- entrygraph/parsing/__init__.py +0 -0
- entrygraph/parsing/parsers.py +49 -0
- entrygraph/parsing/queries.py +39 -0
- entrygraph/pipeline/__init__.py +0 -0
- entrygraph/pipeline/scanner.py +506 -0
- entrygraph/pipeline/worker.py +49 -0
- entrygraph/pipeline/writer.py +41 -0
- entrygraph/py.typed +0 -0
- entrygraph/queries/csharp/calls.scm +4 -0
- entrygraph/queries/csharp/definitions.scm +29 -0
- entrygraph/queries/csharp/imports.scm +4 -0
- entrygraph/queries/go/calls.scm +2 -0
- entrygraph/queries/go/definitions.scm +24 -0
- entrygraph/queries/go/imports.scm +1 -0
- entrygraph/queries/java/calls.scm +2 -0
- entrygraph/queries/java/definitions.scm +14 -0
- entrygraph/queries/java/imports.scm +2 -0
- entrygraph/queries/javascript/calls.scm +4 -0
- entrygraph/queries/javascript/definitions.scm +4 -0
- entrygraph/queries/javascript/imports.scm +6 -0
- entrygraph/queries/php/calls.scm +8 -0
- entrygraph/queries/php/definitions.scm +24 -0
- entrygraph/queries/php/imports.scm +1 -0
- entrygraph/queries/python/calls.scm +2 -0
- entrygraph/queries/python/definitions.scm +11 -0
- entrygraph/queries/python/imports.scm +2 -0
- entrygraph/queries/ruby/calls.scm +4 -0
- entrygraph/queries/ruby/definitions.scm +20 -0
- entrygraph/queries/ruby/imports.scm +7 -0
- entrygraph/queries/rust/calls.scm +5 -0
- entrygraph/queries/rust/definitions.scm +26 -0
- entrygraph/queries/rust/imports.scm +4 -0
- entrygraph/resolve/__init__.py +0 -0
- entrygraph/resolve/externals.py +61 -0
- entrygraph/resolve/hierarchy.py +152 -0
- entrygraph/resolve/resolver.py +275 -0
- entrygraph/resolve/symbol_table.py +48 -0
- entrygraph/results.py +138 -0
- entrygraph-0.1.0.dist-info/METADATA +204 -0
- entrygraph-0.1.0.dist-info/RECORD +102 -0
- entrygraph-0.1.0.dist-info/WHEEL +4 -0
- entrygraph-0.1.0.dist-info/entry_points.txt +2 -0
- entrygraph-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"""Go extractor: .scm queries harvest nodes, this shaper builds the IR.
|
|
2
|
+
|
|
3
|
+
Go qualified names are package-path based. The module path is the file's
|
|
4
|
+
directory (posix, ``/`` -> ``.``); the file stem is *not* part of the qname,
|
|
5
|
+
since Go symbols live at package scope. Methods carry their receiver type in
|
|
6
|
+
the qname (e.g. ``cmd.server.Server.Run``). Exported-ness in Go is the
|
|
7
|
+
capitalization of the first letter of the name.
|
|
8
|
+
|
|
9
|
+
Imports bind an alias into local scope: ``import "os/exec"`` binds ``exec``
|
|
10
|
+
(last path segment) -> module ``os/exec``; ``import f "fmt"`` binds ``f`` ->
|
|
11
|
+
``fmt``. All imports are treated as external for v1 (module kept as written),
|
|
12
|
+
and each import emits a framework signal so gin/echo/cobra/net-http detection
|
|
13
|
+
fires. A selector call ``exec.Command`` is then import-expanded by the shared
|
|
14
|
+
resolver to the external ``go:os/exec.Command`` that sink patterns match.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import TYPE_CHECKING, ClassVar
|
|
20
|
+
|
|
21
|
+
from entrygraph.extract.base import FileContext, node_text, span_of, truncate
|
|
22
|
+
from entrygraph.extract.ir import FileExtraction, RawImport, RawReference, RawSymbol
|
|
23
|
+
from entrygraph.kinds import SymbolKind
|
|
24
|
+
from entrygraph.parsing.queries import captures, load_query
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
27
|
+
from tree_sitter import Node, Tree
|
|
28
|
+
|
|
29
|
+
_SRC_ROOTS = ("src",)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _exported(name: str) -> bool:
|
|
33
|
+
return bool(name) and name[0].isupper()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class GoExtractor:
|
|
37
|
+
language_ids: ClassVar[tuple[str, ...]] = ("go",)
|
|
38
|
+
|
|
39
|
+
def module_path_for(self, repo_relative_path: str) -> tuple[bool | str, bool]:
|
|
40
|
+
parts = repo_relative_path.split("/")
|
|
41
|
+
if parts and parts[0] in _SRC_ROOTS and len(parts) > 1:
|
|
42
|
+
parts = parts[1:]
|
|
43
|
+
parts = parts[:-1] # drop the file name; Go symbols live at package scope
|
|
44
|
+
module = ".".join(p for p in parts if p) or "_root"
|
|
45
|
+
return module, False # Go packages are never treated as is_package here
|
|
46
|
+
|
|
47
|
+
def extract(self, tree: "Tree", ctx: FileContext) -> FileExtraction:
|
|
48
|
+
root = tree.root_node
|
|
49
|
+
out = FileExtraction(
|
|
50
|
+
path=ctx.path,
|
|
51
|
+
language=ctx.language,
|
|
52
|
+
module_path=ctx.module_path,
|
|
53
|
+
parse_ok=not root.has_error,
|
|
54
|
+
error_count=1 if root.has_error else 0,
|
|
55
|
+
)
|
|
56
|
+
self._extract_definitions(root, ctx, out)
|
|
57
|
+
self._extract_imports(root, ctx, out)
|
|
58
|
+
self._extract_calls(root, ctx, out)
|
|
59
|
+
return out
|
|
60
|
+
|
|
61
|
+
# ---------------- definitions ----------------
|
|
62
|
+
|
|
63
|
+
def _extract_definitions(self, root: "Node", ctx: FileContext, out: FileExtraction) -> None:
|
|
64
|
+
caps = captures(load_query("go", "definitions"), root)
|
|
65
|
+
|
|
66
|
+
for node in caps.get("def.function", []):
|
|
67
|
+
name_node = node.child_by_field_name("name")
|
|
68
|
+
if name_node is None:
|
|
69
|
+
continue
|
|
70
|
+
name = node_text(name_node)
|
|
71
|
+
out.symbols.append(
|
|
72
|
+
RawSymbol(
|
|
73
|
+
kind=SymbolKind.FUNCTION,
|
|
74
|
+
name=name,
|
|
75
|
+
qualified_name=f"{ctx.module_path}.{name}",
|
|
76
|
+
span=span_of(node),
|
|
77
|
+
parent_qualified_name=None,
|
|
78
|
+
signature=self._signature(node),
|
|
79
|
+
is_exported=_exported(name),
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
for node in caps.get("def.method", []):
|
|
84
|
+
name_node = node.child_by_field_name("name")
|
|
85
|
+
if name_node is None:
|
|
86
|
+
continue
|
|
87
|
+
name = node_text(name_node)
|
|
88
|
+
receiver = self._receiver_type(node)
|
|
89
|
+
if receiver:
|
|
90
|
+
parent_q = f"{ctx.module_path}.{receiver}"
|
|
91
|
+
qname = f"{parent_q}.{name}"
|
|
92
|
+
else:
|
|
93
|
+
parent_q = None
|
|
94
|
+
qname = f"{ctx.module_path}.{name}"
|
|
95
|
+
out.symbols.append(
|
|
96
|
+
RawSymbol(
|
|
97
|
+
kind=SymbolKind.METHOD,
|
|
98
|
+
name=name,
|
|
99
|
+
qualified_name=qname,
|
|
100
|
+
span=span_of(node),
|
|
101
|
+
parent_qualified_name=parent_q,
|
|
102
|
+
signature=self._signature(node),
|
|
103
|
+
is_exported=_exported(name),
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
for node in caps.get("def.struct", []):
|
|
108
|
+
self._add_type(node, ctx, out, SymbolKind.STRUCT)
|
|
109
|
+
for node in caps.get("def.interface", []):
|
|
110
|
+
self._add_type(node, ctx, out, SymbolKind.INTERFACE)
|
|
111
|
+
|
|
112
|
+
self._add_type_members(caps, ctx, out)
|
|
113
|
+
|
|
114
|
+
for node in caps.get("def.const", []):
|
|
115
|
+
self._add_value(node, ctx, out, SymbolKind.CONSTANT)
|
|
116
|
+
for node in caps.get("def.var", []):
|
|
117
|
+
self._add_value(node, ctx, out, SymbolKind.VARIABLE)
|
|
118
|
+
|
|
119
|
+
def _add_type(self, node: "Node", ctx: FileContext, out: FileExtraction, kind: SymbolKind) -> None:
|
|
120
|
+
# node is the type_declaration; the name is on its type_spec child.
|
|
121
|
+
spec = next((c for c in node.named_children if c.type == "type_spec"), None)
|
|
122
|
+
if spec is None:
|
|
123
|
+
return
|
|
124
|
+
name_node = spec.child_by_field_name("name")
|
|
125
|
+
if name_node is None:
|
|
126
|
+
return
|
|
127
|
+
name = node_text(name_node)
|
|
128
|
+
out.symbols.append(
|
|
129
|
+
RawSymbol(
|
|
130
|
+
kind=kind,
|
|
131
|
+
name=name,
|
|
132
|
+
qualified_name=f"{ctx.module_path}.{name}",
|
|
133
|
+
span=span_of(node),
|
|
134
|
+
parent_qualified_name=None,
|
|
135
|
+
signature=self._signature(node),
|
|
136
|
+
is_exported=_exported(name),
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def _add_type_members(self, caps, ctx: FileContext, out: FileExtraction) -> None:
|
|
141
|
+
for node in caps.get("def.field", []):
|
|
142
|
+
name_node = node.child_by_field_name("name")
|
|
143
|
+
if name_node is None:
|
|
144
|
+
continue
|
|
145
|
+
name = node_text(name_node)
|
|
146
|
+
owner = self._enclosing_type_name(node)
|
|
147
|
+
if owner is None:
|
|
148
|
+
continue
|
|
149
|
+
parent_q = f"{ctx.module_path}.{owner}"
|
|
150
|
+
out.symbols.append(
|
|
151
|
+
RawSymbol(
|
|
152
|
+
kind=SymbolKind.FIELD,
|
|
153
|
+
name=name,
|
|
154
|
+
qualified_name=f"{parent_q}.{name}",
|
|
155
|
+
span=span_of(node),
|
|
156
|
+
parent_qualified_name=parent_q,
|
|
157
|
+
signature=truncate(node_text(node)),
|
|
158
|
+
is_exported=_exported(name),
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
def _add_value(self, node: "Node", ctx: FileContext, out: FileExtraction, kind: SymbolKind) -> None:
|
|
163
|
+
# node is the const_declaration / var_declaration; names live on specs.
|
|
164
|
+
for spec in node.named_children:
|
|
165
|
+
if spec.type not in ("const_spec", "var_spec"):
|
|
166
|
+
continue
|
|
167
|
+
name_node = spec.child_by_field_name("name")
|
|
168
|
+
if name_node is None:
|
|
169
|
+
continue
|
|
170
|
+
name = node_text(name_node)
|
|
171
|
+
out.symbols.append(
|
|
172
|
+
RawSymbol(
|
|
173
|
+
kind=kind,
|
|
174
|
+
name=name,
|
|
175
|
+
qualified_name=f"{ctx.module_path}.{name}",
|
|
176
|
+
span=span_of(spec),
|
|
177
|
+
parent_qualified_name=None,
|
|
178
|
+
signature=truncate(node_text(spec)),
|
|
179
|
+
is_exported=_exported(name),
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# ---------------- imports ----------------
|
|
184
|
+
|
|
185
|
+
def _extract_imports(self, root: "Node", ctx: FileContext, out: FileExtraction) -> None:
|
|
186
|
+
caps = captures(load_query("go", "imports"), root)
|
|
187
|
+
for node in caps.get("import", []):
|
|
188
|
+
for spec in self._import_specs(node):
|
|
189
|
+
self._add_import(spec, node, out)
|
|
190
|
+
for imp in out.imports:
|
|
191
|
+
out.framework_signals.append(("import", imp.module))
|
|
192
|
+
|
|
193
|
+
def _import_specs(self, node: "Node") -> list["Node"]:
|
|
194
|
+
specs: list[Node] = []
|
|
195
|
+
for child in node.named_children:
|
|
196
|
+
if child.type == "import_spec":
|
|
197
|
+
specs.append(child)
|
|
198
|
+
elif child.type == "import_spec_list":
|
|
199
|
+
specs.extend(c for c in child.named_children if c.type == "import_spec")
|
|
200
|
+
return specs
|
|
201
|
+
|
|
202
|
+
def _add_import(self, spec: "Node", node: "Node", out: FileExtraction) -> None:
|
|
203
|
+
path_node = next(
|
|
204
|
+
(c for c in spec.named_children if c.type == "interpreted_string_literal"), None
|
|
205
|
+
)
|
|
206
|
+
if path_node is None:
|
|
207
|
+
return
|
|
208
|
+
module = node_text(path_node).strip('"')
|
|
209
|
+
alias_node = spec.child_by_field_name("name")
|
|
210
|
+
if alias_node is None:
|
|
211
|
+
alias_node = next(
|
|
212
|
+
(c for c in spec.named_children
|
|
213
|
+
if c.type in ("package_identifier", "identifier", "dot", "blank_identifier")),
|
|
214
|
+
None,
|
|
215
|
+
)
|
|
216
|
+
if alias_node is not None:
|
|
217
|
+
alias = node_text(alias_node)
|
|
218
|
+
else:
|
|
219
|
+
alias = module.rstrip("/").split("/")[-1]
|
|
220
|
+
out.imports.append(
|
|
221
|
+
RawImport(module=module, imported_name=None, alias=alias, span=span_of(spec))
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# ---------------- calls / composite literals ----------------
|
|
225
|
+
|
|
226
|
+
def _extract_calls(self, root: "Node", ctx: FileContext, out: FileExtraction) -> None:
|
|
227
|
+
caps = captures(load_query("go", "calls"), root)
|
|
228
|
+
|
|
229
|
+
for node in caps.get("call", []):
|
|
230
|
+
fn = node.child_by_field_name("function")
|
|
231
|
+
if fn is None:
|
|
232
|
+
continue
|
|
233
|
+
if fn.type == "identifier":
|
|
234
|
+
callee_text, callee_name, receiver = node_text(fn), node_text(fn), None
|
|
235
|
+
elif fn.type == "selector_expression":
|
|
236
|
+
operand = fn.child_by_field_name("operand")
|
|
237
|
+
field = fn.child_by_field_name("field")
|
|
238
|
+
if operand is None or field is None:
|
|
239
|
+
continue
|
|
240
|
+
callee_text, callee_name, receiver = node_text(fn), node_text(field), node_text(operand)
|
|
241
|
+
else:
|
|
242
|
+
continue # call of a call / index expr / etc. — not statically resolvable
|
|
243
|
+
args = node.child_by_field_name("arguments")
|
|
244
|
+
out.references.append(
|
|
245
|
+
RawReference(
|
|
246
|
+
kind="call",
|
|
247
|
+
callee_text=callee_text,
|
|
248
|
+
callee_name=callee_name,
|
|
249
|
+
receiver_text=receiver,
|
|
250
|
+
span=span_of(node),
|
|
251
|
+
caller_qualified_name=self._caller(node, ctx),
|
|
252
|
+
arg_count=len(args.named_children) if args is not None else 0,
|
|
253
|
+
arg_preview=truncate(node_text(args)) if args is not None else None,
|
|
254
|
+
)
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
for node in caps.get("composite.type", []):
|
|
258
|
+
text = node_text(node) # e.g. "cobra.Command"
|
|
259
|
+
out.references.append(
|
|
260
|
+
RawReference(
|
|
261
|
+
kind="composite",
|
|
262
|
+
callee_text=text,
|
|
263
|
+
callee_name=text.rsplit(".", 1)[-1],
|
|
264
|
+
receiver_text=text.rsplit(".", 1)[0] if "." in text else None,
|
|
265
|
+
span=span_of(node),
|
|
266
|
+
caller_qualified_name=self._caller(node, ctx),
|
|
267
|
+
)
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# ---------------- walking helpers ----------------
|
|
271
|
+
|
|
272
|
+
def _receiver_type(self, method: "Node") -> str | None:
|
|
273
|
+
"""The bare receiver type name of a method_declaration (deref pointers)."""
|
|
274
|
+
receiver = method.child_by_field_name("receiver")
|
|
275
|
+
if receiver is None:
|
|
276
|
+
# receiver is the first parameter_list child of the method_declaration
|
|
277
|
+
receiver = next((c for c in method.named_children if c.type == "parameter_list"), None)
|
|
278
|
+
if receiver is None:
|
|
279
|
+
return None
|
|
280
|
+
decl = next(
|
|
281
|
+
(c for c in receiver.named_children if c.type == "parameter_declaration"), None
|
|
282
|
+
)
|
|
283
|
+
if decl is None:
|
|
284
|
+
return None
|
|
285
|
+
type_node = decl.child_by_field_name("type")
|
|
286
|
+
if type_node is None:
|
|
287
|
+
type_node = next(
|
|
288
|
+
(c for c in decl.named_children
|
|
289
|
+
if c.type in ("type_identifier", "pointer_type", "generic_type")),
|
|
290
|
+
None,
|
|
291
|
+
)
|
|
292
|
+
return self._type_name(type_node)
|
|
293
|
+
|
|
294
|
+
def _type_name(self, node: "Node | None") -> str | None:
|
|
295
|
+
if node is None:
|
|
296
|
+
return None
|
|
297
|
+
if node.type == "type_identifier":
|
|
298
|
+
return node_text(node)
|
|
299
|
+
if node.type == "pointer_type":
|
|
300
|
+
inner = next(
|
|
301
|
+
(c for c in node.named_children if c.type in ("type_identifier", "generic_type")),
|
|
302
|
+
None,
|
|
303
|
+
)
|
|
304
|
+
return self._type_name(inner)
|
|
305
|
+
if node.type == "generic_type":
|
|
306
|
+
base = node.child_by_field_name("type") or (
|
|
307
|
+
node.named_children[0] if node.named_children else None
|
|
308
|
+
)
|
|
309
|
+
return self._type_name(base)
|
|
310
|
+
return None
|
|
311
|
+
|
|
312
|
+
def _enclosing_type_name(self, node: "Node") -> str | None:
|
|
313
|
+
"""Name of the type_spec that owns this field declaration."""
|
|
314
|
+
current = node.parent
|
|
315
|
+
while current is not None:
|
|
316
|
+
if current.type == "type_spec":
|
|
317
|
+
name = current.child_by_field_name("name")
|
|
318
|
+
return node_text(name) if name is not None else None
|
|
319
|
+
current = current.parent
|
|
320
|
+
return None
|
|
321
|
+
|
|
322
|
+
def _caller(self, node: "Node", ctx: FileContext) -> str | None:
|
|
323
|
+
"""FQN of the enclosing func/method, or None for package level."""
|
|
324
|
+
current = node.parent
|
|
325
|
+
while current is not None:
|
|
326
|
+
if current.type == "function_declaration":
|
|
327
|
+
name = current.child_by_field_name("name")
|
|
328
|
+
if name is not None:
|
|
329
|
+
return f"{ctx.module_path}.{node_text(name)}"
|
|
330
|
+
elif current.type == "method_declaration":
|
|
331
|
+
name = current.child_by_field_name("name")
|
|
332
|
+
if name is not None:
|
|
333
|
+
receiver = self._receiver_type(current)
|
|
334
|
+
if receiver:
|
|
335
|
+
return f"{ctx.module_path}.{receiver}.{node_text(name)}"
|
|
336
|
+
return f"{ctx.module_path}.{node_text(name)}"
|
|
337
|
+
current = current.parent
|
|
338
|
+
return None
|
|
339
|
+
|
|
340
|
+
def _signature(self, node: "Node") -> str:
|
|
341
|
+
first_line = node_text(node).split("\n", 1)[0].rstrip("{").strip()
|
|
342
|
+
return truncate(first_line, 120)
|
entrygraph/extract/ir.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""The extraction intermediate representation.
|
|
2
|
+
|
|
3
|
+
This is the contract between (a) parse workers and the main process, (b) the
|
|
4
|
+
resolver, and (c) the DB writer. Everything here is plain, slotted, and
|
|
5
|
+
pickle-cheap — never tree-sitter objects, which hold unpicklable C pointers.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
|
|
12
|
+
from entrygraph.kinds import EntrypointKind, SymbolKind
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(slots=True)
|
|
16
|
+
class Span:
|
|
17
|
+
start_line: int # 1-based
|
|
18
|
+
start_col: int
|
|
19
|
+
end_line: int
|
|
20
|
+
end_col: int
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(slots=True)
|
|
24
|
+
class RawSymbol:
|
|
25
|
+
kind: SymbolKind
|
|
26
|
+
name: str # "run"
|
|
27
|
+
qualified_name: str # "app.services.runner.Runner.run"
|
|
28
|
+
span: Span
|
|
29
|
+
parent_qualified_name: str | None = None # enclosing class/function FQN
|
|
30
|
+
signature: str | None = None # first line of the def, truncated
|
|
31
|
+
decorators: list[str] = field(default_factory=list) # raw source text
|
|
32
|
+
bases: list[str] = field(default_factory=list) # raw supertype expressions
|
|
33
|
+
modifiers: list[str] = field(default_factory=list) # static/async/exported...
|
|
34
|
+
docstring: str | None = None
|
|
35
|
+
is_exported: bool = True
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(slots=True)
|
|
39
|
+
class RawImport:
|
|
40
|
+
module: str # "subprocess", "./utils", "com.example.Foo"
|
|
41
|
+
imported_name: str | None # None = whole-module import; "*" = star import
|
|
42
|
+
alias: str # name bound in local scope: "sub", "run", "Foo"
|
|
43
|
+
span: Span
|
|
44
|
+
is_relative: bool = False
|
|
45
|
+
relative_level: int = 0 # Python: number of leading dots
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(slots=True)
|
|
49
|
+
class RawReference:
|
|
50
|
+
# "call" | "inherit" | "decorator" | "annotation"
|
|
51
|
+
# | "implement" (interface conformance) | "callback" (function name passed as
|
|
52
|
+
# an argument) | "dynamic_call" (getattr/computed/send — target unknowable)
|
|
53
|
+
kind: str
|
|
54
|
+
callee_text: str # full source text: "self.client.get"
|
|
55
|
+
callee_name: str # rightmost segment: "get"
|
|
56
|
+
receiver_text: str | None # "self.client" or None for bare calls
|
|
57
|
+
span: Span
|
|
58
|
+
caller_qualified_name: str | None # FQN of enclosing def; None = module level
|
|
59
|
+
arg_count: int = 0
|
|
60
|
+
arg_preview: str | None = None # truncated literal args, for sink triage
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(slots=True)
|
|
64
|
+
class RawReexport:
|
|
65
|
+
"""A `export { X } from "./y"` style re-export (barrel files).
|
|
66
|
+
|
|
67
|
+
Kept separate from RawImport so re-exported names never enter the local
|
|
68
|
+
import map — they are only followed when resolving *inbound* references that
|
|
69
|
+
land on this module.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
module: str # source module specifier: "./handler"
|
|
73
|
+
exported_name: str | None # local/exported name; None with is_star
|
|
74
|
+
alias: str | None # renamed export (`export { X as Y }`) or None
|
|
75
|
+
span: Span
|
|
76
|
+
is_star: bool = False # `export * from "..."`
|
|
77
|
+
is_relative: bool = False
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(slots=True)
|
|
81
|
+
class EntrypointHint:
|
|
82
|
+
rule_id: str # "python.flask.route"
|
|
83
|
+
kind: EntrypointKind
|
|
84
|
+
handler_qualified_name: str | None # None if handler is an inline lambda/expr
|
|
85
|
+
route: str | None = None # "/users/<id>"
|
|
86
|
+
http_methods: list[str] = field(default_factory=list)
|
|
87
|
+
name: str | None = None # CLI command name, task name
|
|
88
|
+
span: Span | None = None
|
|
89
|
+
framework: str | None = None
|
|
90
|
+
metadata: dict = field(default_factory=dict)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(slots=True)
|
|
94
|
+
class FileExtraction:
|
|
95
|
+
path: str # repo-relative
|
|
96
|
+
language: str
|
|
97
|
+
module_path: str # "app.services.runner" / "src/utils"
|
|
98
|
+
parse_ok: bool
|
|
99
|
+
error_count: int
|
|
100
|
+
symbols: list[RawSymbol] = field(default_factory=list)
|
|
101
|
+
imports: list[RawImport] = field(default_factory=list)
|
|
102
|
+
references: list[RawReference] = field(default_factory=list)
|
|
103
|
+
reexports: list[RawReexport] = field(default_factory=list)
|
|
104
|
+
entrypoint_hints: list[EntrypointHint] = field(default_factory=list)
|
|
105
|
+
framework_signals: list[tuple[str, str]] = field(default_factory=list) # (kind, value)
|