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,124 @@
1
+ """Entrypoint rule schema and registry.
2
+
3
+ Rules are IR-driven: they match against a FileExtraction (symbols, decorators,
4
+ references, framework signals) rather than re-walking the tree, which keeps
5
+ them cheap and language-uniform. A rule gated on a framework only runs when
6
+ that framework was detected above threshold — `app.get("/x")` in random code
7
+ is not an Express route.
8
+
9
+ Adding a framework = registering one rule (and usually one FrameworkSpec).
10
+ Third parties: entrygraph.detect.entrypoints.register(rule).
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import re
16
+ from dataclasses import dataclass
17
+ from typing import Callable
18
+
19
+ from entrygraph.extract.ir import EntrypointHint, FileExtraction
20
+ from entrygraph.kinds import EntrypointKind
21
+
22
+ Matcher = Callable[[FileExtraction], list[EntrypointHint]]
23
+
24
+
25
+ @dataclass(frozen=True, slots=True)
26
+ class EntrypointRule:
27
+ id: str # "python.flask.route"
28
+ language: str
29
+ framework: str | None # None => language-core rule, always runs
30
+ kind: EntrypointKind
31
+ match: Matcher
32
+
33
+
34
+ _RULES: list[EntrypointRule] = []
35
+
36
+
37
+ def register(rule: EntrypointRule) -> None:
38
+ _RULES.append(rule)
39
+
40
+
41
+ # TypeScript/TSX reuse the JavaScript rule set (one extractor drives all three).
42
+ _LANG_ALIASES = {"typescript": "javascript", "tsx": "javascript"}
43
+
44
+
45
+ def rules_for(language: str, detected_frameworks: set[str]) -> list[EntrypointRule]:
46
+ lang = _LANG_ALIASES.get(language, language)
47
+ return [
48
+ r
49
+ for r in _RULES
50
+ if r.language == lang
51
+ and (r.framework is None or r.framework in detected_frameworks)
52
+ ]
53
+
54
+
55
+ def all_rules() -> list[EntrypointRule]:
56
+ return list(_RULES)
57
+
58
+
59
+ # ---------------- shared decorator-parsing helpers ----------------
60
+
61
+ _STRING_ARG = re.compile(r"""\(\s*[rbf]*["']([^"']+)["']""")
62
+ _METHODS_KWARG = re.compile(r"""methods\s*=\s*[\[(]([^\])]*)[\])]""")
63
+ _QUOTED = re.compile(r"""["']([^"']+)["']""")
64
+ _IDENTIFIER = re.compile(r"[A-Za-z_]\w*")
65
+ _PARAMS = re.compile(r"\(([^)]*)\)")
66
+
67
+ # names conventionally carrying user-controlled input across web frameworks
68
+ _TAINTED_NAMES = frozenset(
69
+ {"request", "req", "params", "event", "body", "payload", "query", "form", "data"}
70
+ )
71
+
72
+
73
+ def first_string_arg(decorator_text: str) -> str | None:
74
+ match = _STRING_ARG.search(decorator_text)
75
+ return match.group(1) if match else None
76
+
77
+
78
+ def methods_kwarg(decorator_text: str) -> list[str]:
79
+ match = _METHODS_KWARG.search(decorator_text)
80
+ if not match:
81
+ return []
82
+ return [m.upper() for m in _QUOTED.findall(match.group(1))]
83
+
84
+
85
+ def identifier_args(arg_preview: str | None) -> list[str]:
86
+ """Bare identifiers appearing as call arguments (best-effort over the preview).
87
+
88
+ Skips string/number literals and keyword-argument names before '='.
89
+ """
90
+ if not arg_preview:
91
+ return []
92
+ names: list[str] = []
93
+ for part in arg_preview.strip("()").split(","):
94
+ token = part.strip()
95
+ if "=" in token: # keyword arg: take the value side only
96
+ token = token.split("=", 1)[1].strip()
97
+ if token and _IDENTIFIER.fullmatch(token):
98
+ names.append(token)
99
+ return names
100
+
101
+
102
+ def tainted_params(signature: str | None, kind: str) -> list[str]:
103
+ """Parameters of an entrypoint handler that are likely user-controlled.
104
+
105
+ HTTP handlers: every parameter except self/cls (path/query params flow in).
106
+ Lambda handlers: the `event` parameter. Otherwise: parameters whose names
107
+ match the conventional tainted-name set.
108
+ """
109
+ if not signature:
110
+ return []
111
+ match = _PARAMS.search(signature)
112
+ if not match:
113
+ return []
114
+ raw = [p.strip() for p in match.group(1).split(",") if p.strip()]
115
+ params = []
116
+ for p in raw:
117
+ name = p.split(":", 1)[0].split("=", 1)[0].strip().lstrip("*")
118
+ if name and name not in ("self", "cls"):
119
+ params.append(name)
120
+ if kind == "http_route":
121
+ return params
122
+ if kind == "lambda_handler":
123
+ return [p for p in params if p == "event"] or params[:1]
124
+ return [p for p in params if p in _TAINTED_NAMES]
@@ -0,0 +1,139 @@
1
+ """Config-file entrypoints — handlers declared outside source code.
2
+
3
+ The walker only feeds source files to extractors, so serverless/SAM handler
4
+ fields, Procfile process lines, and Dockerfile CMD/ENTRYPOINT never reach the
5
+ IR-driven rules. This module scans a handful of well-known root files with the
6
+ same stdlib+regex approach as detect/manifests.py (there is no YAML dependency),
7
+ producing ConfigHints that bind to a real symbol where possible.
8
+
9
+ `Entrypoint.symbol_id` is NOT NULL, so a hint whose handler cannot be bound to
10
+ an indexed symbol is dropped rather than synthesizing a placeholder symbol.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import re
16
+ from dataclasses import dataclass, field
17
+ from pathlib import Path
18
+
19
+ from entrygraph.kinds import EntrypointKind
20
+
21
+ _SERVERLESS_HANDLER = re.compile(r"^\s*handler:\s*([^\s#]+)", re.MULTILINE)
22
+ _SAM_HANDLER = re.compile(r"^\s*Handler:\s*([^\s#]+)", re.MULTILINE)
23
+ _PROCFILE_LINE = re.compile(r"^(\w+):\s*(.+)$", re.MULTILINE)
24
+ _DOCKER_CMD = re.compile(r"^\s*(?:CMD|ENTRYPOINT)\s+(.+)$", re.MULTILINE | re.IGNORECASE)
25
+
26
+ # frameworks whose entrypoints are re-derived from config each index run
27
+ CONFIG_FRAMEWORKS = ("serverless", "sam", "procfile", "docker")
28
+
29
+
30
+ @dataclass(slots=True)
31
+ class ConfigHint:
32
+ kind: EntrypointKind
33
+ framework: str
34
+ handler_ref: str # raw handler string as written in the config
35
+ name: str
36
+ route: str | None = None
37
+ metadata: dict = field(default_factory=dict)
38
+
39
+
40
+ def scan_config_entrypoints(root: str | Path) -> list[ConfigHint]:
41
+ root = Path(root)
42
+ hints: list[ConfigHint] = []
43
+ for name in ("serverless.yml", "serverless.yaml"):
44
+ hints += _scan(root / name, _SERVERLESS_HANDLER, EntrypointKind.LAMBDA_HANDLER, "serverless")
45
+ for name in ("template.yaml", "template.yml"):
46
+ hints += _scan(root / name, _SAM_HANDLER, EntrypointKind.LAMBDA_HANDLER, "sam")
47
+ hints += _scan_procfile(root / "Procfile")
48
+ hints += _scan_dockerfile(root / "Dockerfile")
49
+ return hints
50
+
51
+
52
+ def _read(path: Path) -> str | None:
53
+ if not path.is_file():
54
+ return None
55
+ try:
56
+ return path.read_text(encoding="utf-8", errors="replace")
57
+ except OSError: # pragma: no cover
58
+ return None
59
+
60
+
61
+ def _scan(path: Path, pattern: re.Pattern, kind: EntrypointKind, framework: str) -> list[ConfigHint]:
62
+ text = _read(path)
63
+ if text is None:
64
+ return []
65
+ hints = []
66
+ for match in pattern.finditer(text):
67
+ ref = match.group(1).strip().strip("'\"")
68
+ hints.append(ConfigHint(kind=kind, framework=framework, handler_ref=ref, name=ref))
69
+ return hints
70
+
71
+
72
+ def _scan_procfile(path: Path) -> list[ConfigHint]:
73
+ text = _read(path)
74
+ if text is None:
75
+ return []
76
+ hints = []
77
+ for proc, command in _PROCFILE_LINE.findall(text):
78
+ hints.append(ConfigHint(kind=EntrypointKind.MAIN, framework="procfile",
79
+ handler_ref=command.strip(), name=proc))
80
+ return hints
81
+
82
+
83
+ def _scan_dockerfile(path: Path) -> list[ConfigHint]:
84
+ text = _read(path)
85
+ if text is None:
86
+ return []
87
+ hints = []
88
+ for command in _DOCKER_CMD.findall(text):
89
+ hints.append(ConfigHint(kind=EntrypointKind.MAIN, framework="docker",
90
+ handler_ref=command.strip(), name="cmd"))
91
+ return hints
92
+
93
+
94
+ def bind_handler(handler_ref: str, symbol_id_by_qname: dict[str, int],
95
+ module_symbol_ids: dict[str, int]) -> int | None:
96
+ """Best-effort map a config handler string to an indexed symbol id.
97
+
98
+ Handles the common forms:
99
+ - ``src/app.handler`` / ``app.lambda_handler`` (dotted or path+dot)
100
+ - ``pkg.mod:app`` (gunicorn/uvicorn colon form)
101
+ - ``python -m pkg.mod`` / ``node src/server.js`` (process commands)
102
+ """
103
+ for qname in _candidate_qnames(handler_ref):
104
+ if qname in symbol_id_by_qname:
105
+ return symbol_id_by_qname[qname]
106
+ if qname in module_symbol_ids:
107
+ return module_symbol_ids[qname]
108
+ return None
109
+
110
+
111
+ _SRC_PREFIXES = ("src/", "lib/", "app/")
112
+ _CODE_EXTS = (".py", ".js", ".ts", ".mjs", ".cjs", ".rb", ".go")
113
+
114
+
115
+ def _candidate_qnames(handler_ref: str) -> list[str]:
116
+ ref = handler_ref.strip()
117
+ # process command: take the token that looks like a module/path
118
+ if " " in ref:
119
+ tokens = ref.split()
120
+ if "-m" in tokens:
121
+ i = tokens.index("-m")
122
+ if i + 1 < len(tokens):
123
+ ref = tokens[i + 1]
124
+ else:
125
+ ref = next((t for t in tokens[1:] if "/" in t or "." in t), tokens[-1])
126
+ ref = ref.split(":", 1)[0] if ":" in ref and "/" not in ref.split(":", 1)[1] else ref.replace(":", ".")
127
+ for prefix in _SRC_PREFIXES:
128
+ if ref.startswith(prefix):
129
+ ref = ref[len(prefix):]
130
+ for ext in _CODE_EXTS:
131
+ if ref.endswith(ext):
132
+ ref = ref[: -len(ext)]
133
+ break
134
+ dotted = ref.replace("/", ".").strip(".")
135
+ candidates = [dotted]
136
+ # a handler like module.function -> also try the bare module
137
+ if "." in dotted:
138
+ candidates.append(dotted.rsplit(".", 1)[0])
139
+ return [c for c in candidates if c]
@@ -0,0 +1,156 @@
1
+ """C# entrypoint rules: ASP.NET Core MVC/attribute-routed controllers, minimal
2
+ API route registrations, and the language-core ``static Main`` entrypoint.
3
+
4
+ Controller rules scan ``FileExtraction.symbols`` and their captured attributes
5
+ (stored on ``RawSymbol.decorators`` as raw source text, e.g.
6
+ ``[HttpGet("/x")]``). A controller is a type carrying ``[ApiController]`` /
7
+ ``[Controller]`` or whose base type name contains ``Controller``. Minimal-API
8
+ routes are recognised from ``MapGet``/``MapPost``/... call references.
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, RawSymbol
21
+ from entrygraph.kinds import EntrypointKind, SymbolKind
22
+
23
+ # [HttpGet("/x")] / [HttpPost] / ... -> HTTP verb; [Route("...")] -> generic.
24
+ _HTTP_ATTR = re.compile(r"^\[Http(Get|Post|Put|Delete|Patch|Head|Options)\b")
25
+ _ROUTE_ATTR = re.compile(r"^\[Route\b")
26
+ _CONTROLLER_ATTR = re.compile(r"^\[(ApiController|Controller)\b")
27
+
28
+ _MINIMAL_MAP = {
29
+ "MapGet": "GET",
30
+ "MapPost": "POST",
31
+ "MapPut": "PUT",
32
+ "MapDelete": "DELETE",
33
+ "MapPatch": "PATCH",
34
+ }
35
+
36
+
37
+ def _type_symbols(x: FileExtraction) -> list[RawSymbol]:
38
+ return [s for s in x.symbols if s.kind in (SymbolKind.CLASS, SymbolKind.INTERFACE)]
39
+
40
+
41
+ def _methods(x: FileExtraction) -> list[RawSymbol]:
42
+ return [s for s in x.symbols if s.kind is SymbolKind.METHOD]
43
+
44
+
45
+ def _is_controller(t: RawSymbol) -> bool:
46
+ if any(_CONTROLLER_ATTR.match(d) for d in t.decorators):
47
+ return True
48
+ return any("Controller" in b for b in t.bases)
49
+
50
+
51
+ def _aspnet_controller_routes(x: FileExtraction) -> list[EntrypointHint]:
52
+ controllers = {t.qualified_name for t in _type_symbols(x) if _is_controller(t)}
53
+ if not controllers:
54
+ return []
55
+ hints: list[EntrypointHint] = []
56
+ for method in _methods(x):
57
+ if method.parent_qualified_name not in controllers:
58
+ continue
59
+ for decorator in method.decorators:
60
+ m = _HTTP_ATTR.match(decorator)
61
+ if m:
62
+ hints.append(
63
+ EntrypointHint(
64
+ rule_id="csharp.aspnet.controller-route",
65
+ kind=EntrypointKind.HTTP_ROUTE,
66
+ handler_qualified_name=method.qualified_name,
67
+ route=first_string_arg(decorator) or "",
68
+ http_methods=[m.group(1).upper()],
69
+ framework="aspnetcore",
70
+ )
71
+ )
72
+ break
73
+ else:
74
+ # No Http* verb attribute; a bare [Route("...")] still exposes it.
75
+ route_dec = next(
76
+ (d for d in method.decorators if _ROUTE_ATTR.match(d)), None
77
+ )
78
+ if route_dec is not None:
79
+ hints.append(
80
+ EntrypointHint(
81
+ rule_id="csharp.aspnet.controller-route",
82
+ kind=EntrypointKind.HTTP_ROUTE,
83
+ handler_qualified_name=method.qualified_name,
84
+ route=first_string_arg(route_dec) or "",
85
+ http_methods=["*"],
86
+ framework="aspnetcore",
87
+ )
88
+ )
89
+ return hints
90
+
91
+
92
+ def _aspnet_minimal_api(x: FileExtraction) -> list[EntrypointHint]:
93
+ hints: list[EntrypointHint] = []
94
+ for ref in x.references:
95
+ if ref.kind != "call":
96
+ continue
97
+ verb = _MINIMAL_MAP.get(ref.callee_name)
98
+ if verb is None:
99
+ continue
100
+ route = first_string_arg(ref.arg_preview or "")
101
+ hints.append(
102
+ EntrypointHint(
103
+ rule_id="csharp.aspnet.minimal-api",
104
+ kind=EntrypointKind.HTTP_ROUTE,
105
+ handler_qualified_name=None, # inline lambda handler
106
+ route=route or "",
107
+ http_methods=[verb],
108
+ framework="aspnetcore",
109
+ )
110
+ )
111
+ return hints
112
+
113
+
114
+ def _csharp_main(x: FileExtraction) -> list[EntrypointHint]:
115
+ hints: list[EntrypointHint] = []
116
+ for method in _methods(x):
117
+ if method.name == "Main" and "static" in method.modifiers:
118
+ hints.append(
119
+ EntrypointHint(
120
+ rule_id="csharp.core.main",
121
+ kind=EntrypointKind.MAIN,
122
+ handler_qualified_name=method.qualified_name,
123
+ name=method.qualified_name,
124
+ framework=None,
125
+ )
126
+ )
127
+ return hints
128
+
129
+
130
+ register(
131
+ EntrypointRule(
132
+ "csharp.aspnet.controller-route",
133
+ "csharp",
134
+ "aspnetcore",
135
+ EntrypointKind.HTTP_ROUTE,
136
+ _aspnet_controller_routes,
137
+ )
138
+ )
139
+ register(
140
+ EntrypointRule(
141
+ "csharp.aspnet.minimal-api",
142
+ "csharp",
143
+ "aspnetcore",
144
+ EntrypointKind.HTTP_ROUTE,
145
+ _aspnet_minimal_api,
146
+ )
147
+ )
148
+ register(
149
+ EntrypointRule(
150
+ "csharp.core.main",
151
+ "csharp",
152
+ None,
153
+ EntrypointKind.MAIN,
154
+ _csharp_main,
155
+ )
156
+ )
@@ -0,0 +1,158 @@
1
+ """Go entrypoint rules: the package-main entrypoint, net/http handler
2
+ registration, gin router routes, and cobra CLI commands."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from entrygraph.detect.entrypoints.base import (
7
+ EntrypointRule,
8
+ first_string_arg,
9
+ register,
10
+ )
11
+ from entrygraph.extract.ir import EntrypointHint, FileExtraction
12
+ from entrygraph.kinds import EntrypointKind, SymbolKind
13
+
14
+ _GIN_METHODS = frozenset(
15
+ {"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "Any", "Handle"}
16
+ )
17
+ _NETHTTP_REGISTER = frozenset({"HandleFunc", "Handle"})
18
+
19
+
20
+ def _go_main(x: FileExtraction) -> list[EntrypointHint]:
21
+ hints = []
22
+ for symbol in x.symbols:
23
+ if (
24
+ symbol.kind is SymbolKind.FUNCTION
25
+ and symbol.name == "main"
26
+ and symbol.parent_qualified_name is None
27
+ ):
28
+ hints.append(
29
+ EntrypointHint(
30
+ rule_id="go.core.main",
31
+ kind=EntrypointKind.MAIN,
32
+ handler_qualified_name=symbol.qualified_name,
33
+ name=symbol.qualified_name,
34
+ span=symbol.span,
35
+ )
36
+ )
37
+ return hints
38
+
39
+
40
+ def _nethttp_routes(x: FileExtraction) -> list[EntrypointHint]:
41
+ hints = []
42
+ for ref in x.references:
43
+ if (
44
+ ref.kind == "call"
45
+ and ref.receiver_text == "http"
46
+ and ref.callee_name in _NETHTTP_REGISTER
47
+ and ref.arg_preview
48
+ ):
49
+ route = first_string_arg("(" + ref.arg_preview.lstrip("("))
50
+ if route is not None:
51
+ hints.append(
52
+ EntrypointHint(
53
+ rule_id="go.nethttp.route",
54
+ kind=EntrypointKind.HTTP_ROUTE,
55
+ handler_qualified_name=ref.caller_qualified_name,
56
+ route=route,
57
+ http_methods=["*"],
58
+ framework="net/http",
59
+ metadata={"registration": ref.arg_preview},
60
+ )
61
+ )
62
+ return hints
63
+
64
+
65
+ def _gin_routes(x: FileExtraction) -> list[EntrypointHint]:
66
+ hints = []
67
+ for ref in x.references:
68
+ if (
69
+ ref.kind == "call"
70
+ and ref.receiver_text is not None
71
+ and ref.callee_name in _GIN_METHODS
72
+ and ref.arg_preview
73
+ ):
74
+ route = first_string_arg("(" + ref.arg_preview.lstrip("("))
75
+ if route is not None and route.startswith("/"):
76
+ method = ref.callee_name.upper() if ref.callee_name in {
77
+ "GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"
78
+ } else "*"
79
+ hints.append(
80
+ EntrypointHint(
81
+ rule_id="go.gin.route",
82
+ kind=EntrypointKind.HTTP_ROUTE,
83
+ handler_qualified_name=ref.caller_qualified_name,
84
+ route=route,
85
+ http_methods=[method],
86
+ framework="gin",
87
+ metadata={"registration": ref.arg_preview},
88
+ )
89
+ )
90
+ return hints
91
+
92
+
93
+ def _gin_style(framework: str):
94
+ """chi and fiber share gin's r.Get('/x', handler) registration shape."""
95
+
96
+ def matcher(x: FileExtraction) -> list[EntrypointHint]:
97
+ hints = []
98
+ for hint in _gin_routes(x):
99
+ hints.append(EntrypointHint(
100
+ rule_id=f"go.{framework}.route", kind=hint.kind,
101
+ handler_qualified_name=hint.handler_qualified_name, route=hint.route,
102
+ http_methods=hint.http_methods, framework=framework, metadata=hint.metadata))
103
+ return hints
104
+
105
+ return matcher
106
+
107
+
108
+ def _gorilla_routes(x: FileExtraction) -> list[EntrypointHint]:
109
+ """gorilla/mux: r.HandleFunc('/x', handler)."""
110
+ hints = []
111
+ for ref in x.references:
112
+ if (
113
+ ref.kind == "call"
114
+ and ref.callee_name in ("HandleFunc", "Handle")
115
+ and ref.receiver_text not in (None, "http")
116
+ and ref.arg_preview
117
+ ):
118
+ route = first_string_arg("(" + ref.arg_preview.lstrip("("))
119
+ if route is not None and route.startswith("/"):
120
+ hints.append(EntrypointHint(
121
+ rule_id="go.gorilla-mux.route", kind=EntrypointKind.HTTP_ROUTE,
122
+ handler_qualified_name=ref.caller_qualified_name, route=route,
123
+ http_methods=["*"], framework="gorilla-mux",
124
+ metadata={"registration": ref.arg_preview}))
125
+ return hints
126
+
127
+
128
+ def _cobra_commands(x: FileExtraction) -> list[EntrypointHint]:
129
+ hints = []
130
+ for ref in x.references:
131
+ if ref.kind == "composite" and ref.callee_text == "cobra.Command":
132
+ hints.append(
133
+ EntrypointHint(
134
+ rule_id="go.cobra.command",
135
+ kind=EntrypointKind.CLI_COMMAND,
136
+ handler_qualified_name=ref.caller_qualified_name,
137
+ name=first_string_arg("(" + (ref.arg_preview or "") + ")") or None,
138
+ framework="cobra",
139
+ span=ref.span,
140
+ )
141
+ )
142
+ return hints
143
+
144
+
145
+ register(EntrypointRule("go.core.main", "go", None,
146
+ EntrypointKind.MAIN, _go_main))
147
+ register(EntrypointRule("go.nethttp.route", "go", "net/http",
148
+ EntrypointKind.HTTP_ROUTE, _nethttp_routes))
149
+ register(EntrypointRule("go.gin.route", "go", "gin",
150
+ EntrypointKind.HTTP_ROUTE, _gin_routes))
151
+ register(EntrypointRule("go.chi.route", "go", "chi",
152
+ EntrypointKind.HTTP_ROUTE, _gin_style("chi")))
153
+ register(EntrypointRule("go.fiber.route", "go", "fiber",
154
+ EntrypointKind.HTTP_ROUTE, _gin_style("fiber")))
155
+ register(EntrypointRule("go.gorilla-mux.route", "go", "gorilla-mux",
156
+ EntrypointKind.HTTP_ROUTE, _gorilla_routes))
157
+ register(EntrypointRule("go.cobra.command", "go", "cobra",
158
+ EntrypointKind.CLI_COMMAND, _cobra_commands))