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,187 @@
1
+ """Java entrypoint rules: Spring MVC routes, JAX-RS resources, and the
2
+ language-core ``public static void main`` entrypoint.
3
+
4
+ Rules scan ``FileExtraction.symbols`` and their captured annotations (stored on
5
+ ``RawSymbol.decorators`` as raw source text, e.g. ``@GetMapping("/users")``).
6
+ A Spring route requires the enclosing type to carry ``@RestController`` or
7
+ ``@Controller``; a JAX-RS resource requires a class-level ``@Path``.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import re
13
+
14
+ from entrygraph.detect.entrypoints.base import (
15
+ EntrypointRule,
16
+ first_string_arg,
17
+ register,
18
+ )
19
+ from entrygraph.extract.ir import EntrypointHint, FileExtraction, RawSymbol
20
+ from entrygraph.kinds import EntrypointKind, SymbolKind
21
+
22
+ # @GetMapping / @PostMapping / ... -> HTTP verb; @RequestMapping -> generic.
23
+ _SPRING_MAPPING = re.compile(
24
+ r"^@(Get|Post|Put|Delete|Patch|Request)Mapping\b"
25
+ )
26
+ _MAPPING_VERB = {
27
+ "Get": "GET", "Post": "POST", "Put": "PUT", "Delete": "DELETE", "Patch": "PATCH",
28
+ }
29
+ _SPRING_CONTROLLER = re.compile(r"^@(RestController|Controller)\b")
30
+
31
+ _JAXRS_HTTP = re.compile(r"^@(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\b")
32
+ _JAXRS_PATH = re.compile(r"^@Path\b")
33
+
34
+
35
+ def _annotation_names(decorators: list[str], pattern: re.Pattern) -> bool:
36
+ return any(pattern.match(d) for d in decorators)
37
+
38
+
39
+ def _type_symbols(x: FileExtraction) -> list[RawSymbol]:
40
+ return [s for s in x.symbols if s.kind in (SymbolKind.CLASS, SymbolKind.INTERFACE)]
41
+
42
+
43
+ def _methods(x: FileExtraction) -> list[RawSymbol]:
44
+ return [s for s in x.symbols if s.kind is SymbolKind.METHOD]
45
+
46
+
47
+ def _enclosing_type(method: RawSymbol, types: list[RawSymbol]) -> RawSymbol | None:
48
+ return next((t for t in types if t.qualified_name == method.parent_qualified_name), None)
49
+
50
+
51
+ def _spring_routes(x: FileExtraction) -> list[EntrypointHint]:
52
+ types = _type_symbols(x)
53
+ controllers = {
54
+ t.qualified_name for t in types
55
+ if _annotation_names(t.decorators, _SPRING_CONTROLLER)
56
+ }
57
+ if not controllers:
58
+ return []
59
+ hints = []
60
+ for method in _methods(x):
61
+ if method.parent_qualified_name not in controllers:
62
+ continue
63
+ for decorator in method.decorators:
64
+ match = _SPRING_MAPPING.match(decorator)
65
+ if not match:
66
+ continue
67
+ verb = _MAPPING_VERB.get(match.group(1))
68
+ methods = [verb] if verb else ["*"]
69
+ hints.append(
70
+ EntrypointHint(
71
+ rule_id="java.spring.route",
72
+ kind=EntrypointKind.HTTP_ROUTE,
73
+ handler_qualified_name=method.qualified_name,
74
+ route=first_string_arg(decorator) or "",
75
+ http_methods=methods,
76
+ framework="spring-boot",
77
+ )
78
+ )
79
+ return hints
80
+
81
+
82
+ def _jaxrs_routes(x: FileExtraction) -> list[EntrypointHint]:
83
+ types = _type_symbols(x)
84
+ resources = {
85
+ t.qualified_name for t in types if _annotation_names(t.decorators, _JAXRS_PATH)
86
+ }
87
+ if not resources:
88
+ return []
89
+ hints = []
90
+ for method in _methods(x):
91
+ if method.parent_qualified_name not in resources:
92
+ continue
93
+ verb = next(
94
+ (_JAXRS_HTTP.match(d).group(1) for d in method.decorators if _JAXRS_HTTP.match(d)),
95
+ None,
96
+ )
97
+ if verb is None:
98
+ continue
99
+ route = next((first_string_arg(d) for d in method.decorators
100
+ if _JAXRS_PATH.match(d)), None)
101
+ hints.append(
102
+ EntrypointHint(
103
+ rule_id="java.jaxrs",
104
+ kind=EntrypointKind.HTTP_ROUTE,
105
+ handler_qualified_name=method.qualified_name,
106
+ route=route or "",
107
+ http_methods=[verb],
108
+ framework="jax-rs",
109
+ )
110
+ )
111
+ return hints
112
+
113
+
114
+ _MICRONAUT_CONTROLLER = re.compile(r"^@Controller\b")
115
+ _MICRONAUT_MAPPING = re.compile(r"^@(Get|Post|Put|Delete|Patch)\b")
116
+ _SERVLET_METHODS = frozenset({"doGet", "doPost", "doPut", "doDelete"})
117
+
118
+
119
+ def _micronaut_routes(x: FileExtraction) -> list[EntrypointHint]:
120
+ types = _type_symbols(x)
121
+ controllers = {t.qualified_name for t in types
122
+ if _annotation_names(t.decorators, _MICRONAUT_CONTROLLER)}
123
+ if not controllers:
124
+ return []
125
+ hints = []
126
+ for method in _methods(x):
127
+ if method.parent_qualified_name not in controllers:
128
+ continue
129
+ for decorator in method.decorators:
130
+ m = _MICRONAUT_MAPPING.match(decorator)
131
+ if m:
132
+ hints.append(EntrypointHint(
133
+ rule_id="java.micronaut.route", kind=EntrypointKind.HTTP_ROUTE,
134
+ handler_qualified_name=method.qualified_name,
135
+ route=first_string_arg(decorator) or "",
136
+ http_methods=[m.group(1).upper()], framework="micronaut"))
137
+ return hints
138
+
139
+
140
+ def _servlet_routes(x: FileExtraction) -> list[EntrypointHint]:
141
+ """Classes extending HttpServlet expose doGet/doPost/... as HTTP handlers."""
142
+ servlets = {t.qualified_name for t in _type_symbols(x)
143
+ if any("HttpServlet" in b for b in t.bases)}
144
+ if not servlets:
145
+ return []
146
+ hints = []
147
+ for method in _methods(x):
148
+ if method.parent_qualified_name in servlets and method.name in _SERVLET_METHODS:
149
+ hints.append(EntrypointHint(
150
+ rule_id="java.servlet.route", kind=EntrypointKind.HTTP_ROUTE,
151
+ handler_qualified_name=method.qualified_name, route="",
152
+ http_methods=[method.name.replace("do", "").upper()], framework="servlet-api"))
153
+ return hints
154
+
155
+
156
+ def _java_main(x: FileExtraction) -> list[EntrypointHint]:
157
+ hints = []
158
+ for method in _methods(x):
159
+ if (
160
+ method.name == "main"
161
+ and "static" in method.modifiers
162
+ and "public" in method.modifiers
163
+ and method.signature
164
+ and "String[]" in method.signature
165
+ ):
166
+ hints.append(
167
+ EntrypointHint(
168
+ rule_id="java.core.main",
169
+ kind=EntrypointKind.MAIN,
170
+ handler_qualified_name=method.qualified_name,
171
+ name=method.qualified_name,
172
+ framework=None,
173
+ )
174
+ )
175
+ return hints
176
+
177
+
178
+ register(EntrypointRule("java.spring.route", "java", "spring-boot",
179
+ EntrypointKind.HTTP_ROUTE, _spring_routes))
180
+ register(EntrypointRule("java.jaxrs", "java", "jax-rs",
181
+ EntrypointKind.HTTP_ROUTE, _jaxrs_routes))
182
+ register(EntrypointRule("java.micronaut.route", "java", "micronaut",
183
+ EntrypointKind.HTTP_ROUTE, _micronaut_routes))
184
+ register(EntrypointRule("java.servlet.route", "java", "servlet-api",
185
+ EntrypointKind.HTTP_ROUTE, _servlet_routes))
186
+ register(EntrypointRule("java.core.main", "java", None,
187
+ EntrypointKind.MAIN, _java_main))
@@ -0,0 +1,211 @@
1
+ """JavaScript/TypeScript entrypoint rules: express/fastify routes, NestJS
2
+ decorators, and Next.js file-convention handlers."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import re
7
+
8
+ from entrygraph.detect.entrypoints.base import EntrypointRule, first_string_arg, register
9
+ from entrygraph.extract.ir import EntrypointHint, FileExtraction
10
+ from entrygraph.kinds import EntrypointKind, SymbolKind
11
+
12
+ _HTTP_METHODS = frozenset({"get", "post", "put", "delete", "patch", "all", "options", "head"})
13
+ _NEST_DECORATOR = re.compile(r"^@(Get|Post|Put|Delete|Patch|All)\b")
14
+
15
+
16
+ _TRAILING_IDENT = re.compile(r",\s*([A-Za-z_$][\w$]*)\s*\)?\s*$")
17
+
18
+
19
+ def _router_routes(framework: str):
20
+ """Factory for app.get('/x', handler) / router.post(...) style matchers.
21
+
22
+ express, koa, and hono all register routes this way; the framework label is
23
+ bound per rule so detection stays accurate.
24
+ """
25
+
26
+ def matcher(x: FileExtraction) -> list[EntrypointHint]:
27
+ hints = []
28
+ for ref in x.references:
29
+ if (
30
+ ref.kind == "call"
31
+ and ref.receiver_text is not None
32
+ and ref.callee_name in _HTTP_METHODS
33
+ and ref.arg_preview
34
+ ):
35
+ route = first_string_arg("(" + ref.arg_preview.lstrip("("))
36
+ if route is None or not route.startswith("/"):
37
+ continue
38
+ # link a named handler argument (app.post('/x', createReport)); an
39
+ # inline function falls back to the enclosing scope / module.
40
+ handler = ref.caller_qualified_name
41
+ named = _TRAILING_IDENT.search(ref.arg_preview)
42
+ if named:
43
+ handler = f"{x.module_path}.{named.group(1)}"
44
+ hints.append(
45
+ EntrypointHint(
46
+ rule_id=f"javascript.{framework}.route",
47
+ kind=EntrypointKind.HTTP_ROUTE,
48
+ handler_qualified_name=handler,
49
+ route=route,
50
+ http_methods=[ref.callee_name.upper()],
51
+ framework=framework,
52
+ )
53
+ )
54
+ return hints
55
+
56
+ return matcher
57
+
58
+
59
+ _express_routes = _router_routes("express")
60
+
61
+
62
+ def _event_handlers(x: FileExtraction, receivers: frozenset[str], framework: str) -> list[EntrypointHint]:
63
+ """socket.io / electron style `emitter.on('event', handler)` registrations."""
64
+ hints = []
65
+ for ref in x.references:
66
+ if (
67
+ ref.kind == "call"
68
+ and ref.callee_name in ("on", "handle")
69
+ and ref.receiver_text in receivers
70
+ and ref.arg_preview
71
+ ):
72
+ hints.append(
73
+ EntrypointHint(
74
+ rule_id=f"javascript.{framework}.event",
75
+ kind=EntrypointKind.EVENT_HANDLER,
76
+ handler_qualified_name=ref.caller_qualified_name,
77
+ name=first_string_arg("(" + ref.arg_preview.lstrip("(")),
78
+ framework=framework,
79
+ )
80
+ )
81
+ return hints
82
+
83
+
84
+ def _socketio_events(x: FileExtraction) -> list[EntrypointHint]:
85
+ return _event_handlers(x, frozenset({"io", "socket"}), "socket.io")
86
+
87
+
88
+ def _electron_ipc(x: FileExtraction) -> list[EntrypointHint]:
89
+ return _event_handlers(x, frozenset({"ipcMain"}), "electron")
90
+
91
+
92
+ def _lambda_js_handlers(x: FileExtraction) -> list[EntrypointHint]:
93
+ """An exported `handler` function is the AWS Lambda entrypoint convention."""
94
+ hints = []
95
+ for symbol in x.symbols:
96
+ if (
97
+ symbol.kind in (SymbolKind.FUNCTION, SymbolKind.METHOD)
98
+ and symbol.name == "handler"
99
+ and symbol.is_exported
100
+ ):
101
+ hints.append(
102
+ EntrypointHint(
103
+ rule_id="javascript.aws-lambda.handler",
104
+ kind=EntrypointKind.LAMBDA_HANDLER,
105
+ handler_qualified_name=symbol.qualified_name,
106
+ name=symbol.name,
107
+ framework="aws-lambda-js",
108
+ )
109
+ )
110
+ return hints
111
+
112
+
113
+ def _express_middleware(x: FileExtraction) -> list[EntrypointHint]:
114
+ """app.use(mw) / router.use('/path', mw) -> middleware registration."""
115
+ hints = []
116
+ for ref in x.references:
117
+ if (
118
+ ref.kind == "call"
119
+ and ref.callee_name == "use"
120
+ and ref.receiver_text in ("app", "router")
121
+ ):
122
+ hints.append(
123
+ EntrypointHint(
124
+ rule_id="javascript.express.middleware",
125
+ kind=EntrypointKind.MIDDLEWARE,
126
+ handler_qualified_name=ref.caller_qualified_name,
127
+ name="use",
128
+ framework="express",
129
+ metadata={"registration": ref.arg_preview} if ref.arg_preview else {},
130
+ )
131
+ )
132
+ return hints
133
+
134
+
135
+ _NEST_CONTROLLER = re.compile(r"^@Controller\b")
136
+
137
+
138
+ def _nest_routes(x: FileExtraction) -> list[EntrypointHint]:
139
+ # controller class qname -> its @Controller('prefix') path prefix
140
+ prefixes: dict[str, str] = {}
141
+ for symbol in x.symbols:
142
+ if symbol.kind is SymbolKind.CLASS:
143
+ for decorator in symbol.decorators:
144
+ if _NEST_CONTROLLER.match(decorator):
145
+ prefixes[symbol.qualified_name] = (first_string_arg(decorator) or "").strip("/")
146
+ hints = []
147
+ for symbol in x.symbols:
148
+ if symbol.kind is not SymbolKind.METHOD:
149
+ continue
150
+ for decorator in symbol.decorators:
151
+ m = _NEST_DECORATOR.match(decorator)
152
+ if m:
153
+ prefix = prefixes.get(symbol.parent_qualified_name or "", "")
154
+ path = (first_string_arg(decorator) or "").strip("/")
155
+ route = "/" + "/".join(p for p in (prefix, path) if p)
156
+ hints.append(
157
+ EntrypointHint(
158
+ rule_id="javascript.nestjs.route",
159
+ kind=EntrypointKind.HTTP_ROUTE,
160
+ handler_qualified_name=symbol.qualified_name,
161
+ route=route,
162
+ http_methods=[m.group(1).upper()],
163
+ framework="nestjs",
164
+ )
165
+ )
166
+ return hints
167
+
168
+
169
+ def _next_handlers(x: FileExtraction) -> list[EntrypointHint]:
170
+ """Next.js app-router route handlers: exported GET/POST in app/**/route.{ts,js}."""
171
+ if not (x.path.endswith(("route.ts", "route.js")) and "/app/" in f"/{x.path}"):
172
+ return []
173
+ hints = []
174
+ for symbol in x.symbols:
175
+ if symbol.kind in (SymbolKind.FUNCTION, SymbolKind.METHOD) and symbol.name in _HTTP_METHODS_UPPER:
176
+ hints.append(
177
+ EntrypointHint(
178
+ rule_id="javascript.next.route",
179
+ kind=EntrypointKind.HTTP_ROUTE,
180
+ handler_qualified_name=symbol.qualified_name,
181
+ route="/" + x.path.split("/app/", 1)[1].rsplit("/", 1)[0],
182
+ http_methods=[symbol.name],
183
+ framework="next",
184
+ )
185
+ )
186
+ return hints
187
+
188
+
189
+ _HTTP_METHODS_UPPER = frozenset(m.upper() for m in _HTTP_METHODS)
190
+
191
+
192
+ register(EntrypointRule("javascript.express.route", "javascript", "express",
193
+ EntrypointKind.HTTP_ROUTE, _express_routes))
194
+ register(EntrypointRule("javascript.express.middleware", "javascript", "express",
195
+ EntrypointKind.MIDDLEWARE, _express_middleware))
196
+ register(EntrypointRule("javascript.fastify.route", "javascript", "fastify",
197
+ EntrypointKind.HTTP_ROUTE, _router_routes("fastify")))
198
+ register(EntrypointRule("javascript.koa.route", "javascript", "koa",
199
+ EntrypointKind.HTTP_ROUTE, _router_routes("koa")))
200
+ register(EntrypointRule("javascript.hono.route", "javascript", "hono",
201
+ EntrypointKind.HTTP_ROUTE, _router_routes("hono")))
202
+ register(EntrypointRule("javascript.socketio.event", "javascript", "socket.io",
203
+ EntrypointKind.EVENT_HANDLER, _socketio_events))
204
+ register(EntrypointRule("javascript.electron.ipc", "javascript", "electron",
205
+ EntrypointKind.EVENT_HANDLER, _electron_ipc))
206
+ register(EntrypointRule("javascript.aws-lambda.handler", "javascript", "aws-lambda-js",
207
+ EntrypointKind.LAMBDA_HANDLER, _lambda_js_handlers))
208
+ register(EntrypointRule("javascript.nestjs.route", "javascript", "nestjs",
209
+ EntrypointKind.HTTP_ROUTE, _nest_routes))
210
+ register(EntrypointRule("javascript.next.route", "javascript", "next",
211
+ EntrypointKind.HTTP_ROUTE, _next_handlers))
@@ -0,0 +1,133 @@
1
+ """PHP entrypoint rules: Laravel routes, Symfony route attributes, WordPress
2
+ hooks, and the language-core namespace-less script entrypoint.
3
+
4
+ Laravel route DSLs are ``Route::get('/x', ...)`` scoped calls — extracted as
5
+ call references with ``receiver_text == "Route"`` — so those rules match against
6
+ references rather than symbol decorators (like the Ruby route rules). Symfony
7
+ uses PHP 8 ``#[Route('/x')]`` attributes, which the extractor emits as symbol
8
+ decorators (like Java annotations), so that rule matches decorators.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import re
14
+
15
+ from entrygraph.detect.entrypoints.base import (
16
+ EntrypointRule,
17
+ first_string_arg,
18
+ register,
19
+ )
20
+ from entrygraph.extract.ir import EntrypointHint, FileExtraction
21
+ from entrygraph.kinds import EntrypointKind, SymbolKind
22
+
23
+ _LARAVEL_VERBS = frozenset(
24
+ {"get", "post", "put", "delete", "patch", "any", "match"}
25
+ )
26
+ _SYMFONY_ROUTE = re.compile(r"#\[\s*Route\s*\(")
27
+ _WORDPRESS_HOOKS = frozenset({"add_action", "add_filter"})
28
+
29
+
30
+ def _laravel_routes(x: FileExtraction) -> list[EntrypointHint]:
31
+ if not re.search(r"(^|/)routes/[^/]+\.php$", x.path):
32
+ return []
33
+ hints = []
34
+ for ref in x.references:
35
+ if (
36
+ ref.kind == "call"
37
+ and ref.receiver_text == "Route"
38
+ and ref.callee_name in _LARAVEL_VERBS
39
+ and ref.arg_preview
40
+ ):
41
+ route = first_string_arg("(" + ref.arg_preview.lstrip("("))
42
+ hints.append(
43
+ EntrypointHint(
44
+ rule_id="php.laravel.route",
45
+ kind=EntrypointKind.HTTP_ROUTE,
46
+ handler_qualified_name=None, # handler resolved as a call edge
47
+ route=route if route is not None else "",
48
+ http_methods=["*"] if ref.callee_name in ("any", "match")
49
+ else [ref.callee_name.upper()],
50
+ framework="laravel",
51
+ metadata={"registration": ref.arg_preview},
52
+ )
53
+ )
54
+ return hints
55
+
56
+
57
+ def _symfony_routes(x: FileExtraction) -> list[EntrypointHint]:
58
+ hints = []
59
+ for symbol in x.symbols:
60
+ if symbol.kind not in (SymbolKind.METHOD, SymbolKind.FUNCTION):
61
+ continue
62
+ for decorator in symbol.decorators:
63
+ if _SYMFONY_ROUTE.search(decorator):
64
+ hints.append(
65
+ EntrypointHint(
66
+ rule_id="php.symfony.route",
67
+ kind=EntrypointKind.HTTP_ROUTE,
68
+ handler_qualified_name=symbol.qualified_name,
69
+ route=first_string_arg(decorator) or "",
70
+ http_methods=["*"],
71
+ framework="symfony",
72
+ )
73
+ )
74
+ return hints
75
+
76
+
77
+ def _wordpress_hooks(x: FileExtraction) -> list[EntrypointHint]:
78
+ hints = []
79
+ for ref in x.references:
80
+ if (
81
+ ref.kind == "call"
82
+ and ref.receiver_text is None
83
+ and ref.callee_name in _WORDPRESS_HOOKS
84
+ and ref.arg_preview
85
+ ):
86
+ hints.append(
87
+ EntrypointHint(
88
+ rule_id="php.wordpress.hook",
89
+ kind=EntrypointKind.EVENT_HANDLER,
90
+ handler_qualified_name=None, # callback resolved as call edge
91
+ name=first_string_arg("(" + ref.arg_preview.lstrip("(")),
92
+ framework="wordpress",
93
+ metadata={"registration": ref.arg_preview},
94
+ )
95
+ )
96
+ return hints
97
+
98
+
99
+ def _script(x: FileExtraction) -> list[EntrypointHint]:
100
+ """A namespace-less top-level ``index.php`` with module-level references.
101
+
102
+ The extractor sets ``module_path`` to the file's PHP namespace when one is
103
+ present, otherwise to the directory-derived path (which for ``index.php``
104
+ ends in ``.index`` or equals ``index``). A namespaced ``index.php`` is a
105
+ library file, not a script entrypoint, so we require the directory-derived
106
+ (namespace-less) form. We also require at least one module-level reference
107
+ (a ``caller_qualified_name is None`` call) so an empty file is not flagged.
108
+ """
109
+ if not x.path.endswith("index.php"):
110
+ return []
111
+ if not (x.module_path == "index" or x.module_path.endswith(".index")):
112
+ return [] # namespaced -> not a bare script
113
+ if not any(ref.caller_qualified_name is None for ref in x.references):
114
+ return []
115
+ return [
116
+ EntrypointHint(
117
+ rule_id="php.core.script",
118
+ kind=EntrypointKind.MAIN,
119
+ handler_qualified_name=None, # the module itself
120
+ name=x.module_path,
121
+ framework=None,
122
+ )
123
+ ]
124
+
125
+
126
+ register(EntrypointRule("php.laravel.route", "php", "laravel",
127
+ EntrypointKind.HTTP_ROUTE, _laravel_routes))
128
+ register(EntrypointRule("php.symfony.route", "php", "symfony",
129
+ EntrypointKind.HTTP_ROUTE, _symfony_routes))
130
+ register(EntrypointRule("php.wordpress.hook", "php", "wordpress",
131
+ EntrypointKind.EVENT_HANDLER, _wordpress_hooks))
132
+ register(EntrypointRule("php.core.script", "php", None,
133
+ EntrypointKind.MAIN, _script))