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,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)
@@ -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)