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,329 @@
1
+ """Java extractor: .scm queries harvest nodes, this shaper builds the IR.
2
+
3
+ Module paths are the fully-qualified class path — a leading ``src/main/java/``
4
+ or ``src/test/java/`` root is stripped, ``.java`` dropped, ``/`` -> ``.`` — so
5
+ ``src/main/java/com/example/UserController.java`` -> ``com.example.UserController``.
6
+ Java has no package concept in the IR sense (one type per module path), so
7
+ ``is_package`` is always False.
8
+
9
+ Annotations are captured on ``RawSymbol.decorators`` as raw source text
10
+ (``@GetMapping("/users")``) and also emitted as ``RawReference(kind="decorator")``
11
+ so entrypoint rules and the resolver can match them. Supertypes/interfaces are
12
+ emitted as ``RawReference(kind="inherit")``. External calls with unknown
13
+ receivers (``Runtime.getRuntime().exec(...)``) resolve to ``java:*.exec``.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from typing import TYPE_CHECKING, ClassVar
19
+
20
+ from entrygraph.extract.base import FileContext, node_text, span_of, truncate
21
+ from entrygraph.extract.ir import FileExtraction, RawImport, RawReference, RawSymbol
22
+ from entrygraph.kinds import SymbolKind
23
+ from entrygraph.parsing.queries import captures, load_query
24
+
25
+ if TYPE_CHECKING: # pragma: no cover
26
+ from tree_sitter import Node, Tree
27
+
28
+ _SCOPE_TYPES = frozenset({"class_declaration", "interface_declaration"})
29
+ _ANNOTATION_TYPES = frozenset({"marker_annotation", "annotation"})
30
+ _JAVA_ROOTS = ("src/main/java/", "src/test/java/")
31
+
32
+
33
+ class JavaExtractor:
34
+ language_ids: ClassVar[tuple[str, ...]] = ("java",)
35
+
36
+ def module_path_for(self, repo_relative_path: str) -> tuple[str, bool]:
37
+ path = repo_relative_path
38
+ for root in _JAVA_ROOTS:
39
+ if path.startswith(root):
40
+ path = path[len(root):]
41
+ break
42
+ path = path.removesuffix(".java")
43
+ return ".".join(p for p in path.split("/") if p) or "_root", False
44
+
45
+ def extract(self, tree: "Tree", ctx: FileContext) -> FileExtraction:
46
+ root = tree.root_node
47
+ out = FileExtraction(
48
+ path=ctx.path, language=ctx.language, module_path=ctx.module_path,
49
+ parse_ok=not root.has_error, error_count=1 if root.has_error else 0,
50
+ )
51
+ self._definitions(root, ctx, out)
52
+ self._imports(root, ctx, out)
53
+ self._calls(root, ctx, out)
54
+ return out
55
+
56
+ # ---------------- definitions ----------------
57
+
58
+ def _definitions(self, root: "Node", ctx: FileContext, out: FileExtraction) -> None:
59
+ caps = captures(load_query("java", "definitions"), root)
60
+
61
+ for node in caps.get("def.class", []):
62
+ self._add_type(node, ctx, out, SymbolKind.CLASS)
63
+ for node in caps.get("def.interface", []):
64
+ self._add_type(node, ctx, out, SymbolKind.INTERFACE)
65
+
66
+ for node in caps.get("def.method", []):
67
+ name_node = node.child_by_field_name("name")
68
+ if name_node is None:
69
+ continue
70
+ name = node_text(name_node)
71
+ qname, parent_q = self._qualify(node, name, ctx)
72
+ modifiers = self._modifiers(node)
73
+ annotations = self._annotations(node)
74
+ out.symbols.append(
75
+ RawSymbol(
76
+ kind=SymbolKind.METHOD, name=name, qualified_name=qname,
77
+ span=span_of(node), parent_qualified_name=parent_q,
78
+ signature=self._signature(node),
79
+ decorators=annotations,
80
+ modifiers=modifiers,
81
+ is_exported="public" in modifiers,
82
+ )
83
+ )
84
+ self._emit_annotation_refs(node, qname, out)
85
+
86
+ for node in caps.get("def.field", []):
87
+ scope = self._nearest_scope(node)
88
+ if scope is None:
89
+ continue # only class/interface members are symbols
90
+ declarator = node.child_by_field_name("declarator")
91
+ if declarator is None:
92
+ continue
93
+ name_node = declarator.child_by_field_name("name")
94
+ if name_node is None:
95
+ continue
96
+ name = node_text(name_node)
97
+ qname, parent_q = self._qualify(node, name, ctx)
98
+ modifiers = self._modifiers(node)
99
+ is_const = "static" in modifiers and "final" in modifiers and name.isupper()
100
+ out.symbols.append(
101
+ RawSymbol(
102
+ kind=SymbolKind.CONSTANT if is_const else SymbolKind.FIELD,
103
+ name=name, qualified_name=qname, span=span_of(node),
104
+ parent_qualified_name=parent_q,
105
+ signature=truncate(node_text(node)),
106
+ decorators=self._annotations(node),
107
+ modifiers=modifiers,
108
+ is_exported="public" in modifiers,
109
+ )
110
+ )
111
+
112
+ def _add_type(self, node: "Node", ctx: FileContext, out: FileExtraction, kind: SymbolKind) -> None:
113
+ name_node = node.child_by_field_name("name")
114
+ if name_node is None:
115
+ return
116
+ name = node_text(name_node)
117
+ qname, parent_q = self._qualify(node, name, ctx)
118
+ extends, interfaces = self._supertypes(node)
119
+ modifiers = self._modifiers(node)
120
+ out.symbols.append(
121
+ RawSymbol(
122
+ kind=kind, name=name, qualified_name=qname, span=span_of(node),
123
+ parent_qualified_name=parent_q, signature=self._signature(node),
124
+ decorators=self._annotations(node), bases=[*extends, *interfaces],
125
+ modifiers=modifiers, is_exported="public" in modifiers,
126
+ )
127
+ )
128
+ for base in extends:
129
+ out.references.append(
130
+ RawReference(
131
+ kind="inherit", callee_text=base, callee_name=base.rsplit(".", 1)[-1],
132
+ receiver_text=None, span=span_of(node), caller_qualified_name=qname,
133
+ )
134
+ )
135
+ for iface in interfaces:
136
+ out.references.append(
137
+ RawReference(
138
+ kind="implement", callee_text=iface, callee_name=iface.rsplit(".", 1)[-1],
139
+ receiver_text=None, span=span_of(node), caller_qualified_name=qname,
140
+ )
141
+ )
142
+ self._emit_annotation_refs(node, qname, out)
143
+
144
+ # ---------------- imports ----------------
145
+
146
+ def _imports(self, root: "Node", ctx: FileContext, out: FileExtraction) -> None:
147
+ caps = captures(load_query("java", "imports"), root)
148
+ for node in caps.get("import", []):
149
+ scoped = next((c for c in node.named_children if c.type == "scoped_identifier"), None)
150
+ if scoped is None:
151
+ continue
152
+ full = node_text(scoped)
153
+ wildcard = any(c.type == "asterisk" for c in node.named_children)
154
+ if wildcard:
155
+ out.imports.append(
156
+ RawImport(module=full, imported_name="*", alias="*", span=span_of(node))
157
+ )
158
+ else:
159
+ last = full.rsplit(".", 1)[-1]
160
+ out.imports.append(
161
+ RawImport(module=full, imported_name=last, alias=last, span=span_of(node))
162
+ )
163
+ for imp in out.imports:
164
+ if imp.module:
165
+ out.framework_signals.append(("import", imp.module))
166
+
167
+ # ---------------- calls / annotations ----------------
168
+
169
+ def _calls(self, root: "Node", ctx: FileContext, out: FileExtraction) -> None:
170
+ caps = captures(load_query("java", "calls"), root)
171
+
172
+ for node in caps.get("call", []):
173
+ name_node = node.child_by_field_name("name")
174
+ if name_node is None:
175
+ continue
176
+ callee_name = node_text(name_node)
177
+ obj = node.child_by_field_name("object")
178
+ if obj is not None:
179
+ receiver = node_text(obj)
180
+ callee_text = f"{receiver}.{callee_name}"
181
+ else:
182
+ receiver = None
183
+ callee_text = callee_name
184
+ args = node.child_by_field_name("arguments")
185
+ out.references.append(
186
+ RawReference(
187
+ kind="call", callee_text=callee_text, callee_name=callee_name,
188
+ receiver_text=receiver, span=span_of(node),
189
+ caller_qualified_name=self._caller(node, ctx),
190
+ arg_count=len(args.named_children) if args is not None else 0,
191
+ arg_preview=truncate(node_text(args)) if args is not None else None,
192
+ )
193
+ )
194
+
195
+ for node in caps.get("new", []):
196
+ type_node = node.child_by_field_name("type")
197
+ if type_node is None:
198
+ continue
199
+ type_text = node_text(type_node).split("<", 1)[0].strip()
200
+ callee_name = type_text.rsplit(".", 1)[-1]
201
+ args = node.child_by_field_name("arguments")
202
+ out.references.append(
203
+ RawReference(
204
+ kind="call", callee_text=type_text, callee_name=callee_name,
205
+ receiver_text=None, span=span_of(node),
206
+ caller_qualified_name=self._caller(node, ctx),
207
+ arg_count=len(args.named_children) if args is not None else 0,
208
+ arg_preview=truncate(node_text(args)) if args is not None else None,
209
+ )
210
+ )
211
+
212
+ # ---------------- helpers ----------------
213
+
214
+ def _scope_chain(self, node: "Node", ctx: FileContext) -> list[str]:
215
+ parts, current = [], node.parent
216
+ while current is not None:
217
+ if current.type in _SCOPE_TYPES:
218
+ name = current.child_by_field_name("name")
219
+ if name is not None:
220
+ parts.append(node_text(name))
221
+ current = current.parent
222
+ parts.reverse()
223
+ # The conventional top-level public type shares the file's stem; the
224
+ # module path already encodes it (com.example.UserController), so drop
225
+ # the duplicate leading segment to avoid ...UserController.UserController.
226
+ top = ctx.module_path.rsplit(".", 1)[-1]
227
+ if parts and parts[0] == top:
228
+ parts = parts[1:]
229
+ return parts
230
+
231
+ def _nearest_scope(self, node: "Node") -> str | None:
232
+ current = node.parent
233
+ while current is not None:
234
+ if current.type in _SCOPE_TYPES:
235
+ return current.type
236
+ current = current.parent
237
+ return None
238
+
239
+ def _qualify(self, node: "Node", name: str, ctx: FileContext) -> tuple[str, str | None]:
240
+ chain = self._scope_chain(node, ctx)
241
+ top = ctx.module_path.rsplit(".", 1)[-1]
242
+ # The top-level type named after the file collapses onto the module path
243
+ # itself (com.example.UserController), so its members qualify cleanly as
244
+ # com.example.UserController.getUser and parent to that same qname.
245
+ if not chain and name == top:
246
+ # this node *is* the top-level type
247
+ return ctx.module_path, None
248
+ enclosed_in_top = self._enclosed_in_top_type(node, top)
249
+ parent_q = ".".join([ctx.module_path, *chain]) if (chain or enclosed_in_top) else None
250
+ return ".".join([ctx.module_path, *chain, name]), parent_q
251
+
252
+ def _enclosed_in_top_type(self, node: "Node", top: str) -> bool:
253
+ current = node.parent
254
+ while current is not None:
255
+ if current.type in _SCOPE_TYPES:
256
+ name = current.child_by_field_name("name")
257
+ return name is not None and node_text(name) == top
258
+ current = current.parent
259
+ return False
260
+
261
+ def _caller(self, node: "Node", ctx: FileContext) -> str | None:
262
+ current = node.parent
263
+ while current is not None:
264
+ if current.type == "method_declaration":
265
+ name = current.child_by_field_name("name")
266
+ if name is not None:
267
+ return self._qualify(current, node_text(name), ctx)[0]
268
+ current = current.parent
269
+ return None
270
+
271
+ def _signature(self, node: "Node") -> str:
272
+ return truncate(node_text(node).split("{", 1)[0].strip(), 120)
273
+
274
+ def _modifiers_node(self, node: "Node") -> "Node | None":
275
+ return next((c for c in node.named_children if c.type == "modifiers"), None)
276
+
277
+ def _modifiers(self, node: "Node") -> list[str]:
278
+ mods = self._modifiers_node(node)
279
+ if mods is None:
280
+ return []
281
+ return [
282
+ node_text(c) for c in mods.children
283
+ if c.type not in _ANNOTATION_TYPES and node_text(c).strip()
284
+ ]
285
+
286
+ def _annotations(self, node: "Node") -> list[str]:
287
+ mods = self._modifiers_node(node)
288
+ if mods is None:
289
+ return []
290
+ return [node_text(c) for c in mods.children if c.type in _ANNOTATION_TYPES]
291
+
292
+ def _emit_annotation_refs(self, node: "Node", owner_qname: str, out: FileExtraction) -> None:
293
+ mods = self._modifiers_node(node)
294
+ if mods is None:
295
+ return
296
+ for child in mods.children:
297
+ if child.type not in _ANNOTATION_TYPES:
298
+ continue
299
+ name_node = child.child_by_field_name("name")
300
+ if name_node is None:
301
+ continue
302
+ callee_text = node_text(name_node)
303
+ out.references.append(
304
+ RawReference(
305
+ kind="decorator", callee_text=callee_text,
306
+ callee_name=callee_text.rsplit(".", 1)[-1],
307
+ receiver_text=callee_text.rsplit(".", 1)[0] if "." in callee_text else None,
308
+ span=span_of(child), caller_qualified_name=owner_qname,
309
+ )
310
+ )
311
+
312
+ def _supertypes(self, node: "Node") -> tuple[list[str], list[str]]:
313
+ """Return (extends supertypes, implements interfaces)."""
314
+ extends: list[str] = []
315
+ superclass = node.child_by_field_name("superclass")
316
+ if superclass is not None:
317
+ for child in superclass.named_children:
318
+ text = node_text(child).split("<", 1)[0].strip()
319
+ if text:
320
+ extends.append(text)
321
+ interfaces: list[str] = []
322
+ iface_node = node.child_by_field_name("interfaces")
323
+ if iface_node is not None:
324
+ for type_list in iface_node.named_children:
325
+ for child in type_list.named_children:
326
+ text = node_text(child).split("<", 1)[0].strip()
327
+ if text:
328
+ interfaces.append(text)
329
+ return extends, interfaces