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,400 @@
1
+ """JavaScript / TypeScript / TSX extractor.
2
+
3
+ One extractor drives three grammars. Module paths are dotted (path minus
4
+ extension, ``/`` -> ``.``) and relative imports are pre-expanded to dotted
5
+ project modules here, so the shared (Python-oriented) resolver treats them
6
+ uniformly. ``this`` is handled as a self-receiver by the resolver.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import TYPE_CHECKING, ClassVar
12
+
13
+ from entrygraph.extract.base import FileContext, node_text, span_of, truncate
14
+ from entrygraph.extract.ir import (
15
+ FileExtraction,
16
+ RawImport,
17
+ RawReexport,
18
+ RawReference,
19
+ RawSymbol,
20
+ )
21
+ from entrygraph.kinds import SymbolKind
22
+ from entrygraph.parsing.queries import captures, load_query_for
23
+
24
+ if TYPE_CHECKING: # pragma: no cover
25
+ from tree_sitter import Node, Tree
26
+
27
+ _SCOPE_TYPES = frozenset(
28
+ {"function_declaration", "method_definition", "class_declaration",
29
+ "function_expression", "arrow_function"}
30
+ )
31
+ _NAMED_SCOPES = frozenset({"function_declaration", "method_definition", "class_declaration"})
32
+ _SRC_ROOTS = ("src", "lib", "app")
33
+ _EXPORT_PARENTS = frozenset({"export_statement"})
34
+
35
+
36
+ class JavaScriptExtractor:
37
+ language_ids: ClassVar[tuple[str, ...]] = ("javascript", "typescript", "tsx")
38
+
39
+ def module_path_for(self, repo_relative_path: str) -> tuple[str, bool]:
40
+ parts = repo_relative_path.split("/")
41
+ if parts[0] in _SRC_ROOTS and len(parts) > 1:
42
+ parts = parts[1:]
43
+ stem = parts[-1]
44
+ for ext in (".d.ts", ".ts", ".tsx", ".mts", ".cts", ".js", ".mjs", ".cjs", ".jsx"):
45
+ if stem.endswith(ext):
46
+ stem = stem[: -len(ext)]
47
+ break
48
+ is_package = stem in ("index", "mod")
49
+ parts = parts[:-1] if is_package else [*parts[:-1], stem]
50
+ return ".".join(p for p in parts if p) or "_root", is_package
51
+
52
+ def extract(self, tree: "Tree", ctx: FileContext) -> FileExtraction:
53
+ root = tree.root_node
54
+ out = FileExtraction(
55
+ path=ctx.path, language=ctx.language, module_path=ctx.module_path,
56
+ parse_ok=not root.has_error, error_count=1 if root.has_error else 0,
57
+ )
58
+ self._definitions(root, ctx, out)
59
+ self._imports(root, ctx, out)
60
+ self._calls(root, ctx, out)
61
+ return out
62
+
63
+ # ---------------- definitions ----------------
64
+
65
+ def _definitions(self, root, ctx, out) -> None:
66
+ caps = captures(load_query_for(ctx.language, "javascript", "definitions"), root)
67
+
68
+ for node in caps.get("def.function", []):
69
+ self._add_callable(node, ctx, out, SymbolKind.FUNCTION)
70
+ for node in caps.get("def.method", []):
71
+ self._add_callable(node, ctx, out, SymbolKind.METHOD)
72
+
73
+ for node in caps.get("def.class", []):
74
+ name_node = node.child_by_field_name("name")
75
+ if name_node is None:
76
+ continue
77
+ name = node_text(name_node)
78
+ qname, parent_q = self._qualify(node, name, ctx)
79
+ bases, interfaces = self._heritage(node)
80
+ out.symbols.append(
81
+ RawSymbol(kind=SymbolKind.CLASS, name=name, qualified_name=qname,
82
+ span=span_of(node), parent_qualified_name=parent_q,
83
+ signature=self._signature(node), bases=bases,
84
+ decorators=self._decorators(node),
85
+ is_exported=self._exported(node))
86
+ )
87
+ for base in bases:
88
+ out.references.append(
89
+ RawReference(kind="inherit", callee_text=base,
90
+ callee_name=base.rsplit(".", 1)[-1], receiver_text=None,
91
+ span=span_of(node), caller_qualified_name=qname)
92
+ )
93
+ for iface in interfaces: # TS `implements C, D`
94
+ out.references.append(
95
+ RawReference(kind="implement", callee_text=iface,
96
+ callee_name=iface.rsplit(".", 1)[-1], receiver_text=None,
97
+ span=span_of(node), caller_qualified_name=qname)
98
+ )
99
+
100
+ for node in caps.get("def.var", []):
101
+ scope = self._nearest_named_scope(node)
102
+ if scope in ("function_declaration", "method_definition",
103
+ "function_expression", "arrow_function"):
104
+ continue # locals aren't symbols
105
+ name_node = node.child_by_field_name("name")
106
+ value = node.child_by_field_name("value")
107
+ if name_node is None:
108
+ continue
109
+ # a `const x = () => {}` / function expression is a callable symbol
110
+ if value is not None and value.type in ("arrow_function", "function_expression"):
111
+ self._add_named_callable(node, name_node, ctx, out)
112
+ continue
113
+ name = node_text(name_node)
114
+ qname, parent_q = self._qualify(node, name, ctx)
115
+ out.symbols.append(
116
+ RawSymbol(kind=SymbolKind.VARIABLE, name=name, qualified_name=qname,
117
+ span=span_of(node), parent_qualified_name=parent_q,
118
+ signature=truncate(node_text(node)), is_exported=self._exported(node))
119
+ )
120
+
121
+ def _add_callable(self, node, ctx, out, kind) -> None:
122
+ name_node = node.child_by_field_name("name")
123
+ if name_node is None:
124
+ return
125
+ self._add_named_callable(node, name_node, ctx, out, kind)
126
+
127
+ def _add_named_callable(self, node, name_node, ctx, out, kind=SymbolKind.FUNCTION) -> None:
128
+ name = node_text(name_node)
129
+ qname, parent_q = self._qualify(node, name, ctx)
130
+ if kind is SymbolKind.FUNCTION and self._nearest_named_scope(node) == "class_declaration":
131
+ kind = SymbolKind.METHOD
132
+ out.symbols.append(
133
+ RawSymbol(kind=kind, name=name, qualified_name=qname, span=span_of(node),
134
+ parent_qualified_name=parent_q, signature=self._signature(node),
135
+ decorators=self._decorators(node), is_exported=self._exported(node))
136
+ )
137
+
138
+ # ---------------- imports ----------------
139
+
140
+ def _imports(self, root, ctx, out) -> None:
141
+ caps = captures(load_query_for(ctx.language, "javascript", "imports"), root)
142
+ for node in caps.get("import", []):
143
+ source = node.child_by_field_name("source")
144
+ if source is None:
145
+ continue
146
+ module = self._resolve_module(node_text(source).strip("'\"`"), ctx)
147
+ clause = next((c for c in node.named_children if c.type == "import_clause"), None)
148
+ if clause is None:
149
+ out.imports.append(RawImport(module=module, imported_name=None,
150
+ alias=module.split(".")[-1], span=span_of(node)))
151
+ continue
152
+ for child in clause.named_children:
153
+ if child.type == "identifier": # default import
154
+ # Bind the alias to the module itself (CommonJS/esModuleInterop
155
+ # semantics) so `import cp from 'child_process'; cp.exec()`
156
+ # canonicalizes to child_process.exec, not .default.exec.
157
+ out.imports.append(RawImport(module=module, imported_name=None,
158
+ alias=node_text(child), span=span_of(node)))
159
+ elif child.type == "namespace_import":
160
+ ident = child.named_children[-1]
161
+ out.imports.append(RawImport(module=module, imported_name="*",
162
+ alias=node_text(ident), span=span_of(node)))
163
+ elif child.type == "named_imports":
164
+ for spec in child.named_children:
165
+ if spec.type != "import_specifier":
166
+ continue
167
+ name_node = spec.child_by_field_name("name")
168
+ alias_node = spec.child_by_field_name("alias")
169
+ if name_node is None:
170
+ continue
171
+ name = node_text(name_node)
172
+ out.imports.append(
173
+ RawImport(module=module, imported_name=name,
174
+ alias=node_text(alias_node) if alias_node else name,
175
+ span=span_of(node))
176
+ )
177
+ for node in caps.get("export.from", []):
178
+ self._reexport(node, ctx, out)
179
+
180
+ for imp in out.imports:
181
+ out.framework_signals.append(("import", imp.module.split(".")[0] if "." in imp.module else imp.module))
182
+
183
+ def _reexport(self, node, ctx, out) -> None:
184
+ """`export { X as Y } from "./m"` / `export * from "./m"` -> RawReexport."""
185
+ source = node.child_by_field_name("source")
186
+ if source is None:
187
+ return
188
+ spec = node_text(source).strip("'\"`")
189
+ module = self._resolve_module(spec, ctx)
190
+ is_relative = spec.startswith(".")
191
+ clause = next((c for c in node.named_children if c.type == "export_clause"), None)
192
+ if clause is None: # `export * from "..."`
193
+ out.reexports.append(RawReexport(module=module, exported_name=None, alias=None,
194
+ span=span_of(node), is_star=True,
195
+ is_relative=is_relative))
196
+ return
197
+ for spec_node in clause.named_children:
198
+ if spec_node.type != "export_specifier":
199
+ continue
200
+ name_node = spec_node.child_by_field_name("name")
201
+ alias_node = spec_node.child_by_field_name("alias")
202
+ if name_node is None:
203
+ continue
204
+ out.reexports.append(
205
+ RawReexport(module=module, exported_name=node_text(name_node),
206
+ alias=node_text(alias_node) if alias_node else None,
207
+ span=span_of(node), is_relative=is_relative)
208
+ )
209
+
210
+ def _resolve_module(self, spec: str, ctx: FileContext) -> str:
211
+ if not spec.startswith("."):
212
+ return spec # package import: keep as written (external)
213
+ parts = ctx.module_path.split(".")
214
+ parts = parts if ctx.is_package else parts[:-1]
215
+ for segment in spec.split("/"):
216
+ if segment in ("", "."):
217
+ continue
218
+ if segment == "..":
219
+ parts = parts[:-1]
220
+ else:
221
+ seg = segment
222
+ for ext in (".ts", ".tsx", ".js", ".jsx"):
223
+ if seg.endswith(ext):
224
+ seg = seg[: -len(ext)]
225
+ if seg not in ("index", "mod"):
226
+ parts.append(seg)
227
+ return ".".join(p for p in parts if p) or "_root"
228
+
229
+ # ---------------- calls ----------------
230
+
231
+ def _calls(self, root, ctx, out) -> None:
232
+ caps = captures(load_query_for(ctx.language, "javascript", "calls"), root)
233
+ for node in caps.get("call", []):
234
+ fn = node.child_by_field_name("function")
235
+ if fn is None:
236
+ continue
237
+ args = node.child_by_field_name("arguments")
238
+ caller = self._caller(node, ctx)
239
+ if fn.type == "identifier":
240
+ callee_text, callee_name, receiver = node_text(fn), node_text(fn), None
241
+ elif fn.type == "member_expression":
242
+ obj = fn.child_by_field_name("object")
243
+ prop = fn.child_by_field_name("property")
244
+ if obj is None or prop is None:
245
+ continue
246
+ callee_text, callee_name, receiver = node_text(fn), node_text(prop), node_text(obj)
247
+ elif fn.type == "subscript_expression":
248
+ # obj[name]() — computed member call, target unknowable
249
+ self._emit_dynamic_call(node, fn, caller, out)
250
+ self._emit_callbacks(args, caller, out)
251
+ continue
252
+ else:
253
+ continue
254
+ out.references.append(
255
+ RawReference(kind="call", callee_text=callee_text, callee_name=callee_name,
256
+ receiver_text=receiver, span=span_of(node),
257
+ caller_qualified_name=caller,
258
+ arg_count=len(args.named_children) if args else 0,
259
+ arg_preview=truncate(node_text(args)) if args else None)
260
+ )
261
+ self._emit_callbacks(args, caller, out)
262
+
263
+ def _emit_dynamic_call(self, node, fn, caller, out) -> None:
264
+ args = node.child_by_field_name("arguments")
265
+ out.references.append(
266
+ RawReference(kind="dynamic_call", callee_text=node_text(fn),
267
+ callee_name="<computed>", receiver_text=None, span=span_of(node),
268
+ caller_qualified_name=caller,
269
+ arg_count=len(args.named_children) if args else 0,
270
+ arg_preview=truncate(node_text(args)) if args else None)
271
+ )
272
+
273
+ def _emit_callbacks(self, args, caller, out) -> None:
274
+ """Bare identifier / simple member args passed to a call (`setTimeout(fn)`)."""
275
+ if args is None:
276
+ return
277
+ for arg in args.named_children:
278
+ if arg.type == "identifier":
279
+ name = node_text(arg)
280
+ elif arg.type == "member_expression":
281
+ prop = arg.child_by_field_name("property")
282
+ if prop is None:
283
+ continue
284
+ name = node_text(prop)
285
+ else:
286
+ continue
287
+ out.references.append(
288
+ RawReference(kind="callback", callee_text=node_text(arg), callee_name=name,
289
+ receiver_text=None, span=span_of(arg), caller_qualified_name=caller)
290
+ )
291
+
292
+ # ---------------- helpers ----------------
293
+
294
+ def _scope_chain(self, node) -> list[str]:
295
+ parts, current = [], node.parent
296
+ while current is not None:
297
+ if current.type in _NAMED_SCOPES:
298
+ name = current.child_by_field_name("name")
299
+ if name is not None:
300
+ parts.append(node_text(name))
301
+ current = current.parent
302
+ parts.reverse()
303
+ return parts
304
+
305
+ def _nearest_named_scope(self, node) -> str | None:
306
+ current = node.parent
307
+ while current is not None:
308
+ if current.type in _SCOPE_TYPES:
309
+ return current.type
310
+ current = current.parent
311
+ return None
312
+
313
+ def _qualify(self, node, name, ctx) -> tuple[str, str | None]:
314
+ chain = self._scope_chain(node)
315
+ parent_q = ".".join([ctx.module_path, *chain]) if chain else None
316
+ return ".".join([ctx.module_path, *chain, name]), parent_q
317
+
318
+ def _caller(self, node, ctx) -> str | None:
319
+ current = node.parent
320
+ while current is not None:
321
+ if current.type in ("function_declaration", "method_definition"):
322
+ name = current.child_by_field_name("name")
323
+ if name is not None:
324
+ return self._qualify(current, node_text(name), ctx)[0]
325
+ if current.type == "variable_declarator":
326
+ value = current.child_by_field_name("value")
327
+ if value is not None and value.type in ("arrow_function", "function_expression"):
328
+ name = current.child_by_field_name("name")
329
+ if name is not None:
330
+ return self._qualify(current, node_text(name), ctx)[0]
331
+ current = current.parent
332
+ return None
333
+
334
+ def _signature(self, node) -> str:
335
+ return truncate(node_text(node).split("\n", 1)[0].rstrip("{").strip(), 120)
336
+
337
+ def _decorators(self, node) -> list[str]:
338
+ """Raw text of `decorator` nodes immediately preceding `node`.
339
+
340
+ Decorators are siblings that appear just before their target — class
341
+ decorators inside the enclosing `export_statement`/`program`, method
342
+ decorators inside the `class_body`. A non-decorator sibling resets the
343
+ run, so each target only picks up its own decorators.
344
+ """
345
+ parent = node.parent
346
+ if parent is None:
347
+ return []
348
+ decs: list[str] = []
349
+ for child in parent.named_children:
350
+ if child.start_byte == node.start_byte and child.end_byte == node.end_byte:
351
+ return decs
352
+ if child.type == "decorator":
353
+ decs.append(node_text(child))
354
+ else:
355
+ decs = []
356
+ return decs
357
+
358
+ def _heritage(self, node) -> tuple[list[str], list[str]]:
359
+ """Return (extends bases, implements interfaces).
360
+
361
+ Plain JS puts identifiers directly under `class_heritage`; TS wraps them
362
+ in `extends_clause` / `implements_clause`.
363
+ """
364
+ heritage = next((c for c in node.named_children if c.type == "class_heritage"), None)
365
+ if heritage is None:
366
+ return [], []
367
+ bases: list[str] = []
368
+ interfaces: list[str] = []
369
+
370
+ def _names(container) -> list[str]:
371
+ out = []
372
+ for child in container.named_children:
373
+ if child.type in ("identifier", "member_expression", "type_identifier",
374
+ "generic_type", "nested_type_identifier"):
375
+ text = node_text(child).split("<", 1)[0].strip()
376
+ if text:
377
+ out.append(text)
378
+ return out
379
+
380
+ saw_clause = False
381
+ for child in heritage.named_children:
382
+ if child.type == "extends_clause":
383
+ saw_clause = True
384
+ bases.extend(_names(child))
385
+ elif child.type == "implements_clause":
386
+ saw_clause = True
387
+ interfaces.extend(_names(child))
388
+ if not saw_clause: # plain JS: identifiers directly under class_heritage
389
+ bases.extend(_names(heritage))
390
+ return bases, interfaces
391
+
392
+ def _exported(self, node) -> bool:
393
+ current = node.parent
394
+ while current is not None:
395
+ if current.type in _EXPORT_PARENTS:
396
+ return True
397
+ if current.type in ("program", "statement_block", "class_body"):
398
+ break
399
+ current = current.parent
400
+ return False