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