code-atlas-forge 1.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.
- code_atlas/__init__.py +6 -0
- code_atlas/__main__.py +6 -0
- code_atlas/api/__init__.py +5 -0
- code_atlas/api/handlers.py +1514 -0
- code_atlas/api/server.py +394 -0
- code_atlas/cli.py +2084 -0
- code_atlas/config.py +149 -0
- code_atlas/context/__init__.py +66 -0
- code_atlas/context/contracts.py +277 -0
- code_atlas/context/domain.py +760 -0
- code_atlas/context/engine.py +442 -0
- code_atlas/context/expand.py +133 -0
- code_atlas/context/renderers.py +71 -0
- code_atlas/context/tokenizer.py +37 -0
- code_atlas/contracts/__init__.py +38 -0
- code_atlas/contracts/enums.py +67 -0
- code_atlas/contracts/models.py +175 -0
- code_atlas/contracts/ports.py +70 -0
- code_atlas/discovery/__init__.py +25 -0
- code_atlas/discovery/gitignore.py +119 -0
- code_atlas/discovery/identity.py +52 -0
- code_atlas/discovery/models.py +204 -0
- code_atlas/discovery/scanner.py +495 -0
- code_atlas/domains/__init__.py +107 -0
- code_atlas/domains/api/__init__.py +28 -0
- code_atlas/domains/api/adapter.py +405 -0
- code_atlas/domains/api/common.py +308 -0
- code_atlas/domains/api/consumers.py +224 -0
- code_atlas/domains/api/emit.py +231 -0
- code_atlas/domains/api/openapi.py +398 -0
- code_atlas/domains/api/routes.py +582 -0
- code_atlas/domains/api/yaml_subset.py +683 -0
- code_atlas/domains/aws/__init__.py +28 -0
- code_atlas/domains/aws/adapter.py +382 -0
- code_atlas/domains/aws/common.py +261 -0
- code_atlas/domains/aws/configs.py +1164 -0
- code_atlas/domains/aws/emit.py +411 -0
- code_atlas/domains/aws/pyfiles.py +2220 -0
- code_atlas/domains/aws/sqlbridge.py +448 -0
- code_atlas/domains/cicd/__init__.py +50 -0
- code_atlas/domains/cicd/adapter.py +762 -0
- code_atlas/domains/cicd/classify.py +136 -0
- code_atlas/domains/cicd/workflow.py +504 -0
- code_atlas/domains/cicd/yamlmini.py +946 -0
- code_atlas/domains/cli_surface.py +739 -0
- code_atlas/domains/contracts.py +274 -0
- code_atlas/domains/crossresolve.py +1208 -0
- code_atlas/domains/evidence.py +57 -0
- code_atlas/domains/identity.py +149 -0
- code_atlas/domains/model.py +202 -0
- code_atlas/domains/registry.py +250 -0
- code_atlas/domains/resolution.py +65 -0
- code_atlas/domains/spark/__init__.py +31 -0
- code_atlas/domains/spark/adapter.py +146 -0
- code_atlas/domains/spark/emit.py +344 -0
- code_atlas/domains/spark/extract.py +108 -0
- code_atlas/domains/spark/sqllit.py +192 -0
- code_atlas/domains/spark/walk.py +1943 -0
- code_atlas/domains/sql/__init__.py +30 -0
- code_atlas/domains/sql/adapter.py +382 -0
- code_atlas/domains/sql/extract.py +1271 -0
- code_atlas/domains/sql/nodes.py +264 -0
- code_atlas/domains/sql/parser.py +1510 -0
- code_atlas/domains/sql/tokens.py +318 -0
- code_atlas/domains/summarize.py +126 -0
- code_atlas/domains/terraform/__init__.py +20 -0
- code_atlas/domains/terraform/adapter.py +127 -0
- code_atlas/domains/terraform/catalog.py +43 -0
- code_atlas/domains/terraform/extract.py +1277 -0
- code_atlas/domains/terraform/hcl.py +538 -0
- code_atlas/domains/terraform/refs.py +181 -0
- code_atlas/domains/terraform/tfjson.py +163 -0
- code_atlas/domains/terraform/tfvars.py +98 -0
- code_atlas/domains/validation.py +120 -0
- code_atlas/env_override.py +27 -0
- code_atlas/eval/__init__.py +26 -0
- code_atlas/eval/domain_eval.py +681 -0
- code_atlas/eval/ground_truth.py +688 -0
- code_atlas/eval/impact_truth.py +758 -0
- code_atlas/eval/metrics.py +178 -0
- code_atlas/eval/pack_value.py +610 -0
- code_atlas/federation/__init__.py +72 -0
- code_atlas/federation/federation.py +676 -0
- code_atlas/federation/model.py +355 -0
- code_atlas/federation/store.py +194 -0
- code_atlas/gitbin.py +48 -0
- code_atlas/graph/__init__.py +20 -0
- code_atlas/graph/builder.py +378 -0
- code_atlas/graph/identity.py +54 -0
- code_atlas/graph/queries.py +47 -0
- code_atlas/graph/validation.py +92 -0
- code_atlas/impact/__init__.py +37 -0
- code_atlas/impact/blast_radius.py +171 -0
- code_atlas/impact/contract_analyzer.py +104 -0
- code_atlas/impact/contracts.py +315 -0
- code_atlas/impact/diff_reader.py +142 -0
- code_atlas/impact/domain_traversal.py +768 -0
- code_atlas/impact/engine.py +392 -0
- code_atlas/impact/packs.py +128 -0
- code_atlas/impact/report.py +136 -0
- code_atlas/impact/revision_binding.py +220 -0
- code_atlas/impact/risk_classifier.py +83 -0
- code_atlas/impact/snapshot_resolver.py +136 -0
- code_atlas/impact/symbol_diff.py +210 -0
- code_atlas/impact/test_selector.py +166 -0
- code_atlas/indexing/__init__.py +5 -0
- code_atlas/indexing/cache.py +396 -0
- code_atlas/indexing/change_detector.py +278 -0
- code_atlas/indexing/coordinator.py +1032 -0
- code_atlas/indexing/dependency_planner.py +268 -0
- code_atlas/indexing/persist.py +436 -0
- code_atlas/indexing/reuse.py +85 -0
- code_atlas/install/__init__.py +7 -0
- code_atlas/install/cli.py +168 -0
- code_atlas/install/lifecycle.py +286 -0
- code_atlas/mcp.py +674 -0
- code_atlas/observability/__init__.py +19 -0
- code_atlas/observability/logging.py +199 -0
- code_atlas/parsers/__init__.py +43 -0
- code_atlas/parsers/contracts.py +209 -0
- code_atlas/parsers/engine.py +125 -0
- code_atlas/parsers/languages/__init__.py +1 -0
- code_atlas/parsers/languages/python.py +391 -0
- code_atlas/parsers/languages/tsx.py +28 -0
- code_atlas/parsers/languages/typescript.py +909 -0
- code_atlas/parsers/registry.py +51 -0
- code_atlas/parsers/tree_sitter_runtime.py +118 -0
- code_atlas/resolvers/__init__.py +33 -0
- code_atlas/resolvers/bootstrap.py +246 -0
- code_atlas/resolvers/capabilities.py +33 -0
- code_atlas/resolvers/contracts.py +133 -0
- code_atlas/resolvers/coordinator.py +96 -0
- code_atlas/resolvers/enrichment.py +318 -0
- code_atlas/resolvers/lsp/__init__.py +26 -0
- code_atlas/resolvers/lsp/adapter.py +256 -0
- code_atlas/resolvers/lsp/lifecycle.py +251 -0
- code_atlas/resolvers/lsp/spawn.py +92 -0
- code_atlas/resolvers/lsp/transport.py +240 -0
- code_atlas/resolvers/registry.py +33 -0
- code_atlas/resolvers/scip/__init__.py +49 -0
- code_atlas/resolvers/scip/decoder.py +216 -0
- code_atlas/resolvers/scip/importer.py +248 -0
- code_atlas/resolvers/scip/loader.py +90 -0
- code_atlas/resolvers/scip/mapper.py +271 -0
- code_atlas/resolvers/scip/provenance.py +211 -0
- code_atlas/resolvers/scip/resolver.py +289 -0
- code_atlas/resolvers/scip/validation.py +101 -0
- code_atlas/resolvers/syntactic/__init__.py +8 -0
- code_atlas/resolvers/syntactic/resolver.py +254 -0
- code_atlas/search/__init__.py +32 -0
- code_atlas/search/contracts.py +176 -0
- code_atlas/search/domain.py +561 -0
- code_atlas/search/engine.py +530 -0
- code_atlas/search/federated.py +276 -0
- code_atlas/search/graph.py +282 -0
- code_atlas/search/hybrid.py +151 -0
- code_atlas/search/intent.py +48 -0
- code_atlas/search/lexical.py +176 -0
- code_atlas/search/refs.py +359 -0
- code_atlas/search/structural.py +505 -0
- code_atlas/search/tokenizer.py +43 -0
- code_atlas/search/vector/__init__.py +35 -0
- code_atlas/search/vector/contracts.py +76 -0
- code_atlas/search/vector/embeddings.py +96 -0
- code_atlas/search/vector/index.py +169 -0
- code_atlas/search/vector/provider.py +50 -0
- code_atlas/storage/__init__.py +14 -0
- code_atlas/storage/db.py +47 -0
- code_atlas/storage/migrations.py +552 -0
- code_atlas/storage/store.py +1045 -0
- code_atlas/ui/__init__.py +29 -0
- code_atlas/ui/i18n.py +143 -0
- code_atlas/ui/kit.py +689 -0
- code_atlas/ui/wizard.py +317 -0
- code_atlas_forge-1.1.0.dist-info/METADATA +246 -0
- code_atlas_forge-1.1.0.dist-info/RECORD +181 -0
- code_atlas_forge-1.1.0.dist-info/WHEEL +5 -0
- code_atlas_forge-1.1.0.dist-info/entry_points.txt +2 -0
- code_atlas_forge-1.1.0.dist-info/licenses/LICENSE +202 -0
- code_atlas_forge-1.1.0.dist-info/licenses/NOTICE +17 -0
- code_atlas_forge-1.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
"""Rotas declaradas em código — FastAPI (Python) e Express (TS/JS) (lf-0114).
|
|
2
|
+
|
|
3
|
+
FastAPI (.py): consome artefatos de AST reais — ``ParseResult`` do
|
|
4
|
+
``ParseEngine`` (Tree-sitter vive atrás da fronteira ``parsers/``).
|
|
5
|
+
Decoradores ``@app.get("/x")``/``@router.post(...)`` chegam como texto de
|
|
6
|
+
decorator nos símbolos (``decorated_definition``); receivers
|
|
7
|
+
``FastAPI()``/``APIRouter(prefix=...)`` e ``include_router`` chegam como
|
|
8
|
+
referências ``call`` com span real — argumentos são fatiados do código
|
|
9
|
+
fonte na posição do callee (nunca regex solto em texto bruto).
|
|
10
|
+
|
|
11
|
+
Express (.ts/.tsx): mesmo pipeline de AST; convenção de receiver
|
|
12
|
+
(``app``/``router``/vars de ``express()``/``Router()``) → confiança
|
|
13
|
+
``heuristic`` declarada. ``.js``/``.jsx``/``.mjs``/``.cjs`` não têm
|
|
14
|
+
adaptador Tree-sitter neste repo — regex-static honesto por linha.
|
|
15
|
+
|
|
16
|
+
Relações emitidas:
|
|
17
|
+
|
|
18
|
+
- ``APIService --CONTAINS--> APIEndpoint`` (resolvido internamente);
|
|
19
|
+
- ``<handler symbol> --DEFINES--> APIEndpoint`` — ponte código→domínio
|
|
20
|
+
(``EXPOSES`` não consta do vocabulário fechado de ``DomainRelationKind``;
|
|
21
|
+
``DEFINES`` é o membro honesto: o handler literalmente define a rota);
|
|
22
|
+
- ``<app service> --CONTAINS--> <router service>`` para
|
|
23
|
+
``include_router``/``use`` observados no mesmo arquivo.
|
|
24
|
+
|
|
25
|
+
Limitações declaradas: prefixo de ``include_router`` só se aplica quando
|
|
26
|
+
o router var é declarado no mesmo arquivo (cross-file → diagnóstico);
|
|
27
|
+
paths de rota não-literais (f-string, variável) → diagnóstico
|
|
28
|
+
``dynamic-route-path`` e nenhum endpoint inventado.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import re
|
|
34
|
+
from dataclasses import dataclass
|
|
35
|
+
|
|
36
|
+
from code_atlas.contracts.enums import (
|
|
37
|
+
ConfidenceClass,
|
|
38
|
+
EdgeKind,
|
|
39
|
+
ResolutionStatus,
|
|
40
|
+
)
|
|
41
|
+
from code_atlas.domains.api import common
|
|
42
|
+
from code_atlas.domains.api.emit import Emit
|
|
43
|
+
from code_atlas.domains.identity import DomainIdentity
|
|
44
|
+
from code_atlas.domains.model import DomainEntityKind
|
|
45
|
+
from code_atlas.parsers.contracts import ParseResult, SourceSpan
|
|
46
|
+
|
|
47
|
+
HTTP_METHODS = frozenset(
|
|
48
|
+
{"get", "put", "post", "delete", "options", "head", "patch", "trace"}
|
|
49
|
+
)
|
|
50
|
+
_ROUTE_DEC = re.compile(
|
|
51
|
+
r"^([\w.]+)\.(get|put|post|delete|options|head|patch|trace"
|
|
52
|
+
r"|route|api_route|websocket|webhooks)\s*\("
|
|
53
|
+
)
|
|
54
|
+
_PY_CTOR = re.compile(r"^(?:[\w.]+\.)?(FastAPI|APIRouter|Flask|Blueprint)$")
|
|
55
|
+
_PY_VAR_LHS = re.compile(r"^\s*([A-Za-z_]\w*)\s*(?::[^=]+)?=")
|
|
56
|
+
_TS_CTOR = re.compile(r"^(?:express\.Router|express|Router)$")
|
|
57
|
+
_TS_VAR_LHS = re.compile(
|
|
58
|
+
r"^\s*(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*="
|
|
59
|
+
)
|
|
60
|
+
_SIMPLE_RECV = re.compile(r"^[A-Za-z_$][\w$]*$")
|
|
61
|
+
_JS_ROUTE = re.compile(
|
|
62
|
+
r"^\s*([A-Za-z_$][\w$]*)\."
|
|
63
|
+
r"(get|put|post|delete|options|head|patch|all|use)\s*\("
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True)
|
|
68
|
+
class _Route:
|
|
69
|
+
receiver: str
|
|
70
|
+
method: str # UPPER ou "ANY"
|
|
71
|
+
path: str
|
|
72
|
+
span: SourceSpan | None
|
|
73
|
+
handler: str # qname do símbolo que define a rota
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class _ServiceInfo:
|
|
78
|
+
kind: str # "app" | "router" | "unknown"
|
|
79
|
+
prefix: str # prefixo declarado (APIRouter/root_path)
|
|
80
|
+
mount: str # prefixo de mount (include_router/app.use)
|
|
81
|
+
declared: bool
|
|
82
|
+
span: SourceSpan | None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _args_at(text: str, span_end_byte: int) -> str | None:
|
|
86
|
+
"""Argumentos ``(...)`` logo após o fim do callee (byte offset)."""
|
|
87
|
+
pos = common.byte_to_char(text, span_end_byte)
|
|
88
|
+
return common.args_after(text, pos)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _line_at(text: str, lineno: int) -> str:
|
|
92
|
+
lines = text.splitlines()
|
|
93
|
+
return lines[lineno - 1] if 1 <= lineno <= len(lines) else ""
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _method_lit(token: str, *, lang: str) -> str | None:
|
|
97
|
+
lit = common.parse_str_literal(token, lang=lang)
|
|
98
|
+
if lit is None or lit.dynamic:
|
|
99
|
+
return None
|
|
100
|
+
m = lit.value.strip().upper()
|
|
101
|
+
return m if m in {x.upper() for x in HTTP_METHODS} else None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _first_lit(args: list[str], *, lang: str) -> common.StrLiteral | None:
|
|
105
|
+
if not args:
|
|
106
|
+
return None
|
|
107
|
+
return common.parse_str_literal(args[0], lang=lang)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# -- FastAPI / Python ------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _py_receiver_maps(
|
|
114
|
+
sink: Emit, path: str, text: str, result: ParseResult
|
|
115
|
+
) -> tuple[dict[str, _ServiceInfo], list[tuple[str, str, str]]]:
|
|
116
|
+
"""Vars ``X = FastAPI()/APIRouter()`` e ``X.include_router(Y, ...)``."""
|
|
117
|
+
services: dict[str, _ServiceInfo] = {}
|
|
118
|
+
includes: list[tuple[str, str, str]] = [] # (app_var, router_var, prefix)
|
|
119
|
+
for ref in result.references:
|
|
120
|
+
if ref.context != "call":
|
|
121
|
+
continue
|
|
122
|
+
callee = ref.name.strip()
|
|
123
|
+
m = _PY_CTOR.match(callee)
|
|
124
|
+
if m:
|
|
125
|
+
lhs = _PY_VAR_LHS.match(_line_at(text, ref.span.start_line))
|
|
126
|
+
if not lhs:
|
|
127
|
+
continue
|
|
128
|
+
args = common.split_args(_args_at(text, ref.span.end_byte) or "")
|
|
129
|
+
kw = "prefix" if m.group(1) in ("APIRouter", "Blueprint") else "root_path"
|
|
130
|
+
prefix_lit = common.parse_str_literal(
|
|
131
|
+
common.kwarg(args, kw) or "", lang="py"
|
|
132
|
+
)
|
|
133
|
+
prefix = (
|
|
134
|
+
prefix_lit.value if prefix_lit and not prefix_lit.dynamic else ""
|
|
135
|
+
)
|
|
136
|
+
var = lhs.group(1)
|
|
137
|
+
services[var] = _ServiceInfo(
|
|
138
|
+
kind="router" if m.group(1) in ("APIRouter", "Blueprint") else "app",
|
|
139
|
+
prefix=prefix,
|
|
140
|
+
mount="",
|
|
141
|
+
declared=True,
|
|
142
|
+
span=ref.span,
|
|
143
|
+
)
|
|
144
|
+
continue
|
|
145
|
+
if callee.endswith(".include_router"):
|
|
146
|
+
app_var = callee[: -len(".include_router")].rsplit(".", 1)[-1]
|
|
147
|
+
args_text = _args_at(text, ref.span.end_byte) or ""
|
|
148
|
+
args = common.split_args(args_text)
|
|
149
|
+
if not args:
|
|
150
|
+
continue
|
|
151
|
+
router_var = args[0].strip()
|
|
152
|
+
if not _SIMPLE_RECV.match(router_var):
|
|
153
|
+
continue
|
|
154
|
+
prefix_lit = common.parse_str_literal(
|
|
155
|
+
common.kwarg(args, "prefix") or "", lang="py"
|
|
156
|
+
)
|
|
157
|
+
prefix = (
|
|
158
|
+
prefix_lit.value if prefix_lit and not prefix_lit.dynamic else ""
|
|
159
|
+
)
|
|
160
|
+
includes.append((app_var, router_var, prefix))
|
|
161
|
+
return services, includes
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _py_routes(result: ParseResult) -> tuple[list[_Route], int]:
|
|
165
|
+
"""Rotas a partir dos decorators reais extraídos pelo Tree-sitter."""
|
|
166
|
+
routes: list[_Route] = []
|
|
167
|
+
skipped_nonhttp = 0
|
|
168
|
+
for sym in result.symbols:
|
|
169
|
+
for dec in sym.decorators:
|
|
170
|
+
m = _ROUTE_DEC.match(dec.strip())
|
|
171
|
+
if not m:
|
|
172
|
+
continue
|
|
173
|
+
recv, verb = m.group(1), m.group(2)
|
|
174
|
+
if verb in ("websocket", "webhooks"):
|
|
175
|
+
skipped_nonhttp += 1
|
|
176
|
+
continue
|
|
177
|
+
open_paren = dec.find("(", m.end() - 1)
|
|
178
|
+
args = common.split_args(common.scan_args(dec, open_paren) or "")
|
|
179
|
+
lit = _first_lit(args, lang="py")
|
|
180
|
+
if lit is None or lit.dynamic:
|
|
181
|
+
routes.append(
|
|
182
|
+
_Route(recv, "?", "", sym.span, sym.qualified_name)
|
|
183
|
+
)
|
|
184
|
+
continue
|
|
185
|
+
if verb in ("route", "api_route"):
|
|
186
|
+
methods_raw = common.kwarg(args, "methods") or ""
|
|
187
|
+
methods = re.findall(r"['\"](\w+)['\"]", methods_raw)
|
|
188
|
+
if not methods:
|
|
189
|
+
methods = ["ANY"]
|
|
190
|
+
for mm in methods:
|
|
191
|
+
routes.append(
|
|
192
|
+
_Route(
|
|
193
|
+
recv, mm.upper(), lit.value,
|
|
194
|
+
sym.span, sym.qualified_name,
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
continue
|
|
198
|
+
routes.append(
|
|
199
|
+
_Route(recv, verb.upper(), lit.value,
|
|
200
|
+
sym.span, sym.qualified_name)
|
|
201
|
+
)
|
|
202
|
+
return routes, skipped_nonhttp
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def extract_py_routes(
|
|
206
|
+
*,
|
|
207
|
+
sink: Emit,
|
|
208
|
+
path: str,
|
|
209
|
+
text: str,
|
|
210
|
+
result: ParseResult,
|
|
211
|
+
module: str,
|
|
212
|
+
content_hash: str,
|
|
213
|
+
) -> None:
|
|
214
|
+
"""FastAPI: decorators + receivers via AST real — ``syntactic``."""
|
|
215
|
+
services, includes = _py_receiver_maps(sink, path, text, result)
|
|
216
|
+
routes, skipped_nonhttp = _py_routes(result)
|
|
217
|
+
if skipped_nonhttp:
|
|
218
|
+
sink.diag(
|
|
219
|
+
"non-http-decorator", "info",
|
|
220
|
+
f"{skipped_nonhttp} websocket/webhooks decorator(s) skipped "
|
|
221
|
+
f"in {path} — not HTTP API endpoints",
|
|
222
|
+
path,
|
|
223
|
+
)
|
|
224
|
+
_emit_routes(
|
|
225
|
+
sink=sink, path=path, text=text, module=module,
|
|
226
|
+
content_hash=content_hash, services=services, includes=includes,
|
|
227
|
+
routes=routes, framework="fastapi",
|
|
228
|
+
confidence=ConfidenceClass.SYNTACTIC,
|
|
229
|
+
source="fastapi-decorator",
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# -- Express / TypeScript -----------------------------------------------------
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _ts_receiver_maps(
|
|
237
|
+
text: str, result: ParseResult
|
|
238
|
+
) -> tuple[dict[str, _ServiceInfo], list[tuple[str, str, str]]]:
|
|
239
|
+
"""``X = express()/Router()`` e ``X.use("/p", Y)`` — spans reais."""
|
|
240
|
+
services: dict[str, _ServiceInfo] = {}
|
|
241
|
+
mounts: list[tuple[str, str, str]] = [] # (app_var, router_var, prefix)
|
|
242
|
+
for ref in result.references:
|
|
243
|
+
if ref.context != "call":
|
|
244
|
+
continue
|
|
245
|
+
callee = ref.name.strip()
|
|
246
|
+
if _TS_CTOR.match(callee):
|
|
247
|
+
lhs = _TS_VAR_LHS.match(_line_at(text, ref.span.start_line))
|
|
248
|
+
if not lhs:
|
|
249
|
+
continue
|
|
250
|
+
kind = "router" if callee.endswith("Router") else "app"
|
|
251
|
+
services[lhs.group(1)] = _ServiceInfo(
|
|
252
|
+
kind=kind, prefix="", mount="",
|
|
253
|
+
declared=True, span=ref.span,
|
|
254
|
+
)
|
|
255
|
+
continue
|
|
256
|
+
if callee.endswith(".use"):
|
|
257
|
+
recv = callee[: -len(".use")]
|
|
258
|
+
if not _SIMPLE_RECV.match(recv):
|
|
259
|
+
continue
|
|
260
|
+
args = common.split_args(_args_at(text, ref.span.end_byte) or "")
|
|
261
|
+
if len(args) < 2:
|
|
262
|
+
continue
|
|
263
|
+
mount_lit = common.parse_str_literal(args[0], lang="ts")
|
|
264
|
+
router_var = args[1].strip()
|
|
265
|
+
if (
|
|
266
|
+
mount_lit and not mount_lit.dynamic
|
|
267
|
+
and _SIMPLE_RECV.match(router_var)
|
|
268
|
+
):
|
|
269
|
+
mounts.append((recv, router_var, mount_lit.value))
|
|
270
|
+
continue
|
|
271
|
+
return services, mounts
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _ts_routes(
|
|
275
|
+
text: str,
|
|
276
|
+
result: ParseResult,
|
|
277
|
+
module: str,
|
|
278
|
+
services: dict[str, _ServiceInfo],
|
|
279
|
+
) -> list[_Route]:
|
|
280
|
+
routes: list[_Route] = []
|
|
281
|
+
for ref in result.references:
|
|
282
|
+
if ref.context != "call":
|
|
283
|
+
continue
|
|
284
|
+
callee = ref.name.strip()
|
|
285
|
+
base, _, verb = callee.rpartition(".")
|
|
286
|
+
if verb not in HTTP_METHODS and verb != "all":
|
|
287
|
+
continue
|
|
288
|
+
if not _SIMPLE_RECV.match(base):
|
|
289
|
+
continue
|
|
290
|
+
if base not in services and base not in ("app", "router"):
|
|
291
|
+
continue
|
|
292
|
+
args = common.split_args(_args_at(text, ref.span.end_byte) or "")
|
|
293
|
+
lit = _first_lit(args, lang="ts")
|
|
294
|
+
scope = ".".join(ref.scope_path)
|
|
295
|
+
caller = f"{module}.{scope}" if scope else module
|
|
296
|
+
if lit is None or lit.dynamic:
|
|
297
|
+
routes.append(_Route(base, "?", "", ref.span, caller))
|
|
298
|
+
continue
|
|
299
|
+
method = "ANY" if verb == "all" else verb.upper()
|
|
300
|
+
routes.append(_Route(base, method, lit.value, ref.span, caller))
|
|
301
|
+
return routes
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def extract_ts_routes(
|
|
305
|
+
*,
|
|
306
|
+
sink: Emit,
|
|
307
|
+
path: str,
|
|
308
|
+
text: str,
|
|
309
|
+
result: ParseResult,
|
|
310
|
+
module: str,
|
|
311
|
+
content_hash: str,
|
|
312
|
+
) -> None:
|
|
313
|
+
"""Express em .ts/.tsx: AST real + convenção de receiver — ``heuristic``."""
|
|
314
|
+
services, mounts = _ts_receiver_maps(text, result)
|
|
315
|
+
routes = _ts_routes(text, result, module, services)
|
|
316
|
+
_emit_routes(
|
|
317
|
+
sink=sink, path=path, text=text, module=module,
|
|
318
|
+
content_hash=content_hash, services=services, includes=mounts,
|
|
319
|
+
routes=routes, framework="express",
|
|
320
|
+
confidence=ConfidenceClass.HEURISTIC,
|
|
321
|
+
source="express-literal",
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
# -- Express / JavaScript regex-static (sem adaptador Tree-sitter) -----------
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def extract_js_routes(
|
|
329
|
+
*,
|
|
330
|
+
sink: Emit,
|
|
331
|
+
path: str,
|
|
332
|
+
text: str,
|
|
333
|
+
module: str,
|
|
334
|
+
content_hash: str,
|
|
335
|
+
) -> None:
|
|
336
|
+
"""Regex-static honesto para .js — sem AST; sempre ``heuristic``."""
|
|
337
|
+
services: dict[str, _ServiceInfo] = {}
|
|
338
|
+
mounts: list[tuple[str, str, str]] = []
|
|
339
|
+
routes: list[_Route] = []
|
|
340
|
+
lines = text.splitlines()
|
|
341
|
+
byte_offs = common.byte_offsets(text)
|
|
342
|
+
for lineno, line in enumerate(lines, 1):
|
|
343
|
+
vm = _TS_VAR_LHS.match(line)
|
|
344
|
+
ctor = re.search(
|
|
345
|
+
r"=\s*(?:[\w.]+\.)?(express\.Router|express|Router)\s*\(", line
|
|
346
|
+
)
|
|
347
|
+
if vm and ctor:
|
|
348
|
+
services[vm.group(1)] = _ServiceInfo(
|
|
349
|
+
kind="router" if ctor.group(1).endswith("Router") else "app",
|
|
350
|
+
prefix="", mount="", declared=True,
|
|
351
|
+
span=common.line_span(text, byte_offs, lineno),
|
|
352
|
+
)
|
|
353
|
+
continue
|
|
354
|
+
rm = _JS_ROUTE.match(line)
|
|
355
|
+
if not rm:
|
|
356
|
+
continue
|
|
357
|
+
recv, verb = rm.group(1), rm.group(2).lower()
|
|
358
|
+
args = common.split_args(common.scan_args(line, rm.end() - 1) or "")
|
|
359
|
+
span = common.line_span(text, byte_offs, lineno)
|
|
360
|
+
if verb == "use":
|
|
361
|
+
if len(args) >= 2:
|
|
362
|
+
mount_lit = common.parse_str_literal(args[0], lang="ts")
|
|
363
|
+
router_var = args[1].strip()
|
|
364
|
+
if (
|
|
365
|
+
mount_lit and not mount_lit.dynamic
|
|
366
|
+
and _SIMPLE_RECV.match(router_var)
|
|
367
|
+
):
|
|
368
|
+
mounts.append((recv, router_var, mount_lit.value))
|
|
369
|
+
continue
|
|
370
|
+
lit = _first_lit(args, lang="ts")
|
|
371
|
+
if lit is None or lit.dynamic:
|
|
372
|
+
routes.append(_Route(recv, "?", "", span, module))
|
|
373
|
+
continue
|
|
374
|
+
method = "ANY" if verb == "all" else verb.upper()
|
|
375
|
+
routes.append(_Route(recv, method, lit.value, span, module))
|
|
376
|
+
_emit_routes(
|
|
377
|
+
sink=sink, path=path, text=text, module=module,
|
|
378
|
+
content_hash=content_hash, services=services, includes=mounts,
|
|
379
|
+
routes=routes, framework="express",
|
|
380
|
+
confidence=ConfidenceClass.HEURISTIC,
|
|
381
|
+
source="express-regex",
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
# -- emissão compartilhada ----------------------------------------------------
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _mount_prefixes(
|
|
389
|
+
*,
|
|
390
|
+
sink: Emit,
|
|
391
|
+
path: str,
|
|
392
|
+
services: dict[str, _ServiceInfo],
|
|
393
|
+
includes: list[tuple[str, str, str]],
|
|
394
|
+
) -> dict[str, str]:
|
|
395
|
+
"""``router_var`` → prefixo de mount (primeiro observado no arquivo)."""
|
|
396
|
+
mount_of: dict[str, list[tuple[str, str]]] = {}
|
|
397
|
+
for app_var, router_var, prefix in includes:
|
|
398
|
+
mount_of.setdefault(router_var, []).append((app_var, prefix))
|
|
399
|
+
if router_var not in services:
|
|
400
|
+
sink.diag(
|
|
401
|
+
"cross-file-router-prefix", "info",
|
|
402
|
+
f"{router_var!r} mounted but not declared in {path} — "
|
|
403
|
+
"prefix may be incomplete",
|
|
404
|
+
path,
|
|
405
|
+
)
|
|
406
|
+
mount_str: dict[str, str] = {}
|
|
407
|
+
for router_var, entries in mount_of.items():
|
|
408
|
+
if len(entries) > 1:
|
|
409
|
+
sink.diag(
|
|
410
|
+
"multi-mount", "info",
|
|
411
|
+
f"{router_var!r} mounted {len(entries)}x — first prefix used",
|
|
412
|
+
path,
|
|
413
|
+
)
|
|
414
|
+
mount_str[router_var] = entries[0][1]
|
|
415
|
+
return mount_str
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _emit_routes(
|
|
419
|
+
*,
|
|
420
|
+
sink: Emit,
|
|
421
|
+
path: str,
|
|
422
|
+
text: str,
|
|
423
|
+
module: str,
|
|
424
|
+
content_hash: str,
|
|
425
|
+
services: dict[str, _ServiceInfo],
|
|
426
|
+
includes: list[tuple[str, str, str]],
|
|
427
|
+
routes: list[_Route],
|
|
428
|
+
framework: str,
|
|
429
|
+
confidence: ConfidenceClass,
|
|
430
|
+
source: str,
|
|
431
|
+
) -> None:
|
|
432
|
+
mount_str = _mount_prefixes(
|
|
433
|
+
sink=sink, path=path, services=services, includes=includes,
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
svc_idents: dict[str, DomainIdentity] = {}
|
|
437
|
+
|
|
438
|
+
def _service_ident(recv: str) -> DomainIdentity:
|
|
439
|
+
cached = svc_idents.get(recv)
|
|
440
|
+
if cached is not None:
|
|
441
|
+
return cached
|
|
442
|
+
info = services.get(recv)
|
|
443
|
+
if info is None:
|
|
444
|
+
info = _ServiceInfo("unknown", "", "", False, None)
|
|
445
|
+
services[recv] = info
|
|
446
|
+
sink.diag(
|
|
447
|
+
"undeclared-receiver", "info",
|
|
448
|
+
f"route receiver {recv!r} has no {framework} constructor "
|
|
449
|
+
f"in {path}",
|
|
450
|
+
path,
|
|
451
|
+
)
|
|
452
|
+
mount = mount_str.get(recv, "")
|
|
453
|
+
qname = f"{module}#{recv}"
|
|
454
|
+
ident = sink.entity(
|
|
455
|
+
kind=DomainEntityKind.API_SERVICE,
|
|
456
|
+
name=recv,
|
|
457
|
+
qualified_name=qname,
|
|
458
|
+
path=path,
|
|
459
|
+
span=info.span,
|
|
460
|
+
content_hash=content_hash,
|
|
461
|
+
attributes={
|
|
462
|
+
"framework": framework,
|
|
463
|
+
"receiver": recv,
|
|
464
|
+
"prefix": info.prefix,
|
|
465
|
+
"mounted_prefix": mount,
|
|
466
|
+
"source": source,
|
|
467
|
+
"receiver_declared": "true" if info.declared else "false",
|
|
468
|
+
},
|
|
469
|
+
evidence=(
|
|
470
|
+
sink.evi(path, content_hash, info.span, f"receiver {recv}"),
|
|
471
|
+
) if info.span else (),
|
|
472
|
+
)
|
|
473
|
+
svc_idents[recv] = ident
|
|
474
|
+
sink.bump("api_services")
|
|
475
|
+
return ident
|
|
476
|
+
|
|
477
|
+
# CONTAINS app→router para mounts observados.
|
|
478
|
+
for app_var, router_var, _mount in includes:
|
|
479
|
+
app_ident = _service_ident(app_var)
|
|
480
|
+
router_ident = _service_ident(router_var)
|
|
481
|
+
span = services[router_var].span or services[app_var].span
|
|
482
|
+
sink.relation(
|
|
483
|
+
relation_type=EdgeKind.CONTAINS,
|
|
484
|
+
path=path,
|
|
485
|
+
from_identity=app_ident,
|
|
486
|
+
to_identity=router_ident,
|
|
487
|
+
resolution=sink.resolution(
|
|
488
|
+
ResolutionStatus.RESOLVED, confidence,
|
|
489
|
+
target=router_ident,
|
|
490
|
+
),
|
|
491
|
+
span=span,
|
|
492
|
+
evidence=(sink.evi(path, content_hash, span),) if span else (),
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
for route in routes:
|
|
496
|
+
recv_ident = _service_ident(route.receiver)
|
|
497
|
+
if route.method == "?" or not route.path:
|
|
498
|
+
sink.diag(
|
|
499
|
+
"dynamic-route-path", "warning",
|
|
500
|
+
f"non-literal route path on {route.receiver!r} in {path} "
|
|
501
|
+
f"— no endpoint identity emitted",
|
|
502
|
+
path,
|
|
503
|
+
)
|
|
504
|
+
continue
|
|
505
|
+
_emit_route_endpoint(
|
|
506
|
+
sink=sink,
|
|
507
|
+
path=path,
|
|
508
|
+
module=module,
|
|
509
|
+
content_hash=content_hash,
|
|
510
|
+
route=route,
|
|
511
|
+
recv_ident=recv_ident,
|
|
512
|
+
full_prefix=common.join_paths(
|
|
513
|
+
mount_str.get(route.receiver, ""),
|
|
514
|
+
services[route.receiver].prefix,
|
|
515
|
+
),
|
|
516
|
+
framework=framework,
|
|
517
|
+
confidence=confidence,
|
|
518
|
+
source=source,
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def _emit_route_endpoint(
|
|
523
|
+
*,
|
|
524
|
+
sink: Emit,
|
|
525
|
+
path: str,
|
|
526
|
+
module: str,
|
|
527
|
+
content_hash: str,
|
|
528
|
+
route: _Route,
|
|
529
|
+
recv_ident: DomainIdentity,
|
|
530
|
+
full_prefix: str,
|
|
531
|
+
framework: str,
|
|
532
|
+
confidence: ConfidenceClass,
|
|
533
|
+
source: str,
|
|
534
|
+
) -> None:
|
|
535
|
+
"""APIEndpoint + CONTAINS (serviço→rota) + DEFINES (handler→rota)."""
|
|
536
|
+
# mount vem antes do prefixo declarado: app.include_router(r, "/api")
|
|
537
|
+
# + APIRouter(prefix="/v1") + @r.get("/x") → /api/v1/x.
|
|
538
|
+
full_path = common.join_paths(full_prefix, route.path)
|
|
539
|
+
ep = sink.endpoint(
|
|
540
|
+
name=f"{route.method} {route.path}",
|
|
541
|
+
qualified_name=f"{route.method} {full_path}",
|
|
542
|
+
namespace=f"{module}#{route.receiver}",
|
|
543
|
+
full_path=full_path,
|
|
544
|
+
path=path,
|
|
545
|
+
span=route.span,
|
|
546
|
+
content_hash=content_hash,
|
|
547
|
+
attributes={
|
|
548
|
+
"method": route.method,
|
|
549
|
+
"path": route.path,
|
|
550
|
+
"full_path": full_path,
|
|
551
|
+
"framework": framework,
|
|
552
|
+
"handler": route.handler,
|
|
553
|
+
"source": source,
|
|
554
|
+
},
|
|
555
|
+
evidence=(sink.evi(path, content_hash, route.span),)
|
|
556
|
+
if route.span else (),
|
|
557
|
+
)
|
|
558
|
+
sink.bump("api_endpoints")
|
|
559
|
+
sink.relation(
|
|
560
|
+
relation_type=EdgeKind.CONTAINS,
|
|
561
|
+
path=path,
|
|
562
|
+
from_identity=recv_ident,
|
|
563
|
+
to_identity=ep,
|
|
564
|
+
resolution=sink.resolution(
|
|
565
|
+
ResolutionStatus.RESOLVED, confidence, target=ep,
|
|
566
|
+
),
|
|
567
|
+
span=route.span,
|
|
568
|
+
evidence=(sink.evi(path, content_hash, route.span),)
|
|
569
|
+
if route.span else (),
|
|
570
|
+
)
|
|
571
|
+
sink.relation(
|
|
572
|
+
relation_type=EdgeKind.DEFINES,
|
|
573
|
+
path=path,
|
|
574
|
+
from_symbol=route.handler,
|
|
575
|
+
to_identity=ep,
|
|
576
|
+
resolution=sink.resolution(
|
|
577
|
+
ResolutionStatus.RESOLVED, confidence, target=ep,
|
|
578
|
+
),
|
|
579
|
+
span=route.span,
|
|
580
|
+
evidence=(sink.evi(path, content_hash, route.span),)
|
|
581
|
+
if route.span else (),
|
|
582
|
+
)
|