seer-mcp 0.1.0
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.
- package/.vscode/settings.json +3 -0
- package/LICENSE +176 -0
- package/README.md +272 -0
- package/README_dev.md +199 -0
- package/dist/bundle/ci.d.ts +47 -0
- package/dist/bundle/ci.d.ts.map +1 -0
- package/dist/bundle/ci.js +113 -0
- package/dist/bundle/ci.js.map +1 -0
- package/dist/bundle/contract.d.ts +111 -0
- package/dist/bundle/contract.d.ts.map +1 -0
- package/dist/bundle/contract.js +352 -0
- package/dist/bundle/contract.js.map +1 -0
- package/dist/bundle/export.d.ts +36 -0
- package/dist/bundle/export.d.ts.map +1 -0
- package/dist/bundle/export.js +152 -0
- package/dist/bundle/export.js.map +1 -0
- package/dist/bundle/external.d.ts +66 -0
- package/dist/bundle/external.d.ts.map +1 -0
- package/dist/bundle/external.js +238 -0
- package/dist/bundle/external.js.map +1 -0
- package/dist/bundle/format.d.ts +94 -0
- package/dist/bundle/format.d.ts.map +1 -0
- package/dist/bundle/format.js +42 -0
- package/dist/bundle/format.js.map +1 -0
- package/dist/bundle/import.d.ts +49 -0
- package/dist/bundle/import.d.ts.map +1 -0
- package/dist/bundle/import.js +116 -0
- package/dist/bundle/import.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +1402 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.d.ts +48 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +284 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/db/schema.d.ts +3 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +616 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/db/store.d.ts +1011 -0
- package/dist/db/store.d.ts.map +1 -0
- package/dist/db/store.js +3888 -0
- package/dist/db/store.js.map +1 -0
- package/dist/graph/pagerank.d.ts +9 -0
- package/dist/graph/pagerank.d.ts.map +1 -0
- package/dist/graph/pagerank.js +47 -0
- package/dist/graph/pagerank.js.map +1 -0
- package/dist/indexer/architecture.d.ts +72 -0
- package/dist/indexer/architecture.d.ts.map +1 -0
- package/dist/indexer/architecture.js +112 -0
- package/dist/indexer/architecture.js.map +1 -0
- package/dist/indexer/behavior.d.ts +75 -0
- package/dist/indexer/behavior.d.ts.map +1 -0
- package/dist/indexer/behavior.js +395 -0
- package/dist/indexer/behavior.js.map +1 -0
- package/dist/indexer/boundaries.d.ts +60 -0
- package/dist/indexer/boundaries.d.ts.map +1 -0
- package/dist/indexer/boundaries.js +366 -0
- package/dist/indexer/boundaries.js.map +1 -0
- package/dist/indexer/churn.d.ts +15 -0
- package/dist/indexer/churn.d.ts.map +1 -0
- package/dist/indexer/churn.js +49 -0
- package/dist/indexer/churn.js.map +1 -0
- package/dist/indexer/classify.d.ts +9 -0
- package/dist/indexer/classify.d.ts.map +1 -0
- package/dist/indexer/classify.js +90 -0
- package/dist/indexer/classify.js.map +1 -0
- package/dist/indexer/context.d.ts +176 -0
- package/dist/indexer/context.d.ts.map +1 -0
- package/dist/indexer/context.js +193 -0
- package/dist/indexer/context.js.map +1 -0
- package/dist/indexer/continuity.d.ts +67 -0
- package/dist/indexer/continuity.d.ts.map +1 -0
- package/dist/indexer/continuity.js +288 -0
- package/dist/indexer/continuity.js.map +1 -0
- package/dist/indexer/detectchanges.d.ts +32 -0
- package/dist/indexer/detectchanges.d.ts.map +1 -0
- package/dist/indexer/detectchanges.js +74 -0
- package/dist/indexer/detectchanges.js.map +1 -0
- package/dist/indexer/discovery.d.ts +37 -0
- package/dist/indexer/discovery.d.ts.map +1 -0
- package/dist/indexer/discovery.js +136 -0
- package/dist/indexer/discovery.js.map +1 -0
- package/dist/indexer/externaldeps.d.ts +18 -0
- package/dist/indexer/externaldeps.d.ts.map +1 -0
- package/dist/indexer/externaldeps.js +288 -0
- package/dist/indexer/externaldeps.js.map +1 -0
- package/dist/indexer/freshness.d.ts +48 -0
- package/dist/indexer/freshness.d.ts.map +1 -0
- package/dist/indexer/freshness.js +128 -0
- package/dist/indexer/freshness.js.map +1 -0
- package/dist/indexer/git.d.ts +144 -0
- package/dist/indexer/git.d.ts.map +1 -0
- package/dist/indexer/git.js +444 -0
- package/dist/indexer/git.js.map +1 -0
- package/dist/indexer/index.d.ts +145 -0
- package/dist/indexer/index.d.ts.map +1 -0
- package/dist/indexer/index.js +930 -0
- package/dist/indexer/index.js.map +1 -0
- package/dist/indexer/modules.d.ts +62 -0
- package/dist/indexer/modules.d.ts.map +1 -0
- package/dist/indexer/modules.js +293 -0
- package/dist/indexer/modules.js.map +1 -0
- package/dist/indexer/preflight.d.ts +154 -0
- package/dist/indexer/preflight.d.ts.map +1 -0
- package/dist/indexer/preflight.js +399 -0
- package/dist/indexer/preflight.js.map +1 -0
- package/dist/indexer/protoScanner.d.ts +34 -0
- package/dist/indexer/protoScanner.d.ts.map +1 -0
- package/dist/indexer/protoScanner.js +133 -0
- package/dist/indexer/protoScanner.js.map +1 -0
- package/dist/indexer/risk.d.ts +115 -0
- package/dist/indexer/risk.d.ts.map +1 -0
- package/dist/indexer/risk.js +194 -0
- package/dist/indexer/risk.js.map +1 -0
- package/dist/indexer/serviceHostScanner.d.ts +25 -0
- package/dist/indexer/serviceHostScanner.d.ts.map +1 -0
- package/dist/indexer/serviceHostScanner.js +95 -0
- package/dist/indexer/serviceHostScanner.js.map +1 -0
- package/dist/indexer/serviceLinks.d.ts +105 -0
- package/dist/indexer/serviceLinks.d.ts.map +1 -0
- package/dist/indexer/serviceLinks.js +509 -0
- package/dist/indexer/serviceLinks.js.map +1 -0
- package/dist/indexer/shapehash.d.ts +98 -0
- package/dist/indexer/shapehash.d.ts.map +1 -0
- package/dist/indexer/shapehash.js +354 -0
- package/dist/indexer/shapehash.js.map +1 -0
- package/dist/indexer/skeleton.d.ts +15 -0
- package/dist/indexer/skeleton.d.ts.map +1 -0
- package/dist/indexer/skeleton.js +136 -0
- package/dist/indexer/skeleton.js.map +1 -0
- package/dist/indexer/symbolhistory.d.ts +41 -0
- package/dist/indexer/symbolhistory.d.ts.map +1 -0
- package/dist/indexer/symbolhistory.js +124 -0
- package/dist/indexer/symbolhistory.js.map +1 -0
- package/dist/indexer/watcher.d.ts +68 -0
- package/dist/indexer/watcher.d.ts.map +1 -0
- package/dist/indexer/watcher.js +179 -0
- package/dist/indexer/watcher.js.map +1 -0
- package/dist/mcp/server.d.ts +80 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +1610 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/parser/index.d.ts +8 -0
- package/dist/parser/index.d.ts.map +1 -0
- package/dist/parser/index.js +33 -0
- package/dist/parser/index.js.map +1 -0
- package/dist/parser/languages/cpp.d.ts +3 -0
- package/dist/parser/languages/cpp.d.ts.map +1 -0
- package/dist/parser/languages/cpp.js +350 -0
- package/dist/parser/languages/cpp.js.map +1 -0
- package/dist/parser/languages/csharp.d.ts +3 -0
- package/dist/parser/languages/csharp.d.ts.map +1 -0
- package/dist/parser/languages/csharp.js +239 -0
- package/dist/parser/languages/csharp.js.map +1 -0
- package/dist/parser/languages/go.d.ts +3 -0
- package/dist/parser/languages/go.d.ts.map +1 -0
- package/dist/parser/languages/go.js +259 -0
- package/dist/parser/languages/go.js.map +1 -0
- package/dist/parser/languages/java.d.ts +3 -0
- package/dist/parser/languages/java.d.ts.map +1 -0
- package/dist/parser/languages/java.js +391 -0
- package/dist/parser/languages/java.js.map +1 -0
- package/dist/parser/languages/python.d.ts +3 -0
- package/dist/parser/languages/python.d.ts.map +1 -0
- package/dist/parser/languages/python.js +396 -0
- package/dist/parser/languages/python.js.map +1 -0
- package/dist/parser/languages/rust.d.ts +3 -0
- package/dist/parser/languages/rust.d.ts.map +1 -0
- package/dist/parser/languages/rust.js +159 -0
- package/dist/parser/languages/rust.js.map +1 -0
- package/dist/parser/languages/typescript.d.ts +3 -0
- package/dist/parser/languages/typescript.d.ts.map +1 -0
- package/dist/parser/languages/typescript.js +1442 -0
- package/dist/parser/languages/typescript.js.map +1 -0
- package/dist/parser/parserContext.d.ts +77 -0
- package/dist/parser/parserContext.d.ts.map +1 -0
- package/dist/parser/parserContext.js +354 -0
- package/dist/parser/parserContext.js.map +1 -0
- package/dist/parser/walker.d.ts +81 -0
- package/dist/parser/walker.d.ts.map +1 -0
- package/dist/parser/walker.js +217 -0
- package/dist/parser/walker.js.map +1 -0
- package/dist/parser/worker.d.ts +66 -0
- package/dist/parser/worker.d.ts.map +1 -0
- package/dist/parser/worker.js +129 -0
- package/dist/parser/worker.js.map +1 -0
- package/dist/parser/workerpool.d.ts +107 -0
- package/dist/parser/workerpool.d.ts.map +1 -0
- package/dist/parser/workerpool.js +383 -0
- package/dist/parser/workerpool.js.map +1 -0
- package/dist/scip/format.d.ts +87 -0
- package/dist/scip/format.d.ts.map +1 -0
- package/dist/scip/format.js +31 -0
- package/dist/scip/format.js.map +1 -0
- package/dist/scip/import.d.ts +37 -0
- package/dist/scip/import.d.ts.map +1 -0
- package/dist/scip/import.js +180 -0
- package/dist/scip/import.js.map +1 -0
- package/dist/types.d.ts +392 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/docs/architecture.md +105 -0
- package/docs/benchmarks/methodology.md +134 -0
- package/docs/benchmarks/raw-results.md +71 -0
- package/docs/benchmarks.md +74 -0
- package/docs/cli.md +148 -0
- package/docs/examples/behavior-tests.md +70 -0
- package/docs/examples/change-history.md +85 -0
- package/docs/examples/pre-edit-context.md +81 -0
- package/docs/examples/service-links.md +88 -0
- package/docs/examples.md +80 -0
- package/docs/faq.md +70 -0
- package/docs/internals.md +104 -0
- package/docs/languages.md +70 -0
- package/docs/limits.md +52 -0
- package/docs/mcp.md +199 -0
- package/docs/quickstart.md +119 -0
- package/docs/testing.md +123 -0
- package/docs/tools.md +115 -0
- package/package.json +52 -0
- package/research-codebase.md +578 -0
- package/seer-cli-docs.md +326 -0
- package/seer-master-guide.md +246 -0
- package/src/bundle/ci.ts +141 -0
- package/src/bundle/contract.ts +387 -0
- package/src/bundle/export.ts +175 -0
- package/src/bundle/external.ts +285 -0
- package/src/bundle/format.ts +92 -0
- package/src/bundle/import.ts +157 -0
- package/src/cli/index.ts +1249 -0
- package/src/cli/init.ts +389 -0
- package/src/db/schema.ts +614 -0
- package/src/db/store.ts +4306 -0
- package/src/graph/pagerank.ts +53 -0
- package/src/indexer/architecture.ts +148 -0
- package/src/indexer/behavior.ts +466 -0
- package/src/indexer/boundaries.ts +374 -0
- package/src/indexer/churn.ts +58 -0
- package/src/indexer/classify.ts +96 -0
- package/src/indexer/context.ts +340 -0
- package/src/indexer/continuity.ts +322 -0
- package/src/indexer/detectchanges.ts +94 -0
- package/src/indexer/discovery.ts +176 -0
- package/src/indexer/externaldeps.ts +243 -0
- package/src/indexer/freshness.ts +166 -0
- package/src/indexer/git.ts +453 -0
- package/src/indexer/index.ts +1092 -0
- package/src/indexer/modules.ts +358 -0
- package/src/indexer/preflight.ts +548 -0
- package/src/indexer/protoScanner.ts +147 -0
- package/src/indexer/risk.ts +304 -0
- package/src/indexer/serviceHostScanner.ts +92 -0
- package/src/indexer/serviceLinks.ts +543 -0
- package/src/indexer/shapehash.ts +370 -0
- package/src/indexer/skeleton.ts +169 -0
- package/src/indexer/symbolhistory.ts +172 -0
- package/src/indexer/watcher.ts +206 -0
- package/src/mcp/server.ts +1659 -0
- package/src/parser/index.ts +37 -0
- package/src/parser/languages/cpp.ts +361 -0
- package/src/parser/languages/csharp.ts +235 -0
- package/src/parser/languages/go.ts +259 -0
- package/src/parser/languages/java.ts +382 -0
- package/src/parser/languages/python.ts +370 -0
- package/src/parser/languages/rust.ts +164 -0
- package/src/parser/languages/typescript.ts +1435 -0
- package/src/parser/parserContext.ts +392 -0
- package/src/parser/walker.ts +306 -0
- package/src/parser/worker.ts +181 -0
- package/src/parser/workerpool.ts +448 -0
- package/src/scip/format.ts +83 -0
- package/src/scip/import.ts +216 -0
- package/src/types.ts +457 -0
- package/tests/benchmark-service-links.ts +244 -0
- package/tests/bug-regressions.ts +626 -0
- package/tests/filters.ts +264 -0
- package/tests/fixtures/Counter.tsx +38 -0
- package/tests/fixtures/caller.ts +7 -0
- package/tests/fixtures/collisions.ts +23 -0
- package/tests/fixtures/local_helper.ts +5 -0
- package/tests/fixtures/overloads.java +17 -0
- package/tests/fixtures/remote_helper.ts +4 -0
- package/tests/fixtures/sample.c +15 -0
- package/tests/fixtures/sample.cpp +47 -0
- package/tests/fixtures/sample.cs +62 -0
- package/tests/fixtures/sample.go +68 -0
- package/tests/fixtures/sample.h +30 -0
- package/tests/fixtures/sample.java +85 -0
- package/tests/fixtures/sample.py +46 -0
- package/tests/fixtures/sample.rs +78 -0
- package/tests/fixtures/sample.ts +76 -0
- package/tests/fixtures-service/HttpClients.cs +30 -0
- package/tests/fixtures-service/HttpClients.java +24 -0
- package/tests/fixtures-service/billing.ts +15 -0
- package/tests/fixtures-service/docker-compose.yml +15 -0
- package/tests/fixtures-service/gateway.ts +10 -0
- package/tests/fixtures-service/get_user.ts +11 -0
- package/tests/fixtures-service/graphql_client.ts +63 -0
- package/tests/fixtures-service/graphql_server.ts +30 -0
- package/tests/fixtures-service/grpc_client.go +30 -0
- package/tests/fixtures-service/http_clients.go +23 -0
- package/tests/fixtures-service/http_clients.py +38 -0
- package/tests/fixtures-service/http_clients.ts +49 -0
- package/tests/fixtures-service/k8s/payment-service.yaml +22 -0
- package/tests/fixtures-service/k8s_calls.ts +20 -0
- package/tests/fixtures-service/messaging.ts +87 -0
- package/tests/fixtures-service/trpc_client.ts +39 -0
- package/tests/fixtures-service/trpc_server.ts +39 -0
- package/tests/fixtures-service/user_service.proto +33 -0
- package/tests/fixtures-trackcd/Cargo.toml +11 -0
- package/tests/fixtures-trackcd/SpringController.java +36 -0
- package/tests/fixtures-trackcd/auth_service.ts +19 -0
- package/tests/fixtures-trackcd/complex_module.py +50 -0
- package/tests/fixtures-trackcd/express_app.js +30 -0
- package/tests/fixtures-trackcd/fastapi_app.py +49 -0
- package/tests/fixtures-trackcd/fastify_object_routes.js +32 -0
- package/tests/fixtures-trackcd/go.mod +8 -0
- package/tests/fixtures-trackcd/package.json +15 -0
- package/tests/fixtures-trackcd/requirements.txt +4 -0
- package/tests/fixtures-trackcd/tests/auth_service.test.ts +13 -0
- package/tests/fixtures-tracke/auth/AuthService.ts +23 -0
- package/tests/fixtures-tracke/auth/crypto.ts +7 -0
- package/tests/fixtures-tracke/billing/Billing.ts +20 -0
- package/tests/fixtures-tracke/billing/Invoice.ts +10 -0
- package/tests/fixtures-tracke/billing/server.ts +17 -0
- package/tests/fixtures-tracke/package.json +7 -0
- package/tests/fixtures-tracke/tests/auth.test.ts +23 -0
- package/tests/fixtures-tracke/tests/billing.test.ts +14 -0
- package/tests/fixtures-trackf/package.json +5 -0
- package/tests/fixtures-trackf/src/auth.ts +26 -0
- package/tests/fixtures-trackf/src/handlers.ts +35 -0
- package/tests/fixtures-tracki/billing/routes.ts +12 -0
- package/tests/fixtures-tracki/gateway/client.ts +13 -0
- package/tests/git-features.ts +267 -0
- package/tests/init.ts +141 -0
- package/tests/mcp-jit.ts +130 -0
- package/tests/mcp-smoke.ts +191 -0
- package/tests/mcp-trackcd.ts +169 -0
- package/tests/mcp-tracke.ts +229 -0
- package/tests/mcp-trackf.ts +330 -0
- package/tests/mcp-trackg.ts +219 -0
- package/tests/mcp-tracki.ts +174 -0
- package/tests/mcp-watcher.ts +126 -0
- package/tests/optspec.ts +194 -0
- package/tests/parallel-index.ts +333 -0
- package/tests/parallel-read.ts +125 -0
- package/tests/parallel-recovery.ts +241 -0
- package/tests/perf-callers.ts +145 -0
- package/tests/query-parity.ts +184 -0
- package/tests/query-perf.ts +55 -0
- package/tests/scale-parallel-parity.ts +225 -0
- package/tests/scale-test.ts +523 -0
- package/tests/smoke.ts +396 -0
- package/tests/trackcd.ts +325 -0
- package/tests/tracke-collisions.ts +255 -0
- package/tests/tracke.ts +314 -0
- package/tests/trackf-bugs.ts +406 -0
- package/tests/trackf.ts +390 -0
- package/tests/trackg.ts +1372 -0
- package/tests/tracki-boundaries.ts +202 -0
- package/tests/tracki-continuity.ts +253 -0
- package/tests/tracki-contract-diff.ts +249 -0
- package/tests/tracki-external-bundles.ts +341 -0
- package/tests/tracki-preflight.ts +251 -0
- package/tests/verify-roles.ts +51 -0
- package/tests/worker-parity.ts +286 -0
- package/tests/worker-pool.ts +262 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,1435 @@
|
|
|
1
|
+
import type Parser from 'web-tree-sitter';
|
|
2
|
+
import type { SymbolDef, SymbolKind, RouteDef, ConfigKeyRead, ServiceCallDef } from '../../types.js';
|
|
3
|
+
import type { LanguageExtractor } from '../walker.js';
|
|
4
|
+
import { firstLine } from '../walker.js';
|
|
5
|
+
|
|
6
|
+
// Branch nodes for cyclomatic / cognitive complexity. tree-sitter-typescript
|
|
7
|
+
// shares its node grammar with tree-sitter-javascript and tree-sitter-tsx,
|
|
8
|
+
// so this set covers all three.
|
|
9
|
+
const TS_BRANCH_NODES = new Set<string>([
|
|
10
|
+
'if_statement',
|
|
11
|
+
'switch_case',
|
|
12
|
+
'switch_default',
|
|
13
|
+
'while_statement',
|
|
14
|
+
'do_statement',
|
|
15
|
+
'for_statement',
|
|
16
|
+
'for_in_statement',
|
|
17
|
+
'for_of_statement',
|
|
18
|
+
'catch_clause',
|
|
19
|
+
'ternary_expression',
|
|
20
|
+
'conditional_expression',
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const TS_NESTING_NODES = new Set<string>([
|
|
24
|
+
'if_statement',
|
|
25
|
+
'switch_statement',
|
|
26
|
+
'while_statement',
|
|
27
|
+
'do_statement',
|
|
28
|
+
'for_statement',
|
|
29
|
+
'for_in_statement',
|
|
30
|
+
'for_of_statement',
|
|
31
|
+
'catch_clause',
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
// HTTP method names used by Express/Fastify-style routers. Lower-cased because
|
|
35
|
+
// we compare against `member_expression.property` text.
|
|
36
|
+
const HTTP_METHODS = new Set([
|
|
37
|
+
'get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'all', 'use',
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
// HTTP CLIENT method names (axios/fetch-style outbound calls). Subset of
|
|
41
|
+
// HTTP_METHODS because clients don't expose all/use.
|
|
42
|
+
const HTTP_CLIENT_METHODS = new Set([
|
|
43
|
+
'get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'request',
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
// Receiver names that strongly indicate a route REGISTRATION (Express/
|
|
47
|
+
// Fastify). Used to distinguish `app.get('/x', handler)` (route) from
|
|
48
|
+
// `axios.get('/x')` (client call). Lowercase + capitalised variants.
|
|
49
|
+
const ROUTER_RECEIVER_NAMES = new Set([
|
|
50
|
+
'app', 'router', 'Router', 'server', 'fastify', 'expressApp', 'expressRouter',
|
|
51
|
+
'api', 'apiRouter',
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
// v9 Track-H — tRPC. Server-side procedure terminals (procedure.query(handler))
|
|
55
|
+
// and client-side call terminals (trpc.user.getById.query()). Server names are
|
|
56
|
+
// the ones that mark a node as a procedure DEFINITION; client names are how a
|
|
57
|
+
// call rendezvous through the tRPC proxy.
|
|
58
|
+
const TRPC_PROCEDURE_METHODS = new Set(['query', 'mutation', 'subscription']);
|
|
59
|
+
|
|
60
|
+
// Common tRPC procedure-builder identifiers. Hitting any of these in the
|
|
61
|
+
// receiver chain of a procedure.query(...) is what proves "this is a tRPC
|
|
62
|
+
// procedure definition" (vs. some other library that happens to expose a
|
|
63
|
+
// .query method).
|
|
64
|
+
const TRPC_PROCEDURE_BASES = new Set([
|
|
65
|
+
'procedure', 'publicProcedure', 'protectedProcedure',
|
|
66
|
+
'authedProcedure', 'adminProcedure', 'baseProcedure', 'loggedProcedure',
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
// Terminal client methods on the tRPC proxy. Maps each to its operation kind
|
|
70
|
+
// so query/useQuery both become "query", mutate/useMutation become "mutation",
|
|
71
|
+
// subscribe/useSubscription become "subscription".
|
|
72
|
+
const TRPC_CLIENT_METHODS = new Map<string, 'query' | 'mutation' | 'subscription'>([
|
|
73
|
+
['query', 'query'],
|
|
74
|
+
['mutate', 'mutation'],
|
|
75
|
+
['useQuery', 'query'],
|
|
76
|
+
['useMutation', 'mutation'],
|
|
77
|
+
['useInfiniteQuery', 'query'],
|
|
78
|
+
['useSuspenseQuery', 'query'],
|
|
79
|
+
['useSubscription', 'subscription'],
|
|
80
|
+
['subscribe', 'subscription'],
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
// Root receiver names that mark a member-chain as flowing through the tRPC
|
|
84
|
+
// client proxy. Anything ending in 'trpc' (case-insensitive) is accepted; we
|
|
85
|
+
// also allow bare 'api' / 'client' / 'rpc' since those are the other common
|
|
86
|
+
// proxy variable names in the wild.
|
|
87
|
+
const TRPC_CLIENT_ROOTS = new Set(['trpc', 'api', 'client', 'rpc']);
|
|
88
|
+
function isTrpcClientRoot(name: string): boolean {
|
|
89
|
+
if (!name) return false;
|
|
90
|
+
if (TRPC_CLIENT_ROOTS.has(name)) return true;
|
|
91
|
+
const lower = name.toLowerCase();
|
|
92
|
+
return lower.startsWith('trpc') || lower.endsWith('trpc');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// v9 Track-H — GraphQL.
|
|
96
|
+
//
|
|
97
|
+
// CLIENT METHODS — Apollo/urql/relay-style call terminals. Each maps to an
|
|
98
|
+
// operation kind. `useQuery` / `useMutation` / `useSubscription` are React-
|
|
99
|
+
// hook variants that take the document as their first arg.
|
|
100
|
+
const GRAPHQL_CLIENT_METHODS = new Map<string, 'query' | 'mutation' | 'subscription'>([
|
|
101
|
+
['query', 'query'],
|
|
102
|
+
['mutate', 'mutation'],
|
|
103
|
+
['mutation', 'mutation'],
|
|
104
|
+
['subscribe', 'subscription'],
|
|
105
|
+
['useQuery', 'query'],
|
|
106
|
+
['useMutation', 'mutation'],
|
|
107
|
+
['useSubscription', 'subscription'],
|
|
108
|
+
['useLazyQuery', 'query'],
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
// Server-side resolver-map top-level keys. Anything nested under one of these
|
|
112
|
+
// is a resolver. We emit one route per resolver with operation = field name
|
|
113
|
+
// and method = the parent kind.
|
|
114
|
+
const GRAPHQL_RESOLVER_KEYS = new Set(['Query', 'Mutation', 'Subscription']);
|
|
115
|
+
|
|
116
|
+
// v9 Track-H — messaging protocols (Kafka / SQS / SNS / RabbitMQ / NATS /
|
|
117
|
+
// Redis pub-sub). Each producer/consumer pattern is recognized by its method
|
|
118
|
+
// name combined with a structural cue (option-object field name, receiver
|
|
119
|
+
// name). Keep this list tight — false positives here would link unrelated code.
|
|
120
|
+
type MsgProtocol = 'kafka' | 'sqs' | 'sns' | 'rabbitmq' | 'nats' | 'redis_pubsub';
|
|
121
|
+
|
|
122
|
+
// Receiver-name hints that boost confidence we're looking at the right lib.
|
|
123
|
+
const MSG_RECV_HINTS: Record<MsgProtocol, string[]> = {
|
|
124
|
+
kafka: ['producer', 'kafkaProducer', 'consumer', 'kafkaConsumer', 'kafka'],
|
|
125
|
+
sqs: ['sqs', 'sqsClient'],
|
|
126
|
+
sns: ['sns', 'snsClient'],
|
|
127
|
+
rabbitmq: ['channel', 'amqp', 'rabbit', 'rabbitmq', 'ch'],
|
|
128
|
+
nats: ['nc', 'nats', 'natsClient', 'jetstream'],
|
|
129
|
+
redis_pubsub: ['redis', 'redisClient', 'pubsub', 'publisher', 'subscriber'],
|
|
130
|
+
};
|
|
131
|
+
function receiverHintsProtocol(recvName: string | null, protocol: MsgProtocol): boolean {
|
|
132
|
+
if (!recvName) return false;
|
|
133
|
+
const hints = MSG_RECV_HINTS[protocol] ?? [];
|
|
134
|
+
if (hints.includes(recvName)) return true;
|
|
135
|
+
const lower = recvName.toLowerCase();
|
|
136
|
+
for (const h of hints) if (lower.includes(h.toLowerCase())) return true;
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Union of every node type any tryExtract* on this extractor may accept.
|
|
141
|
+
// Must be a strict superset; missing a type would silently drop whatever the
|
|
142
|
+
// extractor would have emitted for it. The parser/index.ts compiles this into
|
|
143
|
+
// a Tree-Sitter Query and only fires the per-node extract calls on captures.
|
|
144
|
+
const TS_CANDIDATE_NODE_TYPES = [
|
|
145
|
+
// tryExtractDefinition
|
|
146
|
+
'function_declaration',
|
|
147
|
+
'generator_function_declaration',
|
|
148
|
+
'class_declaration',
|
|
149
|
+
'method_definition',
|
|
150
|
+
'interface_declaration',
|
|
151
|
+
'type_alias_declaration',
|
|
152
|
+
'variable_declarator',
|
|
153
|
+
// tryExtractCallName + tryExtractImport + tryExtractRoute (all reuse call_expression)
|
|
154
|
+
'call_expression',
|
|
155
|
+
'new_expression',
|
|
156
|
+
// tryExtractImport (also)
|
|
157
|
+
'import_statement',
|
|
158
|
+
// tryExtractConfigKey
|
|
159
|
+
'member_expression',
|
|
160
|
+
'subscript_expression',
|
|
161
|
+
] as const;
|
|
162
|
+
|
|
163
|
+
// Handles .ts, .tsx, .js, .jsx via separate WASM grammars but shared extractor logic
|
|
164
|
+
export const typescriptExtractor: LanguageExtractor = {
|
|
165
|
+
languageName: 'typescript',
|
|
166
|
+
extensions: ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'],
|
|
167
|
+
branchNodeTypes: TS_BRANCH_NODES,
|
|
168
|
+
nestingNodeTypes: TS_NESTING_NODES,
|
|
169
|
+
candidateNodeTypes: TS_CANDIDATE_NODE_TYPES,
|
|
170
|
+
|
|
171
|
+
tryExtractDefinition(node: Parser.SyntaxNode): SymbolDef | null {
|
|
172
|
+
switch (node.type) {
|
|
173
|
+
case 'function_declaration':
|
|
174
|
+
case 'generator_function_declaration': {
|
|
175
|
+
const nameNode = node.childForFieldName('name');
|
|
176
|
+
if (!nameNode) return null;
|
|
177
|
+
return mkDef(nameNode.text, 'function', node);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
case 'class_declaration': {
|
|
181
|
+
const nameNode = node.childForFieldName('name');
|
|
182
|
+
if (!nameNode) return null;
|
|
183
|
+
return mkDef(nameNode.text, 'class', node);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
case 'method_definition': {
|
|
187
|
+
const nameNode = node.childForFieldName('name');
|
|
188
|
+
if (!nameNode) return null;
|
|
189
|
+
const isConstructor = nameNode.text === 'constructor';
|
|
190
|
+
return mkDef(nameNode.text, isConstructor ? 'constructor' : 'method', node);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
case 'interface_declaration': {
|
|
194
|
+
const nameNode = node.childForFieldName('name');
|
|
195
|
+
if (!nameNode) return null;
|
|
196
|
+
return mkDef(nameNode.text, 'interface', node);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
case 'type_alias_declaration': {
|
|
200
|
+
const nameNode = node.childForFieldName('name');
|
|
201
|
+
if (!nameNode) return null;
|
|
202
|
+
return mkDef(nameNode.text, 'type', node);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
case 'variable_declarator': {
|
|
206
|
+
const nameNode = node.childForFieldName('name');
|
|
207
|
+
const valueNode = node.childForFieldName('value');
|
|
208
|
+
if (!nameNode || !valueNode) return null;
|
|
209
|
+
if (
|
|
210
|
+
valueNode.type === 'arrow_function' ||
|
|
211
|
+
valueNode.type === 'function_expression' ||
|
|
212
|
+
valueNode.type === 'generator_function'
|
|
213
|
+
) {
|
|
214
|
+
return mkDef(nameNode.text, 'function', node);
|
|
215
|
+
}
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
default:
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
tryExtractCallName(node: Parser.SyntaxNode): string | null {
|
|
225
|
+
if (node.type === 'call_expression') {
|
|
226
|
+
const funcNode = node.childForFieldName('function');
|
|
227
|
+
if (!funcNode) return null;
|
|
228
|
+
|
|
229
|
+
if (funcNode.type === 'identifier') return funcNode.text;
|
|
230
|
+
|
|
231
|
+
if (funcNode.type === 'member_expression') {
|
|
232
|
+
return funcNode.childForFieldName('property')?.text ?? null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (node.type === 'new_expression') {
|
|
239
|
+
const ctorNode = node.childForFieldName('constructor');
|
|
240
|
+
if (!ctorNode) return null;
|
|
241
|
+
if (ctorNode.type === 'identifier') return ctorNode.text;
|
|
242
|
+
if (ctorNode.type === 'member_expression') {
|
|
243
|
+
return ctorNode.childForFieldName('property')?.text ?? null;
|
|
244
|
+
}
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return null;
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
tryExtractImport(node: Parser.SyntaxNode): string | null {
|
|
252
|
+
if (node.type === 'import_statement') {
|
|
253
|
+
return node.childForFieldName('source')?.text?.replace(/['"]/g, '') ?? null;
|
|
254
|
+
}
|
|
255
|
+
if (node.type === 'call_expression') {
|
|
256
|
+
const funcNode = node.childForFieldName('function');
|
|
257
|
+
if (funcNode?.text === 'require') {
|
|
258
|
+
const args = node.childForFieldName('arguments');
|
|
259
|
+
const firstArg = args?.namedChildren[0];
|
|
260
|
+
if (firstArg?.type === 'string') {
|
|
261
|
+
return firstArg.text.replace(/['"]/g, '');
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return null;
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Recognize Express/Fastify-style route registrations:
|
|
270
|
+
* app.get('/users', handler)
|
|
271
|
+
* router.post('/login', handler)
|
|
272
|
+
* server.put('/items/:id', handler)
|
|
273
|
+
*
|
|
274
|
+
* Also handles Fastify object-style:
|
|
275
|
+
* app.route({ method: 'GET', url: '/users', handler: foo })
|
|
276
|
+
* app.route({ method: ['GET','POST'], url: '/users', handler: foo })
|
|
277
|
+
* — only when method and url are string (or string-array) literals so it
|
|
278
|
+
* stays deterministic. Returns one RouteDef per method when method is an
|
|
279
|
+
* array.
|
|
280
|
+
*/
|
|
281
|
+
tryExtractRoute(node: Parser.SyntaxNode): RouteDef[] | null {
|
|
282
|
+
// v9 Track-H: GraphQL resolver-map detection. Resolver definitions live in
|
|
283
|
+
// object literals (`const resolvers = { Query: { user: ... } }`) — not
|
|
284
|
+
// call expressions, so we dispatch by node type up front.
|
|
285
|
+
if (node.type === 'variable_declarator') {
|
|
286
|
+
return tryExtractGraphqlResolverMap(node);
|
|
287
|
+
}
|
|
288
|
+
if (node.type !== 'call_expression') return null;
|
|
289
|
+
const funcNode = node.childForFieldName('function');
|
|
290
|
+
if (!funcNode || funcNode.type !== 'member_expression') return null;
|
|
291
|
+
const prop = funcNode.childForFieldName('property');
|
|
292
|
+
if (!prop) return null;
|
|
293
|
+
const method = prop.text.toLowerCase();
|
|
294
|
+
|
|
295
|
+
const args = node.childForFieldName('arguments');
|
|
296
|
+
if (!args) return null;
|
|
297
|
+
const named = args.namedChildren;
|
|
298
|
+
if (named.length < 1) return null;
|
|
299
|
+
|
|
300
|
+
// ── v9 Track-H: tRPC procedure definition ─────────────────────────────
|
|
301
|
+
// `procedure.query(handler)` / `publicProcedure.input(...).mutation(handler)`.
|
|
302
|
+
// We recognize this BEFORE the HTTP path because the prop name `query` /
|
|
303
|
+
// `mutation` / `subscription` would otherwise fall through to "unknown
|
|
304
|
+
// method" and return null — which is fine, but we want to actually emit
|
|
305
|
+
// a tRPC route row for the proc.
|
|
306
|
+
if (TRPC_PROCEDURE_METHODS.has(prop.text)) {
|
|
307
|
+
const trpcRoute = tryExtractTrpcProcedure(node, prop.text, named);
|
|
308
|
+
if (trpcRoute) return [trpcRoute];
|
|
309
|
+
// fall through; not a tRPC procedure — could still be HTTP `app.query(...)`
|
|
310
|
+
// (extremely rare) so we don't return null yet.
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ── v9 Track-H: messaging CONSUMER detection ──────────────────────────
|
|
314
|
+
// Kafka consumer.subscribe({ topic|topics }) / Rabbit channel.consume /
|
|
315
|
+
// SQS receiveMessage / NATS nc.subscribe. The consumer side registers
|
|
316
|
+
// a route so producer→consumer linking works through the same resolver
|
|
317
|
+
// that handles HTTP route → service_call rendezvous.
|
|
318
|
+
const consumerRoutes = tryExtractMessagingConsumer(node, funcNode, named);
|
|
319
|
+
if (consumerRoutes) return consumerRoutes;
|
|
320
|
+
|
|
321
|
+
// ── Fastify object-style: app.route({ method, url, handler }) ──────
|
|
322
|
+
if (method === 'route') {
|
|
323
|
+
const opts = named[0];
|
|
324
|
+
if (opts.type !== 'object') return null;
|
|
325
|
+
const fields = readObjectLiteralFields(opts);
|
|
326
|
+
const urlNode = fields.get('url');
|
|
327
|
+
const methodNode = fields.get('method');
|
|
328
|
+
const handlerNode = fields.get('handler');
|
|
329
|
+
if (!urlNode || !methodNode) return null;
|
|
330
|
+
const urlStr = stringLiteralValue(urlNode);
|
|
331
|
+
if (!urlStr || urlStr.length > 200) return null;
|
|
332
|
+
const methods = stringOrStringArrayValues(methodNode);
|
|
333
|
+
if (methods.length === 0) return null;
|
|
334
|
+
const handlerName = handlerNode ? identifierLikeName(handlerNode) : undefined;
|
|
335
|
+
return methods.map(m => ({
|
|
336
|
+
method: m.toUpperCase(),
|
|
337
|
+
path: urlStr,
|
|
338
|
+
framework: 'fastify',
|
|
339
|
+
handlerName,
|
|
340
|
+
line: node.startPosition.row,
|
|
341
|
+
}));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ── Express/Fastify shorthand: app.<method>(path, handler) ──────────
|
|
345
|
+
if (!HTTP_METHODS.has(method)) return null;
|
|
346
|
+
|
|
347
|
+
// First arg must be a string literal route path
|
|
348
|
+
const pathNode = named[0];
|
|
349
|
+
if (pathNode.type !== 'string' && pathNode.type !== 'template_string') return null;
|
|
350
|
+
const routePath = stripQuotes(pathNode.text);
|
|
351
|
+
if (!routePath || routePath.length > 200) return null;
|
|
352
|
+
|
|
353
|
+
// Distinguish a route REGISTRATION (`app.get('/x', handler)`) from a
|
|
354
|
+
// client CALL (`axios.get('/x')`). A route registration must have either:
|
|
355
|
+
// (a) a router-like receiver name (app, router, server, Router, …), OR
|
|
356
|
+
// (b) ≥2 args where arg 2..N is a handler-shaped node (identifier,
|
|
357
|
+
// member_expression, arrow_function, or function_expression).
|
|
358
|
+
// Without one of those it's almost certainly a client GET/POST call and
|
|
359
|
+
// belongs to the service-call recognizer instead.
|
|
360
|
+
const receiver = funcNode.childForFieldName('object');
|
|
361
|
+
const recvName = receiver?.type === 'identifier' ? receiver.text : null;
|
|
362
|
+
const isRouterReceiver = recvName !== null && ROUTER_RECEIVER_NAMES.has(recvName);
|
|
363
|
+
let hasHandlerArg = false;
|
|
364
|
+
if (named.length >= 2) {
|
|
365
|
+
for (let i = 1; i < named.length; i++) {
|
|
366
|
+
const a = named[i];
|
|
367
|
+
if (a.type === 'identifier' || a.type === 'member_expression'
|
|
368
|
+
|| a.type === 'arrow_function' || a.type === 'function_expression') {
|
|
369
|
+
hasHandlerArg = true;
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (!isRouterReceiver && !hasHandlerArg) return null;
|
|
375
|
+
|
|
376
|
+
// Last positional arg, if it's an identifier, is the handler name.
|
|
377
|
+
let handlerName: string | undefined;
|
|
378
|
+
for (let i = named.length - 1; i >= 1; i--) {
|
|
379
|
+
const a = named[i];
|
|
380
|
+
const found = identifierLikeName(a);
|
|
381
|
+
if (found) { handlerName = found; break; }
|
|
382
|
+
// Inline arrow / function — don't try to name it (would be an empty handler)
|
|
383
|
+
if (a.type === 'arrow_function' || a.type === 'function_expression') break;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return [{
|
|
387
|
+
method: method.toUpperCase(),
|
|
388
|
+
path: routePath,
|
|
389
|
+
framework: 'express',
|
|
390
|
+
handlerName,
|
|
391
|
+
line: node.startPosition.row,
|
|
392
|
+
}];
|
|
393
|
+
},
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Detect HTTP client calls — fetch / axios.{get,post,…} / http.* /
|
|
397
|
+
* any.<method>(literalUrl, …). We only record a service call when the URL
|
|
398
|
+
* argument is a string or template_string whose literal portion contains a
|
|
399
|
+
* path-like fragment; everything else is ignored to keep results deterministic.
|
|
400
|
+
*
|
|
401
|
+
* The recognizer is intentionally conservative:
|
|
402
|
+
* fetch('/api/users') ← yes
|
|
403
|
+
* fetch(`${BASE_URL}/api/users`) ← yes (path lifted, env tagged)
|
|
404
|
+
* axios.get('/api/x') ← yes
|
|
405
|
+
* client.post('/api/x') ← yes (any bare member.<method>(literalUrl…))
|
|
406
|
+
* fetch(someVar) ← no
|
|
407
|
+
* logger.get(record) ← no (no string arg)
|
|
408
|
+
*/
|
|
409
|
+
tryExtractServiceCalls(node: Parser.SyntaxNode): ServiceCallDef[] | null {
|
|
410
|
+
// v9 Track-H: gql document definition — `const X = gql\`...\``. Emitted
|
|
411
|
+
// as a sentinel service_call with framework='gql-doc' so the resolver can
|
|
412
|
+
// map document identifiers used in client calls back to the operation's
|
|
413
|
+
// top-level field name. Sentinels are filtered out of normal listings by
|
|
414
|
+
// their framework value.
|
|
415
|
+
if (node.type === 'variable_declarator') {
|
|
416
|
+
const sentinel = tryExtractGqlDocDefinition(node);
|
|
417
|
+
return sentinel ? [sentinel] : null;
|
|
418
|
+
}
|
|
419
|
+
if (node.type !== 'call_expression') return null;
|
|
420
|
+
const funcNode = node.childForFieldName('function');
|
|
421
|
+
if (!funcNode) return null;
|
|
422
|
+
|
|
423
|
+
// ── v9 Track-H: tRPC client call ─────────────────────────────────────
|
|
424
|
+
// trpc.user.getById.query({...}) / trpc.user.create.mutate(...) /
|
|
425
|
+
// trpc.user.getById.useQuery(...). The terminal method is one of
|
|
426
|
+
// TRPC_CLIENT_METHODS; the receiver chain must root at an identifier
|
|
427
|
+
// that looks like a tRPC proxy (`trpc*`, `api`, `client`, `rpc`).
|
|
428
|
+
if (funcNode.type === 'member_expression') {
|
|
429
|
+
const propNode = funcNode.childForFieldName('property');
|
|
430
|
+
const propTxt = propNode?.text ?? '';
|
|
431
|
+
const trpcKind = TRPC_CLIENT_METHODS.get(propTxt);
|
|
432
|
+
if (trpcKind) {
|
|
433
|
+
const trpcCall = tryExtractTrpcClientCall(node, funcNode, propTxt, trpcKind);
|
|
434
|
+
if (trpcCall) return [trpcCall];
|
|
435
|
+
// fall through — not tRPC-shaped; the HTTP recognizers below may still match.
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// ── v9 Track-H: GraphQL client call ──────────────────────────────────
|
|
440
|
+
// Apollo/urql/relay: client.query({ query: GET_USER }) / .mutate(...) /
|
|
441
|
+
// useQuery(GET_USER) / useMutation(CREATE_USER). The operation name is
|
|
442
|
+
// extracted from the document — either the imported const name (mapped
|
|
443
|
+
// back to its gql definition during indexing — done via a second pass)
|
|
444
|
+
// or directly from a gql template tagged template literal arg.
|
|
445
|
+
//
|
|
446
|
+
// For Seer-Core's purposes we emit the operation name found inline. The
|
|
447
|
+
// caller-side gql tag also gets its own service_call emitted (covers
|
|
448
|
+
// direct `gql\`query GetUser{...}\`` usage in client code).
|
|
449
|
+
if (funcNode.type === 'identifier') {
|
|
450
|
+
// Hook calls: useQuery(GET_USER, …) / useMutation(CREATE_USER, …)
|
|
451
|
+
const hookKind = GRAPHQL_CLIENT_METHODS.get(funcNode.text);
|
|
452
|
+
if (hookKind) {
|
|
453
|
+
const gqlCall = tryExtractGraphqlClientCall(node, funcNode.text, hookKind);
|
|
454
|
+
if (gqlCall) return [gqlCall];
|
|
455
|
+
}
|
|
456
|
+
} else if (funcNode.type === 'member_expression') {
|
|
457
|
+
const propNode = funcNode.childForFieldName('property');
|
|
458
|
+
const propTxt = propNode?.text ?? '';
|
|
459
|
+
const objNode = funcNode.childForFieldName('object');
|
|
460
|
+
const objTxt = objNode?.type === 'identifier' ? objNode.text : null;
|
|
461
|
+
const isGqlClient = objTxt !== null &&
|
|
462
|
+
(objTxt === 'client' || objTxt === 'apollo' || objTxt === 'apolloClient' ||
|
|
463
|
+
objTxt === 'gqlClient' || objTxt === 'urql' || objTxt === 'urqlClient');
|
|
464
|
+
const gqlKind = GRAPHQL_CLIENT_METHODS.get(propTxt);
|
|
465
|
+
if (isGqlClient && gqlKind) {
|
|
466
|
+
const gqlCall = tryExtractGraphqlClientCall(node, propTxt, gqlKind);
|
|
467
|
+
if (gqlCall) return [gqlCall];
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ── v9 Track-H: messaging PRODUCER detection ──────────────────────────
|
|
472
|
+
// Kafka producer.send / SQS sendMessage / SNS publish / Rabbit publish |
|
|
473
|
+
// sendToQueue / NATS publish / Redis publish. Consumer-side detection
|
|
474
|
+
// lives in tryExtractRoute so consumers register as routes.
|
|
475
|
+
if (funcNode.type === 'member_expression') {
|
|
476
|
+
const msgCall = tryExtractMessagingProducer(node, funcNode);
|
|
477
|
+
if (msgCall) return [msgCall];
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
let framework: string | null = null;
|
|
481
|
+
let method: string | undefined;
|
|
482
|
+
|
|
483
|
+
if (funcNode.type === 'identifier') {
|
|
484
|
+
// bare fetch('/x')
|
|
485
|
+
if (funcNode.text === 'fetch') framework = 'fetch';
|
|
486
|
+
else return null;
|
|
487
|
+
} else if (funcNode.type === 'member_expression') {
|
|
488
|
+
const obj = funcNode.childForFieldName('object');
|
|
489
|
+
const prop = funcNode.childForFieldName('property');
|
|
490
|
+
if (!obj || !prop) return null;
|
|
491
|
+
const propText = prop.text;
|
|
492
|
+
const propLower = propText.toLowerCase();
|
|
493
|
+
|
|
494
|
+
// axios.get / axios.post / axios.request / axios.{put,patch,delete,head,options}
|
|
495
|
+
if (obj.type === 'identifier' && obj.text === 'axios' && HTTP_CLIENT_METHODS.has(propLower)) {
|
|
496
|
+
framework = 'axios';
|
|
497
|
+
method = methodFromName(propLower);
|
|
498
|
+
} else if (obj.type === 'identifier' && obj.text === 'fetch' && propLower === 'fetch') {
|
|
499
|
+
framework = 'fetch';
|
|
500
|
+
} else if (HTTP_CLIENT_METHODS.has(propLower)) {
|
|
501
|
+
// Generic client.<method>(literalUrl, …) — record it when the URL arg
|
|
502
|
+
// is a string literal; the path itself is the strongest signal.
|
|
503
|
+
framework = 'http-client';
|
|
504
|
+
method = methodFromName(propLower);
|
|
505
|
+
} else {
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
} else {
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// First argument must be a string-like literal we can read.
|
|
513
|
+
const args = node.childForFieldName('arguments');
|
|
514
|
+
if (!args) return null;
|
|
515
|
+
const first = args.namedChildren[0];
|
|
516
|
+
if (!first) return null;
|
|
517
|
+
|
|
518
|
+
let raw: string | null = null;
|
|
519
|
+
let envKey: string | undefined;
|
|
520
|
+
if (first.type === 'string') {
|
|
521
|
+
raw = stripQuotes(first.text);
|
|
522
|
+
} else if (first.type === 'template_string') {
|
|
523
|
+
const lifted = readTemplateString(first);
|
|
524
|
+
if (!lifted) return null;
|
|
525
|
+
raw = lifted.text;
|
|
526
|
+
envKey = lifted.envKey;
|
|
527
|
+
} else {
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (!raw) return null;
|
|
532
|
+
// Path-y heuristic: starts with '/', or contains a '/' and looks like a URL.
|
|
533
|
+
if (!looksLikeHttpTarget(raw)) return null;
|
|
534
|
+
|
|
535
|
+
// For fetch(url, { method: 'POST' }) — peek at the options arg if available.
|
|
536
|
+
if (!method && framework === 'fetch') {
|
|
537
|
+
const opts = args.namedChildren[1];
|
|
538
|
+
if (opts && opts.type === 'object') {
|
|
539
|
+
const fields = readObjectLiteralFields(opts);
|
|
540
|
+
const m = fields.get('method');
|
|
541
|
+
if (m) {
|
|
542
|
+
const v = stringLiteralValue(m);
|
|
543
|
+
if (v) method = v.toUpperCase();
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const def: ServiceCallDef = {
|
|
549
|
+
protocol: 'http',
|
|
550
|
+
method: method ?? 'ANY',
|
|
551
|
+
rawTarget: raw.slice(0, 240),
|
|
552
|
+
framework,
|
|
553
|
+
line: node.startPosition.row,
|
|
554
|
+
confidence: 0.85,
|
|
555
|
+
};
|
|
556
|
+
if (envKey) def.envKey = envKey;
|
|
557
|
+
return [def];
|
|
558
|
+
},
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Static env var reads: `process.env.NAME` and `process.env["NAME"]`.
|
|
562
|
+
* Also `import.meta.env.NAME` for Vite-style projects.
|
|
563
|
+
*/
|
|
564
|
+
tryExtractConfigKey(node: Parser.SyntaxNode): ConfigKeyRead | null {
|
|
565
|
+
if (node.type !== 'member_expression' && node.type !== 'subscript_expression') return null;
|
|
566
|
+
|
|
567
|
+
// process.env.NAME → member_expression(member_expression("process","env"), "NAME")
|
|
568
|
+
if (node.type === 'member_expression') {
|
|
569
|
+
const obj = node.childForFieldName('object');
|
|
570
|
+
const prop = node.childForFieldName('property');
|
|
571
|
+
if (!obj || !prop) return null;
|
|
572
|
+
if (obj.type === 'member_expression') {
|
|
573
|
+
const objObj = obj.childForFieldName('object');
|
|
574
|
+
const objProp = obj.childForFieldName('property');
|
|
575
|
+
if (objObj && objProp) {
|
|
576
|
+
if (objObj.text === 'process' && objProp.text === 'env') {
|
|
577
|
+
return { key: prop.text, source: 'env', line: node.startPosition.row };
|
|
578
|
+
}
|
|
579
|
+
// import.meta.env.NAME
|
|
580
|
+
if (objObj.type === 'member_expression') {
|
|
581
|
+
const a = objObj.childForFieldName('object');
|
|
582
|
+
const b = objObj.childForFieldName('property');
|
|
583
|
+
if (a?.text === 'import' && b?.text === 'meta' && objProp.text === 'env') {
|
|
584
|
+
return { key: prop.text, source: 'env', line: node.startPosition.row };
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// process.env["NAME"]
|
|
593
|
+
if (node.type === 'subscript_expression') {
|
|
594
|
+
const obj = node.childForFieldName('object');
|
|
595
|
+
const idx = node.childForFieldName('index');
|
|
596
|
+
if (!obj || !idx) return null;
|
|
597
|
+
if (obj.type === 'member_expression') {
|
|
598
|
+
const objObj = obj.childForFieldName('object');
|
|
599
|
+
const objProp = obj.childForFieldName('property');
|
|
600
|
+
if (objObj?.text === 'process' && objProp?.text === 'env'
|
|
601
|
+
&& (idx.type === 'string' || idx.type === 'template_string')) {
|
|
602
|
+
const key = stripQuotes(idx.text);
|
|
603
|
+
if (key) return { key, source: 'env', line: node.startPosition.row };
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return null;
|
|
608
|
+
},
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
function stripQuotes(s: string): string {
|
|
612
|
+
return s.replace(/^[`'"]|[`'"]$/g, '');
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Read a TypeScript/JavaScript object literal into a key→value-node map.
|
|
617
|
+
* Used by the Fastify route detector so we can pick out `url`, `method`,
|
|
618
|
+
* and `handler` fields regardless of declaration order. Computed keys
|
|
619
|
+
* (`[expr]: …`) are dropped — we only handle deterministic literal keys.
|
|
620
|
+
*/
|
|
621
|
+
function readObjectLiteralFields(obj: Parser.SyntaxNode): Map<string, Parser.SyntaxNode> {
|
|
622
|
+
const out = new Map<string, Parser.SyntaxNode>();
|
|
623
|
+
for (const prop of obj.namedChildren) {
|
|
624
|
+
if (prop.type !== 'pair' && prop.type !== 'shorthand_property_identifier'
|
|
625
|
+
&& prop.type !== 'property_identifier') continue;
|
|
626
|
+
if (prop.type === 'pair') {
|
|
627
|
+
const k = prop.childForFieldName('key');
|
|
628
|
+
const v = prop.childForFieldName('value');
|
|
629
|
+
if (!k || !v) continue;
|
|
630
|
+
let key: string | null = null;
|
|
631
|
+
if (k.type === 'property_identifier' || k.type === 'identifier') key = k.text;
|
|
632
|
+
else if (k.type === 'string' || k.type === 'template_string') key = stripQuotes(k.text);
|
|
633
|
+
if (key) out.set(key, v);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
return out;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/** Strip quotes from a string-literal node, returning null for non-strings. */
|
|
640
|
+
function stringLiteralValue(node: Parser.SyntaxNode): string | null {
|
|
641
|
+
if (node.type !== 'string' && node.type !== 'template_string') return null;
|
|
642
|
+
return stripQuotes(node.text);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Pull a list of string values out of a node that is either a single string
|
|
647
|
+
* literal or an array literal containing only string literals. Anything
|
|
648
|
+
* dynamic is dropped (`[]` returned) so route extraction stays deterministic.
|
|
649
|
+
*/
|
|
650
|
+
function stringOrStringArrayValues(node: Parser.SyntaxNode): string[] {
|
|
651
|
+
const single = stringLiteralValue(node);
|
|
652
|
+
if (single) return [single];
|
|
653
|
+
if (node.type !== 'array') return [];
|
|
654
|
+
const out: string[] = [];
|
|
655
|
+
for (const el of node.namedChildren) {
|
|
656
|
+
const v = stringLiteralValue(el);
|
|
657
|
+
if (v) out.push(v);
|
|
658
|
+
}
|
|
659
|
+
return out;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Return the identifier-like name of a node passed as a handler — either a
|
|
664
|
+
* bare `identifier` (foo), a `member_expression` (a.b → "b"), or null for
|
|
665
|
+
* inline functions/arrows where there's no name to extract.
|
|
666
|
+
*/
|
|
667
|
+
function identifierLikeName(node: Parser.SyntaxNode): string | undefined {
|
|
668
|
+
if (node.type === 'identifier') return node.text;
|
|
669
|
+
if (node.type === 'member_expression') {
|
|
670
|
+
return node.childForFieldName('property')?.text ?? undefined;
|
|
671
|
+
}
|
|
672
|
+
return undefined;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/** Upper-cased HTTP verb derived from an axios/fetch method name. */
|
|
676
|
+
function methodFromName(name: string): string | undefined {
|
|
677
|
+
if (name === 'request') return 'ANY';
|
|
678
|
+
return name.toUpperCase();
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Read a template_string and return the literal portion plus the first
|
|
683
|
+
* env-key-looking identifier referenced via process.env.X or import.meta.env.X
|
|
684
|
+
* inside `${…}` placeholders. Used so `fetch(`${process.env.PAYMENT_URL}/charge`)`
|
|
685
|
+
* still records `/charge` + `envKey = PAYMENT_URL`.
|
|
686
|
+
*
|
|
687
|
+
* Returns null when there's nothing useful to extract.
|
|
688
|
+
*/
|
|
689
|
+
function readTemplateString(node: Parser.SyntaxNode): { text: string; envKey?: string } | null {
|
|
690
|
+
let text = '';
|
|
691
|
+
let envKey: string | undefined;
|
|
692
|
+
for (const child of node.namedChildren) {
|
|
693
|
+
if (child.type === 'string_fragment') {
|
|
694
|
+
text += child.text;
|
|
695
|
+
} else if (child.type === 'template_substitution') {
|
|
696
|
+
// ${…} — try to pull a process.env.X env key out of the expression.
|
|
697
|
+
const inner = child.namedChildren[0];
|
|
698
|
+
const k = tryPickEnvKey(inner);
|
|
699
|
+
if (k) {
|
|
700
|
+
if (!envKey) envKey = k;
|
|
701
|
+
// An env-base substitution (`${process.env.PAYMENT_URL}/charge`) is the
|
|
702
|
+
// host/base, not a path segment — drop it; the literal `/charge` tail is
|
|
703
|
+
// what we match against routes.
|
|
704
|
+
} else {
|
|
705
|
+
// A dynamic, NON-env value embedded in the URL is almost always a path
|
|
706
|
+
// parameter (`/api/users/${id}`). Emit a single placeholder segment so
|
|
707
|
+
// the segment COUNT is preserved and the route-pattern matcher can line
|
|
708
|
+
// it up against a parameterised route (`/api/users/:id`, `/users/{id}`,
|
|
709
|
+
// `/users/<id>`). Without this the segment vanished and a real call
|
|
710
|
+
// under-matched (e.g. `/api/users/${id}` collapsed to `/api/users`).
|
|
711
|
+
text += ':param';
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
if (!text && !envKey) return null;
|
|
716
|
+
return { text, envKey };
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/** Walk an expression node looking for process.env.X / import.meta.env.X. */
|
|
720
|
+
function tryPickEnvKey(node: Parser.SyntaxNode | null | undefined): string | undefined {
|
|
721
|
+
if (!node) return undefined;
|
|
722
|
+
if (node.type === 'member_expression') {
|
|
723
|
+
const obj = node.childForFieldName('object');
|
|
724
|
+
const prop = node.childForFieldName('property');
|
|
725
|
+
if (obj?.type === 'member_expression') {
|
|
726
|
+
const objObj = obj.childForFieldName('object');
|
|
727
|
+
const objProp = obj.childForFieldName('property');
|
|
728
|
+
if (objObj?.text === 'process' && objProp?.text === 'env' && prop) return prop.text;
|
|
729
|
+
if (objObj?.type === 'member_expression') {
|
|
730
|
+
const a = objObj.childForFieldName('object');
|
|
731
|
+
const b = objObj.childForFieldName('property');
|
|
732
|
+
if (a?.text === 'import' && b?.text === 'meta' && objProp?.text === 'env' && prop) return prop.text;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
// Fall back: walk children breadth-first up to a small bound so something
|
|
737
|
+
// like `${(process.env.X ?? "y") + "/charge"}` still gets a hit.
|
|
738
|
+
for (const child of node.namedChildren) {
|
|
739
|
+
const found = tryPickEnvKey(child);
|
|
740
|
+
if (found) return found;
|
|
741
|
+
}
|
|
742
|
+
return undefined;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/** Conservative path-likeness check. */
|
|
746
|
+
function looksLikeHttpTarget(s: string): boolean {
|
|
747
|
+
if (!s) return false;
|
|
748
|
+
if (s.startsWith('/')) return true; // /api/users
|
|
749
|
+
if (/^https?:\/\//i.test(s)) return true; // https://x/y
|
|
750
|
+
if (/^[a-zA-Z0-9._-]+\/[a-zA-Z0-9_-]/.test(s)) return true; // hostish/path
|
|
751
|
+
return false;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* v9 Track-H — recognize a tRPC procedure definition.
|
|
756
|
+
*
|
|
757
|
+
* Returns a route with protocol='trpc' when the call_expression is the terminal
|
|
758
|
+
* `.query(handler)` / `.mutation(handler)` / `.subscription(handler)` of a
|
|
759
|
+
* tRPC procedure builder chain (`procedure.input(...).query(handler)`).
|
|
760
|
+
*
|
|
761
|
+
* Two signals must both hold:
|
|
762
|
+
* 1. The receiver chain bottoms out at a known procedure-builder identifier
|
|
763
|
+
* (`procedure`, `publicProcedure`, `protectedProcedure`, …).
|
|
764
|
+
* 2. The call is the VALUE of a `pair` in an object literal — that pair's
|
|
765
|
+
* KEY is the procedure name within its immediate router (we use this as
|
|
766
|
+
* `operation` and `path` so the resolver can rendezvous on it).
|
|
767
|
+
*
|
|
768
|
+
* When either signal is missing we return null so the caller can fall through
|
|
769
|
+
* to the HTTP route extractor.
|
|
770
|
+
*/
|
|
771
|
+
function tryExtractTrpcProcedure(
|
|
772
|
+
node: Parser.SyntaxNode,
|
|
773
|
+
methodName: string,
|
|
774
|
+
args: Parser.SyntaxNode[],
|
|
775
|
+
): RouteDef | null {
|
|
776
|
+
// (1) Walk receiver chain for a procedure-builder identifier.
|
|
777
|
+
const funcNode = node.childForFieldName('function');
|
|
778
|
+
if (!funcNode || funcNode.type !== 'member_expression') return null;
|
|
779
|
+
let cur: Parser.SyntaxNode | null = funcNode.childForFieldName('object');
|
|
780
|
+
let foundBase = false;
|
|
781
|
+
// Bounded walk so a runaway chain can't burn time.
|
|
782
|
+
for (let i = 0; i < 12 && cur; i++) {
|
|
783
|
+
if (cur.type === 'identifier' && TRPC_PROCEDURE_BASES.has(cur.text)) {
|
|
784
|
+
foundBase = true; break;
|
|
785
|
+
}
|
|
786
|
+
if (cur.type === 'member_expression') {
|
|
787
|
+
const obj = cur.childForFieldName('object');
|
|
788
|
+
if (obj?.type === 'identifier' && TRPC_PROCEDURE_BASES.has(obj.text)) {
|
|
789
|
+
foundBase = true; break;
|
|
790
|
+
}
|
|
791
|
+
// Also accept member chains like `t.procedure` — check the property.
|
|
792
|
+
const prop = cur.childForFieldName('property');
|
|
793
|
+
if (prop && TRPC_PROCEDURE_BASES.has(prop.text)) {
|
|
794
|
+
foundBase = true; break;
|
|
795
|
+
}
|
|
796
|
+
cur = obj;
|
|
797
|
+
} else if (cur.type === 'call_expression') {
|
|
798
|
+
const f = cur.childForFieldName('function');
|
|
799
|
+
cur = f?.type === 'member_expression' ? f.childForFieldName('object') : null;
|
|
800
|
+
} else {
|
|
801
|
+
cur = null;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
if (!foundBase) return null;
|
|
805
|
+
|
|
806
|
+
// (2) Walk UP to enclosing pair to harvest the procedure key.
|
|
807
|
+
let parent: Parser.SyntaxNode | null = node.parent;
|
|
808
|
+
let keyName: string | null = null;
|
|
809
|
+
for (let i = 0; i < 8 && parent; i++) {
|
|
810
|
+
if (parent.type === 'pair') {
|
|
811
|
+
const k = parent.childForFieldName('key');
|
|
812
|
+
if (k) {
|
|
813
|
+
if (k.type === 'property_identifier' || k.type === 'identifier') keyName = k.text;
|
|
814
|
+
else if (k.type === 'string' || k.type === 'template_string') keyName = stripQuotes(k.text);
|
|
815
|
+
}
|
|
816
|
+
break;
|
|
817
|
+
}
|
|
818
|
+
parent = parent.parent;
|
|
819
|
+
}
|
|
820
|
+
if (!keyName) return null;
|
|
821
|
+
|
|
822
|
+
// Handler name when the first arg is a named function/identifier.
|
|
823
|
+
const handlerArg = args[0];
|
|
824
|
+
const handlerName = handlerArg ? identifierLikeName(handlerArg) : undefined;
|
|
825
|
+
|
|
826
|
+
const opKind = methodName === 'mutation' ? 'mutation'
|
|
827
|
+
: methodName === 'subscription' ? 'subscription'
|
|
828
|
+
: 'query';
|
|
829
|
+
|
|
830
|
+
return {
|
|
831
|
+
method: opKind.toUpperCase(),
|
|
832
|
+
path: keyName,
|
|
833
|
+
framework: 'trpc',
|
|
834
|
+
handlerName,
|
|
835
|
+
line: node.startPosition.row,
|
|
836
|
+
protocol: 'trpc',
|
|
837
|
+
operation: keyName,
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* v9 Track-H — recognize a tRPC client call.
|
|
843
|
+
*
|
|
844
|
+
* trpc.user.getById.query({...}) → operation = "user.getById", method=QUERY
|
|
845
|
+
* trpc.user.create.mutate(...) → operation = "user.create", method=MUTATION
|
|
846
|
+
* trpc.user.getById.useQuery(...) → operation = "user.getById", method=QUERY
|
|
847
|
+
*
|
|
848
|
+
* Recognition rules:
|
|
849
|
+
* - Terminal method is one of TRPC_CLIENT_METHODS.
|
|
850
|
+
* - The chain rooted at the leftmost identifier matches isTrpcClientRoot().
|
|
851
|
+
* - At least one procedure-name segment exists between the root and the terminal.
|
|
852
|
+
*
|
|
853
|
+
* The operation path is everything between the root and the terminal joined by
|
|
854
|
+
* '.'. The resolver matches client.operation == server.operation.
|
|
855
|
+
*/
|
|
856
|
+
function tryExtractTrpcClientCall(
|
|
857
|
+
node: Parser.SyntaxNode,
|
|
858
|
+
funcNode: Parser.SyntaxNode,
|
|
859
|
+
terminalMethod: string,
|
|
860
|
+
kind: 'query' | 'mutation' | 'subscription',
|
|
861
|
+
): ServiceCallDef | null {
|
|
862
|
+
const segs: string[] = [];
|
|
863
|
+
// Walk receiver chain from .object downward, collecting property segments
|
|
864
|
+
// and stopping at the first non-member identifier.
|
|
865
|
+
let cur: Parser.SyntaxNode | null = funcNode.childForFieldName('object');
|
|
866
|
+
let rootName: string | null = null;
|
|
867
|
+
for (let i = 0; i < 16 && cur; i++) {
|
|
868
|
+
if (cur.type === 'identifier') {
|
|
869
|
+
rootName = cur.text;
|
|
870
|
+
break;
|
|
871
|
+
}
|
|
872
|
+
if (cur.type === 'member_expression') {
|
|
873
|
+
const prop = cur.childForFieldName('property');
|
|
874
|
+
if (prop) segs.unshift(prop.text);
|
|
875
|
+
cur = cur.childForFieldName('object');
|
|
876
|
+
} else {
|
|
877
|
+
// bracket access etc. break the chain — too risky to guess
|
|
878
|
+
return null;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
if (!rootName) return null;
|
|
882
|
+
if (!isTrpcClientRoot(rootName)) return null;
|
|
883
|
+
if (segs.length < 1) return null; // need at least one procedure segment
|
|
884
|
+
|
|
885
|
+
const operation = segs.join('.');
|
|
886
|
+
return {
|
|
887
|
+
protocol: 'trpc',
|
|
888
|
+
method: kind.toUpperCase(),
|
|
889
|
+
rawTarget: operation,
|
|
890
|
+
framework: `trpc-${terminalMethod}`,
|
|
891
|
+
line: node.startPosition.row,
|
|
892
|
+
confidence: 0.9,
|
|
893
|
+
operation,
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* v9 Track-H — recognize a GraphQL resolver map.
|
|
899
|
+
*
|
|
900
|
+
* `const resolvers = { Query: { user: handler, ... }, Mutation: { ... } }`
|
|
901
|
+
* (or `{ Query: { user() { ... } } }` with shorthand method syntax).
|
|
902
|
+
*
|
|
903
|
+
* Emits one route per resolver field with operation = field name, method =
|
|
904
|
+
* 'QUERY' | 'MUTATION' | 'SUBSCRIPTION'. Only fires when the variable's
|
|
905
|
+
* value is an object literal whose top-level keys are exactly the GraphQL
|
|
906
|
+
* resolver-kind names — that gate keeps us from confusing this with arbitrary
|
|
907
|
+
* config objects.
|
|
908
|
+
*/
|
|
909
|
+
function tryExtractGraphqlResolverMap(node: Parser.SyntaxNode): RouteDef[] | null {
|
|
910
|
+
const valNode = node.childForFieldName('value');
|
|
911
|
+
if (!valNode || valNode.type !== 'object') return null;
|
|
912
|
+
const fields = readObjectLiteralFields(valNode);
|
|
913
|
+
// At least one of Query/Mutation/Subscription must be present and itself be
|
|
914
|
+
// an object — otherwise this isn't a resolver map.
|
|
915
|
+
let hasResolverKey = false;
|
|
916
|
+
for (const k of GRAPHQL_RESOLVER_KEYS) {
|
|
917
|
+
if (fields.has(k)) { hasResolverKey = true; break; }
|
|
918
|
+
}
|
|
919
|
+
if (!hasResolverKey) return null;
|
|
920
|
+
const routes: RouteDef[] = [];
|
|
921
|
+
for (const kindKey of GRAPHQL_RESOLVER_KEYS) {
|
|
922
|
+
const inner = fields.get(kindKey);
|
|
923
|
+
if (!inner || inner.type !== 'object') continue;
|
|
924
|
+
const kind = kindKey.toLowerCase();
|
|
925
|
+
for (const child of inner.namedChildren) {
|
|
926
|
+
let fieldName: string | null = null;
|
|
927
|
+
let handlerName: string | undefined;
|
|
928
|
+
const line = child.startPosition.row;
|
|
929
|
+
if (child.type === 'pair') {
|
|
930
|
+
const k = child.childForFieldName('key');
|
|
931
|
+
const v = child.childForFieldName('value');
|
|
932
|
+
if (!k) continue;
|
|
933
|
+
if (k.type === 'property_identifier' || k.type === 'identifier') fieldName = k.text;
|
|
934
|
+
else if (k.type === 'string' || k.type === 'template_string') fieldName = stripQuotes(k.text);
|
|
935
|
+
if (v) handlerName = identifierLikeName(v);
|
|
936
|
+
} else if (child.type === 'method_definition' || child.type === 'shorthand_property_identifier') {
|
|
937
|
+
const k = child.childForFieldName('name');
|
|
938
|
+
if (k) fieldName = k.text;
|
|
939
|
+
handlerName = fieldName ?? undefined;
|
|
940
|
+
} else {
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
if (!fieldName) continue;
|
|
944
|
+
routes.push({
|
|
945
|
+
method: kind.toUpperCase(),
|
|
946
|
+
path: fieldName,
|
|
947
|
+
framework: 'graphql',
|
|
948
|
+
handlerName,
|
|
949
|
+
line,
|
|
950
|
+
protocol: 'graphql',
|
|
951
|
+
operation: fieldName,
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
return routes.length > 0 ? routes : null;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
/**
|
|
959
|
+
* v9 Track-H — recognize a GraphQL client call.
|
|
960
|
+
*
|
|
961
|
+
* Forms supported:
|
|
962
|
+
* client.query({ query: GET_USER }) — operation lifted from the
|
|
963
|
+
* document-identifier name
|
|
964
|
+
* client.mutate({ mutation: gql`mutation Foo { createUser { id } }` })
|
|
965
|
+
* — operation lifted from the gql
|
|
966
|
+
* body's first top-level field
|
|
967
|
+
* useQuery(GET_USER, { variables: ... }) — useQuery / useMutation hooks
|
|
968
|
+
* useQuery(gql`query Foo { user { id } }`, ...)
|
|
969
|
+
*
|
|
970
|
+
* Operation matching priority:
|
|
971
|
+
* 1. Top-level field name parsed from the gql body (matches resolver-map keys)
|
|
972
|
+
* 2. Operation name from the gql header
|
|
973
|
+
* 3. Document-identifier (e.g. GET_USER) as a fallback so the call is still
|
|
974
|
+
* recorded — won't link to a resolver but the row carries the evidence.
|
|
975
|
+
*/
|
|
976
|
+
function tryExtractGraphqlClientCall(
|
|
977
|
+
node: Parser.SyntaxNode,
|
|
978
|
+
method: string,
|
|
979
|
+
kind: 'query' | 'mutation' | 'subscription',
|
|
980
|
+
): ServiceCallDef | null {
|
|
981
|
+
const args = node.childForFieldName('arguments');
|
|
982
|
+
if (!args) return null;
|
|
983
|
+
const first = args.namedChildren[0];
|
|
984
|
+
if (!first) return null;
|
|
985
|
+
|
|
986
|
+
let docNode: Parser.SyntaxNode | null = null;
|
|
987
|
+
let documentIdent: string | null = null;
|
|
988
|
+
|
|
989
|
+
if (first.type === 'object') {
|
|
990
|
+
// client.{query,mutate,subscribe}({ query | mutation | subscription: DOC })
|
|
991
|
+
const fields = readObjectLiteralFields(first);
|
|
992
|
+
const fieldKey = kind === 'mutation' ? 'mutation'
|
|
993
|
+
: kind === 'subscription' ? 'subscription' : 'query';
|
|
994
|
+
const v = fields.get(fieldKey);
|
|
995
|
+
if (!v) return null;
|
|
996
|
+
if (v.type === 'identifier') { documentIdent = v.text; }
|
|
997
|
+
else docNode = v;
|
|
998
|
+
} else if (first.type === 'identifier') {
|
|
999
|
+
// useQuery(GET_USER, ...)
|
|
1000
|
+
documentIdent = first.text;
|
|
1001
|
+
} else {
|
|
1002
|
+
// useQuery(gql`...`, ...) — the tagged template literal itself
|
|
1003
|
+
docNode = first;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
let opName: string | undefined;
|
|
1007
|
+
let opField: string | undefined;
|
|
1008
|
+
let rawTarget: string;
|
|
1009
|
+
|
|
1010
|
+
if (docNode) {
|
|
1011
|
+
const txt = docNode.text;
|
|
1012
|
+
rawTarget = txt.slice(0, 240);
|
|
1013
|
+
const parsed = parseGqlOperation(txt);
|
|
1014
|
+
if (parsed) {
|
|
1015
|
+
opName = parsed.opName;
|
|
1016
|
+
opField = parsed.fieldName;
|
|
1017
|
+
}
|
|
1018
|
+
} else if (documentIdent) {
|
|
1019
|
+
rawTarget = documentIdent;
|
|
1020
|
+
} else {
|
|
1021
|
+
return null;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// Operation field is what matches a resolver-map key on the server. Op name
|
|
1025
|
+
// is the user-given operation alias (GetUser). Doc-ident is the const name.
|
|
1026
|
+
const operation = opField ?? opName ?? documentIdent ?? undefined;
|
|
1027
|
+
if (!operation) return null;
|
|
1028
|
+
|
|
1029
|
+
const def: ServiceCallDef = {
|
|
1030
|
+
protocol: 'graphql',
|
|
1031
|
+
method: kind.toUpperCase(),
|
|
1032
|
+
rawTarget,
|
|
1033
|
+
framework: `graphql-${method}`,
|
|
1034
|
+
line: node.startPosition.row,
|
|
1035
|
+
// Higher confidence when we parsed the field name out of the gql body;
|
|
1036
|
+
// lower when all we have is a document const name.
|
|
1037
|
+
confidence: opField ? 0.9 : (opName ? 0.85 : 0.65),
|
|
1038
|
+
operation,
|
|
1039
|
+
};
|
|
1040
|
+
if (opName || documentIdent) {
|
|
1041
|
+
def.metadataJson = JSON.stringify({
|
|
1042
|
+
operationName: opName ?? null,
|
|
1043
|
+
documentIdent: documentIdent ?? null,
|
|
1044
|
+
fieldName: opField ?? null,
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
return def;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/**
|
|
1051
|
+
* Pull operation kind, name, and first top-level selection field out of a
|
|
1052
|
+
* GraphQL document literal. Forgiving: handles `gql\`…\``, `\`…\``, bare
|
|
1053
|
+
* strings, and shorthand `{ field }` query bodies.
|
|
1054
|
+
*
|
|
1055
|
+
* Returns null when nothing GraphQL-shaped is found.
|
|
1056
|
+
*/
|
|
1057
|
+
function parseGqlOperation(src: string): { opKind: string; opName?: string; fieldName?: string } | null {
|
|
1058
|
+
let s = src;
|
|
1059
|
+
// strip a leading tag (gql, graphql, parse, etc.) if it's followed by a backtick
|
|
1060
|
+
s = s.replace(/^[A-Za-z_][A-Za-z0-9_]*`/, '`');
|
|
1061
|
+
// trim wrapping backticks / quotes
|
|
1062
|
+
s = s.replace(/^[`'"]/, '').replace(/[`'"]\s*$/, '').trim();
|
|
1063
|
+
if (!s) return null;
|
|
1064
|
+
// strip /* ... */ block comments and # line comments so the regexes below
|
|
1065
|
+
// don't trip on doc-comments before the operation header
|
|
1066
|
+
s = s.replace(/\/\*[\s\S]*?\*\//g, '').replace(/#[^\n]*/g, '').trim();
|
|
1067
|
+
|
|
1068
|
+
let opKind = 'query';
|
|
1069
|
+
let opName: string | undefined;
|
|
1070
|
+
const opMatch = s.match(/^(query|mutation|subscription)\b(?:\s+([A-Za-z_][A-Za-z0-9_]*))?/);
|
|
1071
|
+
if (opMatch) {
|
|
1072
|
+
opKind = opMatch[1];
|
|
1073
|
+
opName = opMatch[2];
|
|
1074
|
+
s = s.slice(opMatch[0].length).trim();
|
|
1075
|
+
// drop ($vars) declaration block when present
|
|
1076
|
+
if (s.startsWith('(')) {
|
|
1077
|
+
const close = balancedClose(s, '(', ')');
|
|
1078
|
+
if (close > 0) s = s.slice(close + 1).trim();
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
// first '{' opens the selection set
|
|
1082
|
+
const openBrace = s.indexOf('{');
|
|
1083
|
+
if (openBrace < 0) return { opKind, opName };
|
|
1084
|
+
s = s.slice(openBrace + 1).trim();
|
|
1085
|
+
// first identifier in the selection set is the top-level field
|
|
1086
|
+
const fieldMatch = s.match(/^([A-Za-z_][A-Za-z0-9_]*)/);
|
|
1087
|
+
const fieldName = fieldMatch ? fieldMatch[1] : undefined;
|
|
1088
|
+
return { opKind, opName, fieldName };
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
/**
|
|
1092
|
+
* v9 Track-H — extract a `const X = gql\`...\`` document definition.
|
|
1093
|
+
*
|
|
1094
|
+
* Emitted as a sentinel service_call with framework='gql-doc' so the resolver
|
|
1095
|
+
* can map document identifiers (GET_USER) to their parsed operation field
|
|
1096
|
+
* (user). The row carries:
|
|
1097
|
+
* - raw_target = the document constant name
|
|
1098
|
+
* - operation = the parsed top-level field name (if any)
|
|
1099
|
+
* - method = operation kind (QUERY / MUTATION / SUBSCRIPTION)
|
|
1100
|
+
* - confidence = 0.4 — these aren't actual outbound calls, so we keep
|
|
1101
|
+
* confidence low to discourage them from showing up in
|
|
1102
|
+
* risk / context surfaces.
|
|
1103
|
+
*/
|
|
1104
|
+
function tryExtractGqlDocDefinition(node: Parser.SyntaxNode): ServiceCallDef | null {
|
|
1105
|
+
const nameNode = node.childForFieldName('name');
|
|
1106
|
+
if (!nameNode || (nameNode.type !== 'identifier' && nameNode.type !== 'property_identifier')) {
|
|
1107
|
+
return null;
|
|
1108
|
+
}
|
|
1109
|
+
const valNode = node.childForFieldName('value');
|
|
1110
|
+
if (!valNode) return null;
|
|
1111
|
+
// Accept both `gql\`...\`` (tagged template) and a bare template literal
|
|
1112
|
+
// that obviously wraps a GraphQL operation header.
|
|
1113
|
+
let body: string | null = null;
|
|
1114
|
+
if (valNode.type === 'call_expression') {
|
|
1115
|
+
// Some tag wrappers parse as call_expression(text("gql"), template_string).
|
|
1116
|
+
body = valNode.text;
|
|
1117
|
+
} else if (valNode.type === 'template_string') {
|
|
1118
|
+
body = valNode.text;
|
|
1119
|
+
}
|
|
1120
|
+
if (!body) return null;
|
|
1121
|
+
// Only emit when this actually looks GraphQL-shaped.
|
|
1122
|
+
const parsed = parseGqlOperation(body);
|
|
1123
|
+
if (!parsed || (!parsed.opName && !parsed.fieldName)) return null;
|
|
1124
|
+
const def: ServiceCallDef = {
|
|
1125
|
+
protocol: 'graphql',
|
|
1126
|
+
method: parsed.opKind.toUpperCase(),
|
|
1127
|
+
rawTarget: nameNode.text,
|
|
1128
|
+
framework: 'gql-doc',
|
|
1129
|
+
line: node.startPosition.row,
|
|
1130
|
+
confidence: 0.4,
|
|
1131
|
+
operation: parsed.fieldName ?? parsed.opName ?? nameNode.text,
|
|
1132
|
+
};
|
|
1133
|
+
def.metadataJson = JSON.stringify({
|
|
1134
|
+
documentIdent: nameNode.text,
|
|
1135
|
+
operationName: parsed.opName ?? null,
|
|
1136
|
+
fieldName: parsed.fieldName ?? null,
|
|
1137
|
+
});
|
|
1138
|
+
return def;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
/**
|
|
1142
|
+
* v9 Track-H — recognize a messaging PRODUCER call.
|
|
1143
|
+
*
|
|
1144
|
+
* Decisions made by inspecting (a) the method name and (b) the shape of the
|
|
1145
|
+
* first argument, with the receiver name as a final disambiguation cue when
|
|
1146
|
+
* multiple protocols share a method (publish is the prototypical example).
|
|
1147
|
+
*
|
|
1148
|
+
* Kafka: producer.send({ topic: 'orders', messages: [...] })
|
|
1149
|
+
* Kafka v1: kafkaProducer.send('orders', message)
|
|
1150
|
+
* SQS: sqs.sendMessage({ QueueUrl: '...', MessageBody: '...' })
|
|
1151
|
+
* SNS: sns.publish({ TopicArn: '...', Message: '...' })
|
|
1152
|
+
* RabbitMQ: channel.publish('exch', 'rk', body) / channel.sendToQueue('q', body)
|
|
1153
|
+
* NATS: nc.publish('subject', data)
|
|
1154
|
+
* Redis: redis.publish('chan', msg)
|
|
1155
|
+
*
|
|
1156
|
+
* On match returns a ServiceCallDef carrying the relevant protocol field
|
|
1157
|
+
* (topic / queue / exchange).
|
|
1158
|
+
*/
|
|
1159
|
+
function tryExtractMessagingProducer(
|
|
1160
|
+
node: Parser.SyntaxNode,
|
|
1161
|
+
funcNode: Parser.SyntaxNode,
|
|
1162
|
+
): ServiceCallDef | null {
|
|
1163
|
+
const propNode = funcNode.childForFieldName('property');
|
|
1164
|
+
const objNode = funcNode.childForFieldName('object');
|
|
1165
|
+
if (!propNode || !objNode) return null;
|
|
1166
|
+
const method = propNode.text;
|
|
1167
|
+
const recvName = objNode.type === 'identifier' ? objNode.text : null;
|
|
1168
|
+
const args = node.childForFieldName('arguments');
|
|
1169
|
+
if (!args) return null;
|
|
1170
|
+
const named = args.namedChildren;
|
|
1171
|
+
const first = named[0];
|
|
1172
|
+
if (!first) return null;
|
|
1173
|
+
|
|
1174
|
+
// Helper: read a string literal from a node (returns null otherwise).
|
|
1175
|
+
const litString = (n: Parser.SyntaxNode | undefined): string | null => {
|
|
1176
|
+
if (!n) return null;
|
|
1177
|
+
if (n.type === 'string' || n.type === 'template_string') return stripQuotes(n.text);
|
|
1178
|
+
return null;
|
|
1179
|
+
};
|
|
1180
|
+
|
|
1181
|
+
// ── send: Kafka topic ─────────────────────────────────────────────────
|
|
1182
|
+
if (method === 'send') {
|
|
1183
|
+
if (first.type === 'object') {
|
|
1184
|
+
const fields = readObjectLiteralFields(first);
|
|
1185
|
+
const topicNode = fields.get('topic');
|
|
1186
|
+
const topic = litString(topicNode);
|
|
1187
|
+
if (topic) {
|
|
1188
|
+
return mkMsgCall(node, 'kafka', method, 'kafkajs', { topic, line: node.startPosition.row });
|
|
1189
|
+
}
|
|
1190
|
+
} else if (first.type === 'string' || first.type === 'template_string') {
|
|
1191
|
+
// Older kafkajs / node-rdkafka: producer.send('topic', msg)
|
|
1192
|
+
if (receiverHintsProtocol(recvName, 'kafka')) {
|
|
1193
|
+
return mkMsgCall(node, 'kafka', method, 'kafka', { topic: stripQuotes(first.text), line: node.startPosition.row });
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// ── sendMessage: SQS ──────────────────────────────────────────────────
|
|
1199
|
+
if (method === 'sendMessage' && first.type === 'object') {
|
|
1200
|
+
const fields = readObjectLiteralFields(first);
|
|
1201
|
+
const queueUrl = litString(fields.get('QueueUrl'));
|
|
1202
|
+
if (queueUrl !== null) {
|
|
1203
|
+
return mkMsgCall(node, 'sqs', method, 'aws-sdk-sqs', {
|
|
1204
|
+
queue: extractQueueName(queueUrl),
|
|
1205
|
+
rawTarget: queueUrl,
|
|
1206
|
+
line: node.startPosition.row,
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
// ── sendToQueue: RabbitMQ ─────────────────────────────────────────────
|
|
1212
|
+
if (method === 'sendToQueue') {
|
|
1213
|
+
const queue = litString(first);
|
|
1214
|
+
if (queue) {
|
|
1215
|
+
return mkMsgCall(node, 'rabbitmq', method, 'amqplib', { queue, line: node.startPosition.row });
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// ── publish: SNS / RabbitMQ / NATS / Redis ────────────────────────────
|
|
1220
|
+
if (method === 'publish') {
|
|
1221
|
+
if (first.type === 'object') {
|
|
1222
|
+
// SNS: { TopicArn, Message }
|
|
1223
|
+
const fields = readObjectLiteralFields(first);
|
|
1224
|
+
const topicArn = litString(fields.get('TopicArn'));
|
|
1225
|
+
if (topicArn !== null) {
|
|
1226
|
+
return mkMsgCall(node, 'sns', method, 'aws-sdk-sns', {
|
|
1227
|
+
topic: extractTopicNameFromArn(topicArn),
|
|
1228
|
+
rawTarget: topicArn,
|
|
1229
|
+
line: node.startPosition.row,
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
// RabbitMQ: channel.publish('exchange', 'routingKey', body)
|
|
1234
|
+
if (receiverHintsProtocol(recvName, 'rabbitmq')) {
|
|
1235
|
+
const exchange = litString(first);
|
|
1236
|
+
const routingKey = litString(named[1]);
|
|
1237
|
+
if (exchange !== null) {
|
|
1238
|
+
return mkMsgCall(node, 'rabbitmq', method, 'amqplib', {
|
|
1239
|
+
exchange,
|
|
1240
|
+
metadataJson: routingKey ? JSON.stringify({ routingKey }) : undefined,
|
|
1241
|
+
line: node.startPosition.row,
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
// NATS / Redis: nc.publish('subject', data) / redis.publish('chan', msg)
|
|
1246
|
+
const subject = litString(first);
|
|
1247
|
+
if (subject !== null) {
|
|
1248
|
+
if (receiverHintsProtocol(recvName, 'nats')) {
|
|
1249
|
+
return mkMsgCall(node, 'nats', method, 'nats', { topic: subject, line: node.startPosition.row });
|
|
1250
|
+
}
|
|
1251
|
+
if (receiverHintsProtocol(recvName, 'redis_pubsub')) {
|
|
1252
|
+
return mkMsgCall(node, 'redis_pubsub', method, 'redis', { topic: subject, line: node.startPosition.row });
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
return null;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
/**
|
|
1261
|
+
* v9 Track-H — recognize a messaging CONSUMER registration as a RouteDef.
|
|
1262
|
+
*
|
|
1263
|
+
* Kafka: consumer.subscribe({ topic | topics: [...] })
|
|
1264
|
+
* Rabbit: channel.consume('queue', handler)
|
|
1265
|
+
* SQS: sqs.receiveMessage({ QueueUrl })
|
|
1266
|
+
* NATS: nc.subscribe('subject')
|
|
1267
|
+
*
|
|
1268
|
+
* Returns one route per topic/queue. The handler is recovered when it's a
|
|
1269
|
+
* named identifier in the args list.
|
|
1270
|
+
*/
|
|
1271
|
+
function tryExtractMessagingConsumer(
|
|
1272
|
+
node: Parser.SyntaxNode,
|
|
1273
|
+
funcNode: Parser.SyntaxNode,
|
|
1274
|
+
named: Parser.SyntaxNode[],
|
|
1275
|
+
): RouteDef[] | null {
|
|
1276
|
+
if (funcNode.type !== 'member_expression') return null;
|
|
1277
|
+
const propNode = funcNode.childForFieldName('property');
|
|
1278
|
+
const objNode = funcNode.childForFieldName('object');
|
|
1279
|
+
if (!propNode || !objNode) return null;
|
|
1280
|
+
const method = propNode.text;
|
|
1281
|
+
const recvName = objNode.type === 'identifier' ? objNode.text : null;
|
|
1282
|
+
const first = named[0];
|
|
1283
|
+
if (!first) return null;
|
|
1284
|
+
const line = node.startPosition.row;
|
|
1285
|
+
|
|
1286
|
+
const litString = (n: Parser.SyntaxNode | undefined): string | null => {
|
|
1287
|
+
if (!n) return null;
|
|
1288
|
+
if (n.type === 'string' || n.type === 'template_string') return stripQuotes(n.text);
|
|
1289
|
+
return null;
|
|
1290
|
+
};
|
|
1291
|
+
|
|
1292
|
+
// ── Kafka consumer.subscribe({ topic | topics }) ─────────────────────
|
|
1293
|
+
if (method === 'subscribe' && first.type === 'object'
|
|
1294
|
+
&& receiverHintsProtocol(recvName, 'kafka')) {
|
|
1295
|
+
const fields = readObjectLiteralFields(first);
|
|
1296
|
+
const single = litString(fields.get('topic'));
|
|
1297
|
+
if (single) {
|
|
1298
|
+
return [mkMsgRoute('kafka', 'kafkajs', { topic: single, line, path: single })];
|
|
1299
|
+
}
|
|
1300
|
+
const arrNode = fields.get('topics');
|
|
1301
|
+
if (arrNode && arrNode.type === 'array') {
|
|
1302
|
+
const out: RouteDef[] = [];
|
|
1303
|
+
for (const el of arrNode.namedChildren) {
|
|
1304
|
+
const t = litString(el);
|
|
1305
|
+
if (t) out.push(mkMsgRoute('kafka', 'kafkajs', { topic: t, line, path: t }));
|
|
1306
|
+
}
|
|
1307
|
+
if (out.length > 0) return out;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// ── NATS nc.subscribe('subject') ─────────────────────────────────────
|
|
1312
|
+
if (method === 'subscribe' && receiverHintsProtocol(recvName, 'nats')) {
|
|
1313
|
+
const subject = litString(first);
|
|
1314
|
+
if (subject) {
|
|
1315
|
+
return [mkMsgRoute('nats', 'nats', { topic: subject, line, path: subject })];
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
// ── Redis subscribe('chan') ──────────────────────────────────────────
|
|
1320
|
+
if (method === 'subscribe' && receiverHintsProtocol(recvName, 'redis_pubsub')) {
|
|
1321
|
+
const chan = litString(first);
|
|
1322
|
+
if (chan) {
|
|
1323
|
+
return [mkMsgRoute('redis_pubsub', 'redis', { topic: chan, line, path: chan })];
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
// ── RabbitMQ channel.consume('queue', handler) ────────────────────────
|
|
1328
|
+
if (method === 'consume' && receiverHintsProtocol(recvName, 'rabbitmq')) {
|
|
1329
|
+
const queue = litString(first);
|
|
1330
|
+
if (queue) {
|
|
1331
|
+
const handlerName = named[1] ? identifierLikeName(named[1]) : undefined;
|
|
1332
|
+
return [mkMsgRoute('rabbitmq', 'amqplib', {
|
|
1333
|
+
queue, line, path: queue, handlerName,
|
|
1334
|
+
})];
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
// ── SQS receiveMessage({ QueueUrl }) ─────────────────────────────────
|
|
1339
|
+
if (method === 'receiveMessage' && first.type === 'object') {
|
|
1340
|
+
const fields = readObjectLiteralFields(first);
|
|
1341
|
+
const queueUrl = litString(fields.get('QueueUrl'));
|
|
1342
|
+
if (queueUrl !== null) {
|
|
1343
|
+
return [mkMsgRoute('sqs', 'aws-sdk-sqs', {
|
|
1344
|
+
queue: extractQueueName(queueUrl), line, path: queueUrl,
|
|
1345
|
+
})];
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
return null;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
interface MsgCallOpts {
|
|
1353
|
+
topic?: string; queue?: string; exchange?: string;
|
|
1354
|
+
rawTarget?: string;
|
|
1355
|
+
line: number;
|
|
1356
|
+
metadataJson?: string;
|
|
1357
|
+
}
|
|
1358
|
+
function mkMsgCall(
|
|
1359
|
+
node: Parser.SyntaxNode, protocol: MsgProtocol, method: string,
|
|
1360
|
+
framework: string, opts: MsgCallOpts,
|
|
1361
|
+
): ServiceCallDef {
|
|
1362
|
+
const rawTarget = opts.rawTarget ?? opts.topic ?? opts.queue ?? opts.exchange ?? '';
|
|
1363
|
+
return {
|
|
1364
|
+
protocol,
|
|
1365
|
+
method: 'PUBLISH',
|
|
1366
|
+
rawTarget: rawTarget.slice(0, 240),
|
|
1367
|
+
framework,
|
|
1368
|
+
line: opts.line,
|
|
1369
|
+
confidence: 0.9,
|
|
1370
|
+
topic: opts.topic,
|
|
1371
|
+
queue: opts.queue,
|
|
1372
|
+
exchange: opts.exchange,
|
|
1373
|
+
metadataJson: opts.metadataJson,
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
interface MsgRouteOpts {
|
|
1378
|
+
topic?: string; queue?: string; exchange?: string;
|
|
1379
|
+
path: string; line: number; handlerName?: string;
|
|
1380
|
+
}
|
|
1381
|
+
function mkMsgRoute(
|
|
1382
|
+
protocol: MsgProtocol, framework: string, opts: MsgRouteOpts,
|
|
1383
|
+
): RouteDef {
|
|
1384
|
+
return {
|
|
1385
|
+
method: 'CONSUME',
|
|
1386
|
+
path: opts.path,
|
|
1387
|
+
framework,
|
|
1388
|
+
handlerName: opts.handlerName,
|
|
1389
|
+
line: opts.line,
|
|
1390
|
+
protocol,
|
|
1391
|
+
topic: opts.topic,
|
|
1392
|
+
queue: opts.queue,
|
|
1393
|
+
exchange: opts.exchange,
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
/** SQS QueueUrl → final path segment (queue name). */
|
|
1398
|
+
function extractQueueName(url: string): string {
|
|
1399
|
+
if (!url) return url;
|
|
1400
|
+
const slash = url.lastIndexOf('/');
|
|
1401
|
+
return slash >= 0 ? url.slice(slash + 1) : url;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
/** SNS TopicArn → topic name (the part after the last colon). */
|
|
1405
|
+
function extractTopicNameFromArn(arn: string): string {
|
|
1406
|
+
if (!arn) return arn;
|
|
1407
|
+
const colon = arn.lastIndexOf(':');
|
|
1408
|
+
return colon >= 0 ? arn.slice(colon + 1) : arn;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
/** Index of the matching close paren/brace for a string that starts with `open`. */
|
|
1412
|
+
function balancedClose(s: string, open: string, close: string): number {
|
|
1413
|
+
let depth = 0;
|
|
1414
|
+
for (let i = 0; i < s.length; i++) {
|
|
1415
|
+
const ch = s[i];
|
|
1416
|
+
if (ch === open) depth++;
|
|
1417
|
+
else if (ch === close) {
|
|
1418
|
+
depth--;
|
|
1419
|
+
if (depth === 0) return i;
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
return -1;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
function mkDef(name: string, kind: SymbolKind, node: Parser.SyntaxNode): SymbolDef {
|
|
1426
|
+
return {
|
|
1427
|
+
name,
|
|
1428
|
+
kind,
|
|
1429
|
+
lineStart: node.startPosition.row,
|
|
1430
|
+
lineEnd: node.endPosition.row,
|
|
1431
|
+
colStart: node.startPosition.column,
|
|
1432
|
+
colEnd: node.endPosition.column,
|
|
1433
|
+
signature: firstLine(node),
|
|
1434
|
+
};
|
|
1435
|
+
}
|