@mka-rainmaker/ama 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/LICENSE +21 -0
- package/README.md +149 -0
- package/dist/analyzers/baseline/analyzer.d.ts +47 -0
- package/dist/analyzers/baseline/analyzer.d.ts.map +1 -0
- package/dist/analyzers/baseline/analyzer.js +84 -0
- package/dist/analyzers/baseline/analyzer.js.map +1 -0
- package/dist/analyzers/baseline/c.d.ts +12 -0
- package/dist/analyzers/baseline/c.d.ts.map +1 -0
- package/dist/analyzers/baseline/c.js +56 -0
- package/dist/analyzers/baseline/c.js.map +1 -0
- package/dist/analyzers/baseline/config.d.ts +21 -0
- package/dist/analyzers/baseline/config.d.ts.map +1 -0
- package/dist/analyzers/baseline/config.js +32 -0
- package/dist/analyzers/baseline/config.js.map +1 -0
- package/dist/analyzers/baseline/csharp.d.ts +9 -0
- package/dist/analyzers/baseline/csharp.d.ts.map +1 -0
- package/dist/analyzers/baseline/csharp.js +107 -0
- package/dist/analyzers/baseline/csharp.js.map +1 -0
- package/dist/analyzers/baseline/go.d.ts +11 -0
- package/dist/analyzers/baseline/go.d.ts.map +1 -0
- package/dist/analyzers/baseline/go.js +66 -0
- package/dist/analyzers/baseline/go.js.map +1 -0
- package/dist/analyzers/baseline/java.d.ts +9 -0
- package/dist/analyzers/baseline/java.d.ts.map +1 -0
- package/dist/analyzers/baseline/java.js +50 -0
- package/dist/analyzers/baseline/java.js.map +1 -0
- package/dist/analyzers/baseline/javascript.d.ts +10 -0
- package/dist/analyzers/baseline/javascript.d.ts.map +1 -0
- package/dist/analyzers/baseline/javascript.js +55 -0
- package/dist/analyzers/baseline/javascript.js.map +1 -0
- package/dist/analyzers/baseline/kotlin.d.ts +11 -0
- package/dist/analyzers/baseline/kotlin.d.ts.map +1 -0
- package/dist/analyzers/baseline/kotlin.js +67 -0
- package/dist/analyzers/baseline/kotlin.js.map +1 -0
- package/dist/analyzers/baseline/paths.d.ts +6 -0
- package/dist/analyzers/baseline/paths.d.ts.map +1 -0
- package/dist/analyzers/baseline/paths.js +17 -0
- package/dist/analyzers/baseline/paths.js.map +1 -0
- package/dist/analyzers/baseline/php.d.ts +11 -0
- package/dist/analyzers/baseline/php.d.ts.map +1 -0
- package/dist/analyzers/baseline/php.js +76 -0
- package/dist/analyzers/baseline/php.js.map +1 -0
- package/dist/analyzers/baseline/python.d.ts +10 -0
- package/dist/analyzers/baseline/python.d.ts.map +1 -0
- package/dist/analyzers/baseline/python.js +63 -0
- package/dist/analyzers/baseline/python.js.map +1 -0
- package/dist/analyzers/baseline/rust.d.ts +10 -0
- package/dist/analyzers/baseline/rust.d.ts.map +1 -0
- package/dist/analyzers/baseline/rust.js +45 -0
- package/dist/analyzers/baseline/rust.js.map +1 -0
- package/dist/analyzers/baseline/swift.d.ts +11 -0
- package/dist/analyzers/baseline/swift.d.ts.map +1 -0
- package/dist/analyzers/baseline/swift.js +19 -0
- package/dist/analyzers/baseline/swift.js.map +1 -0
- package/dist/analyzers/baseline/treesitter.d.ts +11 -0
- package/dist/analyzers/baseline/treesitter.d.ts.map +1 -0
- package/dist/analyzers/baseline/treesitter.js +87 -0
- package/dist/analyzers/baseline/treesitter.js.map +1 -0
- package/dist/analyzers/baseline/walk.d.ts +26 -0
- package/dist/analyzers/baseline/walk.d.ts.map +1 -0
- package/dist/analyzers/baseline/walk.js +76 -0
- package/dist/analyzers/baseline/walk.js.map +1 -0
- package/dist/analyzers/registry.d.ts +19 -0
- package/dist/analyzers/registry.d.ts.map +1 -0
- package/dist/analyzers/registry.js +43 -0
- package/dist/analyzers/registry.js.map +1 -0
- package/dist/analyzers/sfc/analyzer.d.ts +17 -0
- package/dist/analyzers/sfc/analyzer.d.ts.map +1 -0
- package/dist/analyzers/sfc/analyzer.js +141 -0
- package/dist/analyzers/sfc/analyzer.js.map +1 -0
- package/dist/analyzers/sidecar/analyzer.d.ts +29 -0
- package/dist/analyzers/sidecar/analyzer.d.ts.map +1 -0
- package/dist/analyzers/sidecar/analyzer.js +114 -0
- package/dist/analyzers/sidecar/analyzer.js.map +1 -0
- package/dist/analyzers/sidecar/protocol.d.ts +508 -0
- package/dist/analyzers/sidecar/protocol.d.ts.map +1 -0
- package/dist/analyzers/sidecar/protocol.js +102 -0
- package/dist/analyzers/sidecar/protocol.js.map +1 -0
- package/dist/analyzers/types.d.ts +46 -0
- package/dist/analyzers/types.d.ts.map +1 -0
- package/dist/analyzers/types.js +2 -0
- package/dist/analyzers/types.js.map +1 -0
- package/dist/analyzers/typescript/analyzer.d.ts +126 -0
- package/dist/analyzers/typescript/analyzer.d.ts.map +1 -0
- package/dist/analyzers/typescript/analyzer.js +1600 -0
- package/dist/analyzers/typescript/analyzer.js.map +1 -0
- package/dist/cli/commands/cycles.d.ts +6 -0
- package/dist/cli/commands/cycles.d.ts.map +1 -0
- package/dist/cli/commands/cycles.js +27 -0
- package/dist/cli/commands/cycles.js.map +1 -0
- package/dist/cli/commands/files.d.ts +6 -0
- package/dist/cli/commands/files.d.ts.map +1 -0
- package/dist/cli/commands/files.js +33 -0
- package/dist/cli/commands/files.js.map +1 -0
- package/dist/cli/commands/impact.d.ts +18 -0
- package/dist/cli/commands/impact.d.ts.map +1 -0
- package/dist/cli/commands/impact.js +113 -0
- package/dist/cli/commands/impact.js.map +1 -0
- package/dist/cli/commands/lifecycle.d.ts +5 -0
- package/dist/cli/commands/lifecycle.d.ts.map +1 -0
- package/dist/cli/commands/lifecycle.js +83 -0
- package/dist/cli/commands/lifecycle.js.map +1 -0
- package/dist/cli/commands/query.d.ts +31 -0
- package/dist/cli/commands/query.d.ts.map +1 -0
- package/dist/cli/commands/query.js +187 -0
- package/dist/cli/commands/query.js.map +1 -0
- package/dist/cli/commands/search.d.ts +21 -0
- package/dist/cli/commands/search.d.ts.map +1 -0
- package/dist/cli/commands/search.js +160 -0
- package/dist/cli/commands/search.js.map +1 -0
- package/dist/cli/commands/status.d.ts +6 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +63 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/commands/sync.d.ts +6 -0
- package/dist/cli/commands/sync.d.ts.map +1 -0
- package/dist/cli/commands/sync.js +57 -0
- package/dist/cli/commands/sync.js.map +1 -0
- package/dist/cli/emit.d.ts +9 -0
- package/dist/cli/emit.d.ts.map +1 -0
- package/dist/cli/emit.js +10 -0
- package/dist/cli/emit.js.map +1 -0
- package/dist/cli/index.d.ts +37 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +128 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/paths.d.ts +7 -0
- package/dist/cli/paths.d.ts.map +1 -0
- package/dist/cli/paths.js +10 -0
- package/dist/cli/paths.js.map +1 -0
- package/dist/cli/query-runner.d.ts +13 -0
- package/dist/cli/query-runner.d.ts.map +1 -0
- package/dist/cli/query-runner.js +33 -0
- package/dist/cli/query-runner.js.map +1 -0
- package/dist/graph/dispatch.d.ts +17 -0
- package/dist/graph/dispatch.d.ts.map +1 -0
- package/dist/graph/dispatch.js +82 -0
- package/dist/graph/dispatch.js.map +1 -0
- package/dist/graph/id.d.ts +19 -0
- package/dist/graph/id.d.ts.map +1 -0
- package/dist/graph/id.js +17 -0
- package/dist/graph/id.js.map +1 -0
- package/dist/graph/index.d.ts +6 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/index.js +4 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/graph/types.d.ts +71 -0
- package/dist/graph/types.d.ts.map +1 -0
- package/dist/graph/types.js +52 -0
- package/dist/graph/types.js.map +1 -0
- package/dist/indexer/debouncer.d.ts +32 -0
- package/dist/indexer/debouncer.d.ts.map +1 -0
- package/dist/indexer/debouncer.js +81 -0
- package/dist/indexer/debouncer.js.map +1 -0
- package/dist/indexer/ignore.d.ts +55 -0
- package/dist/indexer/ignore.d.ts.map +1 -0
- package/dist/indexer/ignore.js +170 -0
- package/dist/indexer/ignore.js.map +1 -0
- package/dist/indexer/indexer.d.ts +112 -0
- package/dist/indexer/indexer.d.ts.map +1 -0
- package/dist/indexer/indexer.js +392 -0
- package/dist/indexer/indexer.js.map +1 -0
- package/dist/indexer/watcher.d.ts +50 -0
- package/dist/indexer/watcher.d.ts.map +1 -0
- package/dist/indexer/watcher.js +86 -0
- package/dist/indexer/watcher.js.map +1 -0
- package/dist/mcp/build-info.d.ts +16 -0
- package/dist/mcp/build-info.d.ts.map +1 -0
- package/dist/mcp/build-info.js +54 -0
- package/dist/mcp/build-info.js.map +1 -0
- package/dist/mcp/http.d.ts +18 -0
- package/dist/mcp/http.d.ts.map +1 -0
- package/dist/mcp/http.js +145 -0
- package/dist/mcp/http.js.map +1 -0
- package/dist/mcp/server.d.ts +22 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +401 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/session.d.ts +155 -0
- package/dist/mcp/session.d.ts.map +1 -0
- package/dist/mcp/session.js +319 -0
- package/dist/mcp/session.js.map +1 -0
- package/dist/query/service.d.ts +329 -0
- package/dist/query/service.d.ts.map +1 -0
- package/dist/query/service.js +959 -0
- package/dist/query/service.js.map +1 -0
- package/dist/runtime/entrypoint.d.ts +11 -0
- package/dist/runtime/entrypoint.d.ts.map +1 -0
- package/dist/runtime/entrypoint.js +22 -0
- package/dist/runtime/entrypoint.js.map +1 -0
- package/dist/runtime/quiet-sqlite-warning.d.ts +14 -0
- package/dist/runtime/quiet-sqlite-warning.d.ts.map +1 -0
- package/dist/runtime/quiet-sqlite-warning.js +26 -0
- package/dist/runtime/quiet-sqlite-warning.js.map +1 -0
- package/dist/runtime/wasm-tier.d.ts +2 -0
- package/dist/runtime/wasm-tier.d.ts.map +1 -0
- package/dist/runtime/wasm-tier.js +54 -0
- package/dist/runtime/wasm-tier.js.map +1 -0
- package/dist/store/memory.d.ts +54 -0
- package/dist/store/memory.d.ts.map +1 -0
- package/dist/store/memory.js +210 -0
- package/dist/store/memory.js.map +1 -0
- package/dist/store/sqlite.d.ts +38 -0
- package/dist/store/sqlite.d.ts.map +1 -0
- package/dist/store/sqlite.js +298 -0
- package/dist/store/sqlite.js.map +1 -0
- package/dist/store/types.d.ts +76 -0
- package/dist/store/types.d.ts.map +1 -0
- package/dist/store/types.js +2 -0
- package/dist/store/types.js.map +1 -0
- package/package.json +59 -0
|
@@ -0,0 +1,1600 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
import { deriveDispatchEdges, fileId, symbolId } from "../../graph/index.js";
|
|
4
|
+
/**
|
|
5
|
+
* Log a per-file analyzer failure to stderr and carry on. The deep TypeScript
|
|
6
|
+
* analyzer shares one ts.Program across the whole batch for cross-file
|
|
7
|
+
* resolution, so it can't give each file its own program the way the baseline
|
|
8
|
+
* analyzer isolates per file (ama-eww); instead each per-file *pass* is wrapped so
|
|
9
|
+
* one pathological file degrades to a skipped file rather than throwing out of
|
|
10
|
+
* analyze() — which the indexer's per-analyzer catch would turn into every .ts
|
|
11
|
+
* file vanishing from the graph. (ama-bm2)
|
|
12
|
+
*/
|
|
13
|
+
function reportFileFailure(rel, phase, err) {
|
|
14
|
+
console.error(`[ama] typescript analyzer failed on ${rel} (${phase}); skipping it. ` +
|
|
15
|
+
`${err instanceof Error ? err.message : String(err)}`);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Deep TypeScript analyzer built on the TypeScript Compiler API.
|
|
19
|
+
*
|
|
20
|
+
* Two passes over each source file:
|
|
21
|
+
* 1. Structural — emit nodes (File, Function, Class, Interface, Enum, TypeAlias,
|
|
22
|
+
* Method, Property — see `describe` for the full set) and `Defines` edges,
|
|
23
|
+
* recording each declaration's AST node so later references link back to ids.
|
|
24
|
+
* 2. Resolution — through the type checker, emit `Calls` edges (enclosing
|
|
25
|
+
* function/method → callee), `Inherits`/`Implements` edges (class → base
|
|
26
|
+
* class / interface), `UsesType` edges (enclosing symbol → each named type
|
|
27
|
+
* used in a parameter, return, or property annotation), and `Imports` edges
|
|
28
|
+
* (file → each symbol it imports or re-exports). References to symbols
|
|
29
|
+
* outside the analyzed set (library code) resolve to no node and are
|
|
30
|
+
* skipped, so the graph only asserts edges it can actually back.
|
|
31
|
+
*/
|
|
32
|
+
export class TypeScriptAnalyzer {
|
|
33
|
+
language = "typescript";
|
|
34
|
+
tier = "deep";
|
|
35
|
+
extensions = [".ts", ".tsx", ".mts", ".cts"];
|
|
36
|
+
analyze(root, files) {
|
|
37
|
+
const relByAbs = new Map();
|
|
38
|
+
for (const rel of files)
|
|
39
|
+
relByAbs.set(path.resolve(root, rel), rel);
|
|
40
|
+
const program = ts.createProgram([...relByAbs.keys()], {
|
|
41
|
+
target: ts.ScriptTarget.ES2022,
|
|
42
|
+
module: ts.ModuleKind.ESNext,
|
|
43
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
44
|
+
allowJs: false,
|
|
45
|
+
noEmit: true,
|
|
46
|
+
skipLibCheck: true,
|
|
47
|
+
});
|
|
48
|
+
const nodes = [];
|
|
49
|
+
const edges = [];
|
|
50
|
+
const resolution = { callsTotal: 0, callsResolved: 0, unresolved: {} };
|
|
51
|
+
/** AST declaration node -> graph node id, so resolved calls find their target. */
|
|
52
|
+
const declToId = new Map();
|
|
53
|
+
for (const [abs, rel] of relByAbs) {
|
|
54
|
+
const sf = program.getSourceFile(abs);
|
|
55
|
+
if (!sf)
|
|
56
|
+
continue;
|
|
57
|
+
try {
|
|
58
|
+
this.walkFile(sf, rel, nodes, edges, declToId);
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
reportFileFailure(rel, "structure", err);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const checker = program.getTypeChecker();
|
|
65
|
+
// Mount pre-pass (all files first): map each router declaration to the path
|
|
66
|
+
// prefix it's mounted at (app.use("/api", router)), so route detection can
|
|
67
|
+
// prepend it. Cross-file — the checker follows imported router symbols.
|
|
68
|
+
const mountPrefixes = new Map();
|
|
69
|
+
for (const [abs, rel] of relByAbs) {
|
|
70
|
+
const sf = program.getSourceFile(abs);
|
|
71
|
+
if (!sf)
|
|
72
|
+
continue;
|
|
73
|
+
try {
|
|
74
|
+
this.collectMounts(sf, checker, mountPrefixes);
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
reportFileFailure(rel, "mounts", err);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
for (const [abs, rel] of relByAbs) {
|
|
81
|
+
const sf = program.getSourceFile(abs);
|
|
82
|
+
if (!sf)
|
|
83
|
+
continue;
|
|
84
|
+
try {
|
|
85
|
+
// Routes first: it registers inline-handler arrows in declToId, so the
|
|
86
|
+
// following collectCalls attributes each handler's body to its node.
|
|
87
|
+
this.collectRoutes(sf, rel, declToId, checker, nodes, edges, root, mountPrefixes);
|
|
88
|
+
// File-based routes: the URL comes from the file path, not a call. (ama-rme.7, ama-w7g)
|
|
89
|
+
this.collectFileRoutes(sf, rel, declToId, checker, nodes, edges, root);
|
|
90
|
+
// Then callback-argument handlers (tap("name", () => …)) — same trick:
|
|
91
|
+
// register the arrow before collectCalls so its body attributes to it.
|
|
92
|
+
this.collectCallbackHandlers(sf, undefined, rel, sf, declToId, nodes, edges);
|
|
93
|
+
// Events after callback-handler synthesis so inline `.on("ch", () => …)`
|
|
94
|
+
// arrows are already handler nodes it can connect an emit to. (ama-hft.14)
|
|
95
|
+
this.collectEvents(sf, declToId, checker, edges, root);
|
|
96
|
+
// A call at module top-level (an entry block, a module-init side effect)
|
|
97
|
+
// attributes to the File node rather than being dropped for lack of an
|
|
98
|
+
// enclosing symbol, so find_callers surfaces module-level wiring as
|
|
99
|
+
// Defines/Imports edges already do. The File is the fallback only at true
|
|
100
|
+
// file scope (atFileScope), NOT inside a transparent callback — else a
|
|
101
|
+
// top-level `describe(() => it(() => expect()))` would make the file
|
|
102
|
+
// "call" it/expect. (ama-53q)
|
|
103
|
+
this.collectCalls(sf, undefined, declToId, checker, edges, root, resolution, fileId(rel), true);
|
|
104
|
+
this.collectVarReferences(sf, undefined, declToId, checker, edges, root);
|
|
105
|
+
this.collectHeritage(sf, declToId, checker, edges, root);
|
|
106
|
+
this.collectTypeUsages(sf, undefined, declToId, checker, edges, root);
|
|
107
|
+
this.collectImports(sf, fileId(rel), declToId, checker, edges, root);
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
reportFileFailure(rel, "resolution", err);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Dispatch fan-out (interface/override) is a whole-graph derivation now shared
|
|
114
|
+
// with the indexer's incremental re-derivation — see graph/dispatch.ts and
|
|
115
|
+
// ama-tr1. Over the full batch (a full index) it's exact; the derived edges are
|
|
116
|
+
// tagged provenance:"dispatch" so the indexer can re-derive them on reindex.
|
|
117
|
+
edges.push(...deriveDispatchEdges(nodes, edges));
|
|
118
|
+
return { nodes, edges: accumulateCallSites(edges), resolution };
|
|
119
|
+
}
|
|
120
|
+
walkFile(sf, rel, nodes, edges, declToId) {
|
|
121
|
+
const id = fileId(rel);
|
|
122
|
+
nodes.push({
|
|
123
|
+
id,
|
|
124
|
+
kind: "File",
|
|
125
|
+
name: path.basename(rel),
|
|
126
|
+
file: rel,
|
|
127
|
+
qualifiedName: "",
|
|
128
|
+
tier: "deep",
|
|
129
|
+
// The whole file, line 1 to EOF — `rangeOf` would skip leading comments
|
|
130
|
+
// (getStart trims trivia), truncating a File snippet's header.
|
|
131
|
+
range: { ...rangeOf(sf, sf), startLine: 1 },
|
|
132
|
+
});
|
|
133
|
+
// Register the file itself so module references (namespace imports,
|
|
134
|
+
// star re-exports) — which alias to the SourceFile — resolve to this node.
|
|
135
|
+
declToId.set(sf, id);
|
|
136
|
+
sf.forEachChild((child) => this.visit(child, sf, rel, id, "", nodes, edges, declToId));
|
|
137
|
+
}
|
|
138
|
+
visit(node, sf, rel, containerId, prefix, nodes, edges, declToId) {
|
|
139
|
+
// A `const f = () => …` / `= function …` is a VariableStatement wrapping the
|
|
140
|
+
// declaration we actually emit a node for; recurse into its declarations.
|
|
141
|
+
if (ts.isVariableStatement(node)) {
|
|
142
|
+
for (const declaration of node.declarationList.declarations) {
|
|
143
|
+
this.visit(declaration, sf, rel, containerId, prefix, nodes, edges, declToId);
|
|
144
|
+
}
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const decl = describe(node);
|
|
148
|
+
if (!decl) {
|
|
149
|
+
// `const X = { m() {…}, p: () => {…} }`: X itself isn't a node (there's no
|
|
150
|
+
// object kind), but recurse into its members so function-valued ones become
|
|
151
|
+
// `X.m` Method nodes. Otherwise logic that lives in object literals — every
|
|
152
|
+
// CLI command's `run`, dispatch tables, config handlers — is invisible to
|
|
153
|
+
// the call graph (its calls attribute to nothing).
|
|
154
|
+
if (ts.isVariableDeclaration(node) &&
|
|
155
|
+
node.initializer !== undefined &&
|
|
156
|
+
ts.isObjectLiteralExpression(node.initializer) &&
|
|
157
|
+
ts.isIdentifier(node.name)) {
|
|
158
|
+
const objPrefix = prefix ? `${prefix}.${node.name.text}` : node.name.text;
|
|
159
|
+
// A *typed* object const (`const spec: LanguageSpec = {…}`) is a named,
|
|
160
|
+
// typed symbol worth a node — so it's queryable and its `: T` annotation
|
|
161
|
+
// has an owner to hang the UsesType edge on (collectTypeUsages keys off
|
|
162
|
+
// declToId). An *untyped* object literal stays node-less, so we don't emit
|
|
163
|
+
// a node per anonymous config/dispatch table. (ama-g73)
|
|
164
|
+
if (node.type) {
|
|
165
|
+
const id = symbolId({ file: rel, qualifiedName: objPrefix });
|
|
166
|
+
nodes.push({
|
|
167
|
+
id,
|
|
168
|
+
kind: "Variable",
|
|
169
|
+
name: node.name.text,
|
|
170
|
+
file: rel,
|
|
171
|
+
qualifiedName: objPrefix,
|
|
172
|
+
tier: "deep",
|
|
173
|
+
range: rangeOf(node, sf),
|
|
174
|
+
});
|
|
175
|
+
edges.push({ from: containerId, to: id, kind: "Defines" });
|
|
176
|
+
declToId.set(node, id);
|
|
177
|
+
}
|
|
178
|
+
for (const member of node.initializer.properties) {
|
|
179
|
+
this.visit(member, sf, rel, containerId, objPrefix, nodes, edges, declToId);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const qualifiedName = prefix ? `${prefix}.${decl.name}` : decl.name;
|
|
185
|
+
const id = symbolId({ file: rel, qualifiedName });
|
|
186
|
+
nodes.push({
|
|
187
|
+
id,
|
|
188
|
+
kind: decl.kind,
|
|
189
|
+
name: decl.name,
|
|
190
|
+
file: rel,
|
|
191
|
+
qualifiedName,
|
|
192
|
+
tier: "deep",
|
|
193
|
+
range: rangeOf(node, sf),
|
|
194
|
+
});
|
|
195
|
+
edges.push({ from: containerId, to: id, kind: "Defines" });
|
|
196
|
+
declToId.set(node, id);
|
|
197
|
+
if (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node)) {
|
|
198
|
+
for (const member of node.members) {
|
|
199
|
+
this.visit(member, sf, rel, id, qualifiedName, nodes, edges, declToId);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// A namespace/module is a container: recurse into its body so members nest
|
|
203
|
+
// (`Geometry.area`) and don't collide with same-named top-level symbols. The
|
|
204
|
+
// body is a block of statements, or a nested namespace for `namespace A.B`. (ama-hft.13)
|
|
205
|
+
if (ts.isModuleDeclaration(node) && node.body) {
|
|
206
|
+
if (ts.isModuleBlock(node.body)) {
|
|
207
|
+
for (const stmt of node.body.statements) {
|
|
208
|
+
this.visit(stmt, sf, rel, id, qualifiedName, nodes, edges, declToId);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
else if (ts.isModuleDeclaration(node.body)) {
|
|
212
|
+
this.visit(node.body, sf, rel, id, qualifiedName, nodes, edges, declToId);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// A constructor's parameter properties (`constructor(private readonly x: T)`)
|
|
216
|
+
// are real class members, but they're declared on the parameter rather than in
|
|
217
|
+
// the class body — emit a Property for each, under the class (`containerId`),
|
|
218
|
+
// so it's queryable like a declared field. The declToId entry lets references
|
|
219
|
+
// and the parameter's type annotation resolve to it. (ama-259)
|
|
220
|
+
if (ts.isConstructorDeclaration(node)) {
|
|
221
|
+
for (const param of node.parameters) {
|
|
222
|
+
if (!ts.isIdentifier(param.name) || !isParameterProperty(param))
|
|
223
|
+
continue;
|
|
224
|
+
const propName = param.name.text;
|
|
225
|
+
const propQn = prefix ? `${prefix}.${propName}` : propName;
|
|
226
|
+
const propId = symbolId({ file: rel, qualifiedName: propQn });
|
|
227
|
+
nodes.push({
|
|
228
|
+
id: propId,
|
|
229
|
+
kind: "Property",
|
|
230
|
+
name: propName,
|
|
231
|
+
file: rel,
|
|
232
|
+
qualifiedName: propQn,
|
|
233
|
+
tier: "deep",
|
|
234
|
+
range: rangeOf(param, sf),
|
|
235
|
+
});
|
|
236
|
+
edges.push({ from: containerId, to: propId, kind: "Defines" });
|
|
237
|
+
declToId.set(param, propId);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/** Walk a subtree, attributing each call to the nearest enclosing symbol — or,
|
|
242
|
+
* for a call at true module scope, to the File node so module-level wiring is
|
|
243
|
+
* queryable. `atFileScope` is true until the walk enters any function body.
|
|
244
|
+
* (ama-53q) */
|
|
245
|
+
collectCalls(node, enclosingId, declToId, checker, edges, root, counts, fileNodeId, atFileScope) {
|
|
246
|
+
node.forEachChild((child) => {
|
|
247
|
+
// Decorators are usage edges, not calls (collectTypeUsages emits a UsesType
|
|
248
|
+
// edge for each). Skip the whole decorator so `@log()` doesn't masquerade as
|
|
249
|
+
// the decorated symbol calling `log`, and so calls inside decorator arguments
|
|
250
|
+
// (decorator config, not the symbol's behaviour) aren't attributed to it.
|
|
251
|
+
if (ts.isDecorator(child))
|
|
252
|
+
return;
|
|
253
|
+
// The call's owner: the nearest enclosing symbol, or the File when the call
|
|
254
|
+
// sits at true module scope. A call inside a transparent (unregistered)
|
|
255
|
+
// callback has neither — it's dropped, so a top-level `describe(() => it(…))`
|
|
256
|
+
// doesn't make the file appear to call `it`/`expect`. (ama-53q)
|
|
257
|
+
const from = enclosingId ?? (atFileScope ? fileNodeId : undefined);
|
|
258
|
+
// A `new Foo()` is a construction call site, resolved the same way as a
|
|
259
|
+
// plain call (to Foo's class node), so `find_callers` sees constructions.
|
|
260
|
+
if ((ts.isCallExpression(child) || ts.isNewExpression(child)) && from) {
|
|
261
|
+
// A call site that can be attributed (has an owner) — count it, and whether
|
|
262
|
+
// it resolved, for the coverage metric. (ama-m8k.12)
|
|
263
|
+
counts.callsTotal++;
|
|
264
|
+
const callee = resolveCallee(child, checker, declToId, root);
|
|
265
|
+
if (callee) {
|
|
266
|
+
counts.callsResolved++;
|
|
267
|
+
// `new X()` is a construction — a distinct Instantiates edge, not Calls.
|
|
268
|
+
const kind = ts.isNewExpression(child) ? "Instantiates" : "Calls";
|
|
269
|
+
edges.push({ from, to: callee, kind, at: locationOf(child) });
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
// Unresolved — record what it called (by root) so coverage is explainable. (ama-qbn)
|
|
273
|
+
const targetRoot = calleeRoot(child);
|
|
274
|
+
if (targetRoot)
|
|
275
|
+
counts.unresolved[targetRoot] = (counts.unresolved[targetRoot] ?? 0) + 1;
|
|
276
|
+
// An unresolved higher-order call (arr.map(fn), p.then(handler)) invokes
|
|
277
|
+
// its function argument; attribute a heuristic Calls edge to each named
|
|
278
|
+
// callback, since that control flow is otherwise invisible. (ama-hft.15)
|
|
279
|
+
if (ts.isCallExpression(child) &&
|
|
280
|
+
ts.isPropertyAccessExpression(child.expression) &&
|
|
281
|
+
HIGHER_ORDER_METHODS.has(child.expression.name.text)) {
|
|
282
|
+
for (const arg of child.arguments) {
|
|
283
|
+
if (!ts.isIdentifier(arg) && !ts.isPropertyAccessExpression(arg))
|
|
284
|
+
continue;
|
|
285
|
+
const cb = resolveValueRef(arg, checker, declToId, root);
|
|
286
|
+
if (cb && cb !== from) {
|
|
287
|
+
edges.push({
|
|
288
|
+
from,
|
|
289
|
+
to: cb,
|
|
290
|
+
kind: "Calls",
|
|
291
|
+
provenance: "heuristic",
|
|
292
|
+
at: locationOf(arg),
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const childId = declToId.get(child);
|
|
300
|
+
// A function-valued `const` is a node (ama-4s2); descending into it makes
|
|
301
|
+
// it the enclosing symbol, so calls in its body attribute to the const.
|
|
302
|
+
// A function-valued object-literal property (ama-zkr) is a node too, so its
|
|
303
|
+
// body's calls attribute to the property rather than leaking to the file.
|
|
304
|
+
// An arrow/function-expression is only enclosing when something registered
|
|
305
|
+
// it in declToId (ama-gpe: inline route handlers) — so ordinary callbacks
|
|
306
|
+
// (.map, .then) stay transparent.
|
|
307
|
+
const nextEnclosing = childId &&
|
|
308
|
+
(ts.isFunctionDeclaration(child) ||
|
|
309
|
+
ts.isMethodDeclaration(child) ||
|
|
310
|
+
ts.isConstructorDeclaration(child) ||
|
|
311
|
+
ts.isVariableDeclaration(child) ||
|
|
312
|
+
ts.isPropertyAssignment(child) ||
|
|
313
|
+
ts.isArrowFunction(child) ||
|
|
314
|
+
ts.isFunctionExpression(child))
|
|
315
|
+
? childId
|
|
316
|
+
: enclosingId;
|
|
317
|
+
// Once the walk enters any function/arrow/accessor body, calls deeper in this
|
|
318
|
+
// subtree are no longer at module scope, so the File fallback stops applying —
|
|
319
|
+
// a call in an unregistered callback there is dropped, not attributed to the
|
|
320
|
+
// file. (ama-53q)
|
|
321
|
+
const childAtFileScope = atFileScope && !ts.isFunctionLike(child);
|
|
322
|
+
this.collectCalls(child, nextEnclosing, declToId, checker, edges, root, counts, fileNodeId, childAtFileScope);
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Emit a `References` edge from the enclosing symbol to a module-level Variable
|
|
327
|
+
* node (ama-hft.12) each time its value is read — so `find_callers("MAX_RETRIES")`
|
|
328
|
+
* answers "who reads this constant". Mirrors `collectCalls`' enclosing-tracking.
|
|
329
|
+
*
|
|
330
|
+
* `resolveModuleVarRef` restricts targets to top-level const/let/var
|
|
331
|
+
* declarations (by *decl kind*, not a batch-local id set), so reads of
|
|
332
|
+
* functions/classes don't become References — and a cross-file const still
|
|
333
|
+
* resolves in a single-file reindex, where the batch holds no other file's
|
|
334
|
+
* Variable nodes (ama-l6k). Most false positives filter themselves out: a
|
|
335
|
+
* property-access member name and an import specifier resolve away, and a
|
|
336
|
+
* declaration's own name is caught by the `to !== enclosingId` guard. (ama-6k0)
|
|
337
|
+
*/
|
|
338
|
+
collectVarReferences(node, enclosingId, declToId, checker, edges, root) {
|
|
339
|
+
node.forEachChild((child) => {
|
|
340
|
+
if (ts.isIdentifier(child) && enclosingId) {
|
|
341
|
+
const parent = child.parent;
|
|
342
|
+
if (ts.isPropertyAccessExpression(parent) && parent.name === child) {
|
|
343
|
+
// The member side of `X.prop`. A method *call* (`X.m()`) is already a
|
|
344
|
+
// Calls edge, but a non-call read — a field, a parameter property, or a
|
|
345
|
+
// method used as a value — is a References to that member, so
|
|
346
|
+
// find_referrers on a property shows where it's used. Covers both
|
|
347
|
+
// `this.<prop>` (ama-qo3) and cross-instance `obj.<prop>` reads (ama-emb):
|
|
348
|
+
// resolveValueRef → nodeIdForDecl already filters targets to in-project
|
|
349
|
+
// top-level + class/interface members, so external reads (`console.log`)
|
|
350
|
+
// and locals add no edge — only the `X.m()` call form is excluded here.
|
|
351
|
+
if (!(ts.isCallExpression(parent.parent) && parent.parent.expression === parent)) {
|
|
352
|
+
const to = resolveValueRef(child, checker, declToId, root);
|
|
353
|
+
if (to && to !== enclosingId) {
|
|
354
|
+
edges.push({ from: enclosingId, to, kind: "References" });
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
const to = resolveModuleVarRef(child, checker, declToId, root);
|
|
360
|
+
if (to && to !== enclosingId) {
|
|
361
|
+
edges.push({ from: enclosingId, to, kind: "References" });
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
else if (ts.isObjectBindingPattern(child) && enclosingId) {
|
|
366
|
+
// Destructuring (`const { a, b: c } = obj`, `({ a }: T) =>`) reads each element's
|
|
367
|
+
// property of the pattern's type — track it like a `.prop` read so find_referrers
|
|
368
|
+
// sees destructuring sites, not just `obj.prop`. A renamed `{ a: b }` reads `a`;
|
|
369
|
+
// the same nodeIdForDecl filtering keeps external/local targets out. (ama-eda)
|
|
370
|
+
const type = checker.getTypeAtLocation(child);
|
|
371
|
+
for (const el of child.elements) {
|
|
372
|
+
const prop = el.propertyName ?? el.name;
|
|
373
|
+
if (!ts.isIdentifier(prop))
|
|
374
|
+
continue; // skip computed keys / nested patterns
|
|
375
|
+
const to = resolveTypeMember(type, prop.text, declToId, root);
|
|
376
|
+
if (to && to !== enclosingId)
|
|
377
|
+
edges.push({ from: enclosingId, to, kind: "References" });
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
const childId = declToId.get(child);
|
|
381
|
+
const nextEnclosing = childId &&
|
|
382
|
+
(ts.isFunctionDeclaration(child) ||
|
|
383
|
+
ts.isMethodDeclaration(child) ||
|
|
384
|
+
ts.isConstructorDeclaration(child) ||
|
|
385
|
+
ts.isVariableDeclaration(child) ||
|
|
386
|
+
ts.isPropertyAssignment(child) ||
|
|
387
|
+
ts.isArrowFunction(child) ||
|
|
388
|
+
ts.isFunctionExpression(child))
|
|
389
|
+
? childId
|
|
390
|
+
: enclosingId;
|
|
391
|
+
this.collectVarReferences(child, nextEnclosing, declToId, checker, edges, root);
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
/** Walk a subtree emitting an `Implements` edge for each `class … implements I`. */
|
|
395
|
+
collectHeritage(node, declToId, checker, edges, root) {
|
|
396
|
+
if (ts.isClassDeclaration(node)) {
|
|
397
|
+
const from = declToId.get(node);
|
|
398
|
+
for (const clause of node.heritageClauses ?? []) {
|
|
399
|
+
// On a class, `extends` is inheritance; `implements` is interface conformance.
|
|
400
|
+
const kind = clause.token === ts.SyntaxKind.ExtendsKeyword ? "Inherits" : "Implements";
|
|
401
|
+
for (const type of clause.types) {
|
|
402
|
+
const to = from && resolveHeritage(type.expression, checker, declToId, root);
|
|
403
|
+
if (from && to)
|
|
404
|
+
edges.push({ from, to, kind });
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
node.forEachChild((child) => this.collectHeritage(child, declToId, checker, edges, root));
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Emit an `Imports` edge from a file to each symbol it imports, and from a
|
|
412
|
+
* re-exporting file (`export { x } from "./m.js"`) to the re-exported symbol.
|
|
413
|
+
* Import/re-export bindings are aliases, so the edge target is the symbol's
|
|
414
|
+
* original declaration — even through a chain of barrels.
|
|
415
|
+
*/
|
|
416
|
+
collectImports(sf, fromId, declToId, checker, edges, root) {
|
|
417
|
+
// Type-only imports/exports (`import type`, `import { type X }`, `export
|
|
418
|
+
// type`) are erased at runtime, so they get an ImportsType edge — counted for
|
|
419
|
+
// dependents/affected but excluded from runtime analyses (circular_imports).
|
|
420
|
+
const link = (name, typeOnly) => {
|
|
421
|
+
const to = resolveImport(name, checker, declToId, root);
|
|
422
|
+
if (to)
|
|
423
|
+
edges.push({ from: fromId, to, kind: typeOnly ? "ImportsType" : "Imports" });
|
|
424
|
+
};
|
|
425
|
+
// Imports can only appear as top-level statements in an ES module.
|
|
426
|
+
for (const stmt of sf.statements) {
|
|
427
|
+
if (ts.isImportDeclaration(stmt) && stmt.importClause) {
|
|
428
|
+
const clause = stmt.importClause;
|
|
429
|
+
if (clause.name)
|
|
430
|
+
link(clause.name, clause.isTypeOnly); // default import
|
|
431
|
+
const { namedBindings } = clause;
|
|
432
|
+
if (namedBindings) {
|
|
433
|
+
if (ts.isNamedImports(namedBindings)) {
|
|
434
|
+
// `import type {…}` makes the whole clause type-only; `import { type X }`
|
|
435
|
+
// marks a single specifier.
|
|
436
|
+
for (const spec of namedBindings.elements) {
|
|
437
|
+
link(spec.name, clause.isTypeOnly || spec.isTypeOnly);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
link(namedBindings.name, clause.isTypeOnly); // `import * as ns`
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
else if (ts.isExportDeclaration(stmt) &&
|
|
446
|
+
stmt.moduleSpecifier // `export { x }` without a source is a local export, not a re-export
|
|
447
|
+
) {
|
|
448
|
+
const { exportClause } = stmt;
|
|
449
|
+
if (!exportClause) {
|
|
450
|
+
link(stmt.moduleSpecifier, stmt.isTypeOnly); // `export * from`
|
|
451
|
+
}
|
|
452
|
+
else if (ts.isNamedExports(exportClause)) {
|
|
453
|
+
for (const spec of exportClause.elements) {
|
|
454
|
+
link(spec.name, stmt.isTypeOnly || spec.isTypeOnly);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
link(exportClause.name, stmt.isTypeOnly); // `export * as ns from`
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Emit a `UsesType` edge for each named type referenced in a parameter, a
|
|
465
|
+
* function/method return type, a property type, or a generic instantiation's
|
|
466
|
+
* type arguments (`f<Widget>()`, `new Box<Widget>()`, `extends Base<Widget>`),
|
|
467
|
+
* attributed to the nearest enclosing emitted symbol. Composite annotations are
|
|
468
|
+
* walked, so `Widget[]` or `Map<K, Widget>` still link to `Widget`. Types
|
|
469
|
+
* outside the analyzed set (`number`, library types) resolve to no node and are
|
|
470
|
+
* skipped.
|
|
471
|
+
*/
|
|
472
|
+
collectTypeUsages(node, enclosingId, declToId, checker, edges, root) {
|
|
473
|
+
const annotations = [];
|
|
474
|
+
const returnAnnotations = []; // → Returns, kept distinct (ama-37c)
|
|
475
|
+
if (ts.isParameter(node) ||
|
|
476
|
+
ts.isPropertyDeclaration(node) ||
|
|
477
|
+
ts.isPropertySignature(node) ||
|
|
478
|
+
ts.isVariableDeclaration(node)) {
|
|
479
|
+
if (node.type)
|
|
480
|
+
annotations.push(node.type);
|
|
481
|
+
}
|
|
482
|
+
else if (ts.isFunctionDeclaration(node) ||
|
|
483
|
+
ts.isMethodDeclaration(node) ||
|
|
484
|
+
ts.isMethodSignature(node) ||
|
|
485
|
+
ts.isGetAccessorDeclaration(node)) {
|
|
486
|
+
if (node.type)
|
|
487
|
+
returnAnnotations.push(node.type); // return type → Returns
|
|
488
|
+
}
|
|
489
|
+
else if (ts.isCallExpression(node) ||
|
|
490
|
+
ts.isNewExpression(node) ||
|
|
491
|
+
ts.isExpressionWithTypeArguments(node)) {
|
|
492
|
+
// Generic instantiation: `f<Widget>()`, `new Box<Widget>()`, `extends Base<Widget>`.
|
|
493
|
+
if (node.typeArguments)
|
|
494
|
+
annotations.push(...node.typeArguments);
|
|
495
|
+
}
|
|
496
|
+
if (enclosingId) {
|
|
497
|
+
for (const annotation of annotations) {
|
|
498
|
+
for (const ref of typeReferencesIn(annotation)) {
|
|
499
|
+
const to = resolveTypeRef(ref.typeName, checker, declToId, root);
|
|
500
|
+
// A type used inside its own declaration's signature is noise, not a usage.
|
|
501
|
+
if (to && to !== enclosingId)
|
|
502
|
+
edges.push({ from: enclosingId, to, kind: "UsesType" });
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
for (const annotation of returnAnnotations) {
|
|
506
|
+
for (const ref of typeReferencesIn(annotation)) {
|
|
507
|
+
const to = resolveTypeRef(ref.typeName, checker, declToId, root);
|
|
508
|
+
if (to && to !== enclosingId)
|
|
509
|
+
edges.push({ from: enclosingId, to, kind: "Returns" });
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
// A decorator is a metadata/annotation dependency of the decorated symbol —
|
|
513
|
+
// modelled as UsesType (decorated → decorator), uniformly for call-form
|
|
514
|
+
// (`@log()`) and bare (`@sealed`) decorators. So `find_type_users(Component)`
|
|
515
|
+
// answers "what is decorated by @Component?".
|
|
516
|
+
if (ts.canHaveDecorators(node)) {
|
|
517
|
+
for (const decorator of ts.getDecorators(node) ?? []) {
|
|
518
|
+
const ref = ts.isCallExpression(decorator.expression)
|
|
519
|
+
? decorator.expression.expression
|
|
520
|
+
: decorator.expression;
|
|
521
|
+
const to = resolveValueRef(ref, checker, declToId, root);
|
|
522
|
+
if (to && to !== enclosingId)
|
|
523
|
+
edges.push({ from: enclosingId, to, kind: "UsesType" });
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
node.forEachChild((child) => {
|
|
528
|
+
const childId = declToId.get(child);
|
|
529
|
+
this.collectTypeUsages(child, childId ?? enclosingId, declToId, checker, edges, root);
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Detect framework routes (Express/NestJS-style call APIs: `app.get("/x", h)`,
|
|
534
|
+
* `router.post(...)`) and emit a Route node per route, plus a References edge to
|
|
535
|
+
* each named handler. Deliberately scoped to avoid false positives: an HTTP-verb
|
|
536
|
+
* method call whose first arg is a "/"-prefixed string literal and which has at
|
|
537
|
+
* least one handler arg (so `map.get("k")` / `headers.get("x")` don't match).
|
|
538
|
+
* Inline arrow/function handlers get a Route node but no edge yet — naming an
|
|
539
|
+
* anonymous handler is the arg-position-handler follow-up (ama-y9q).
|
|
540
|
+
*/
|
|
541
|
+
collectRoutes(sf, rel, declToId, checker, nodes, edges, root, mountPrefixes) {
|
|
542
|
+
const visit = (n) => {
|
|
543
|
+
if (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression)) {
|
|
544
|
+
const method = n.expression.name.text.toLowerCase();
|
|
545
|
+
const [first, ...rest] = n.arguments;
|
|
546
|
+
const handlers = rest.filter((a) => ts.isArrowFunction(a) ||
|
|
547
|
+
ts.isFunctionExpression(a) ||
|
|
548
|
+
ts.isIdentifier(a) ||
|
|
549
|
+
ts.isPropertyAccessExpression(a));
|
|
550
|
+
if (ROUTE_METHODS.has(method) &&
|
|
551
|
+
first !== undefined &&
|
|
552
|
+
ts.isStringLiteralLike(first) &&
|
|
553
|
+
first.text.startsWith("/") &&
|
|
554
|
+
handlers.length > 0) {
|
|
555
|
+
// If this route's receiver is a router mounted under a prefix, prepend it.
|
|
556
|
+
const receiverDecl = valueDeclOf(n.expression.expression, checker);
|
|
557
|
+
const prefix = receiverDecl ? mountPrefixes.get(receiverDecl) : undefined;
|
|
558
|
+
const name = `${method.toUpperCase()} ${joinRoutePath(prefix, first.text)}`;
|
|
559
|
+
const routeId = symbolId({ file: rel, qualifiedName: name });
|
|
560
|
+
nodes.push({
|
|
561
|
+
id: routeId,
|
|
562
|
+
kind: "Route",
|
|
563
|
+
name,
|
|
564
|
+
file: rel,
|
|
565
|
+
qualifiedName: name,
|
|
566
|
+
tier: "deep",
|
|
567
|
+
range: rangeOf(n, sf),
|
|
568
|
+
});
|
|
569
|
+
this.emitRouteHandlers(routeId, name, handlers, rel, sf, declToId, checker, nodes, edges, root);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
// Object-config routes: Hapi `server.route({ method, path, handler })` and
|
|
573
|
+
// Fastify `fastify.route({ method, url, handler })` (also `.route([{…}])`).
|
|
574
|
+
// The method-named path above already covers the `app.get(path, h)` style
|
|
575
|
+
// that Fastify/Koa/Hono share with Express. (ama-rme.10)
|
|
576
|
+
if (ts.isCallExpression(n) &&
|
|
577
|
+
ts.isPropertyAccessExpression(n.expression) &&
|
|
578
|
+
n.expression.name.text === "route") {
|
|
579
|
+
const arg = n.arguments[0];
|
|
580
|
+
const configs = arg ? (ts.isArrayLiteralExpression(arg) ? arg.elements : [arg]) : [];
|
|
581
|
+
for (const config of configs) {
|
|
582
|
+
if (!ts.isObjectLiteralExpression(config))
|
|
583
|
+
continue;
|
|
584
|
+
const methods = routeMethods(objectProp(config, "method"));
|
|
585
|
+
const pathExpr = objectProp(config, "path") ?? objectProp(config, "url");
|
|
586
|
+
const handler = objectProp(config, "handler");
|
|
587
|
+
if (methods.length === 0 || !pathExpr || !ts.isStringLiteralLike(pathExpr) || !handler) {
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
if (!pathExpr.text.startsWith("/"))
|
|
591
|
+
continue;
|
|
592
|
+
for (const method of methods) {
|
|
593
|
+
const name = `${method.toUpperCase()} ${pathExpr.text}`;
|
|
594
|
+
const routeId = symbolId({ file: rel, qualifiedName: name });
|
|
595
|
+
nodes.push({
|
|
596
|
+
id: routeId,
|
|
597
|
+
kind: "Route",
|
|
598
|
+
name,
|
|
599
|
+
file: rel,
|
|
600
|
+
qualifiedName: name,
|
|
601
|
+
tier: "deep",
|
|
602
|
+
range: rangeOf(config, sf),
|
|
603
|
+
});
|
|
604
|
+
this.emitRouteHandlers(routeId, name, [handler], rel, sf, declToId, checker, nodes, edges, root);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
// tRPC: a router property `name: <chain>.query/mutation/subscription(handler)`.
|
|
609
|
+
// The property key is the procedure name; the call's first arg is the
|
|
610
|
+
// handler. (ama-rme.11)
|
|
611
|
+
if (ts.isPropertyAssignment(n) &&
|
|
612
|
+
ts.isIdentifier(n.name) &&
|
|
613
|
+
ts.isCallExpression(n.initializer) &&
|
|
614
|
+
ts.isPropertyAccessExpression(n.initializer.expression) &&
|
|
615
|
+
PROCEDURE_TYPES.has(n.initializer.expression.name.text)) {
|
|
616
|
+
const handler = n.initializer.arguments[0];
|
|
617
|
+
if (handler && isHandlerExpr(handler)) {
|
|
618
|
+
const name = `${n.initializer.expression.name.text} ${n.name.text}`;
|
|
619
|
+
const routeId = symbolId({ file: rel, qualifiedName: name });
|
|
620
|
+
nodes.push({
|
|
621
|
+
id: routeId,
|
|
622
|
+
kind: "Route",
|
|
623
|
+
name,
|
|
624
|
+
file: rel,
|
|
625
|
+
qualifiedName: name,
|
|
626
|
+
tier: "deep",
|
|
627
|
+
range: rangeOf(n, sf),
|
|
628
|
+
});
|
|
629
|
+
this.emitRouteHandlers(routeId, name, [handler], rel, sf, declToId, checker, nodes, edges, root);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
// GraphQL: a resolver map `{ Query: { field: resolver }, Mutation: {…} }` —
|
|
633
|
+
// each field under a Query/Mutation/Subscription root is a `Type.field`
|
|
634
|
+
// route referencing its resolver. (ama-rme.11)
|
|
635
|
+
if (ts.isObjectLiteralExpression(n)) {
|
|
636
|
+
for (const typeProp of n.properties) {
|
|
637
|
+
if (!ts.isPropertyAssignment(typeProp) ||
|
|
638
|
+
!ts.isIdentifier(typeProp.name) ||
|
|
639
|
+
!GRAPHQL_ROOTS.has(typeProp.name.text) ||
|
|
640
|
+
!ts.isObjectLiteralExpression(typeProp.initializer)) {
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
for (const field of typeProp.initializer.properties) {
|
|
644
|
+
if (!ts.isPropertyAssignment(field) ||
|
|
645
|
+
!ts.isIdentifier(field.name) ||
|
|
646
|
+
!isHandlerExpr(field.initializer)) {
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
const name = `${typeProp.name.text}.${field.name.text}`;
|
|
650
|
+
const routeId = symbolId({ file: rel, qualifiedName: name });
|
|
651
|
+
nodes.push({
|
|
652
|
+
id: routeId,
|
|
653
|
+
kind: "Route",
|
|
654
|
+
name,
|
|
655
|
+
file: rel,
|
|
656
|
+
qualifiedName: name,
|
|
657
|
+
tier: "deep",
|
|
658
|
+
range: rangeOf(field, sf),
|
|
659
|
+
});
|
|
660
|
+
this.emitRouteHandlers(routeId, name, [field.initializer], rel, sf, declToId, checker, nodes, edges, root);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
// NestJS: @Controller("prefix") class whose methods carry @Get/@Post/...
|
|
665
|
+
// decorators. The decorated method IS the handler (already a Method node);
|
|
666
|
+
// the route path is the controller prefix joined with the method's path.
|
|
667
|
+
if (ts.isClassDeclaration(n) && ts.canHaveDecorators(n)) {
|
|
668
|
+
const controller = (ts.getDecorators(n) ?? [])
|
|
669
|
+
.map(decoratorInfo)
|
|
670
|
+
.find((d) => d.name === "Controller");
|
|
671
|
+
if (controller) {
|
|
672
|
+
for (const member of n.members) {
|
|
673
|
+
if (!ts.isMethodDeclaration(member) || !ts.canHaveDecorators(member))
|
|
674
|
+
continue;
|
|
675
|
+
const handlerId = declToId.get(member);
|
|
676
|
+
if (handlerId === undefined)
|
|
677
|
+
continue;
|
|
678
|
+
for (const dec of ts.getDecorators(member) ?? []) {
|
|
679
|
+
const info = decoratorInfo(dec);
|
|
680
|
+
if (!ROUTE_METHODS.has(info.name.toLowerCase()))
|
|
681
|
+
continue;
|
|
682
|
+
const name = `${info.name.toUpperCase()} ${joinRoutePath(controller.arg, info.arg)}`;
|
|
683
|
+
const routeId = symbolId({ file: rel, qualifiedName: name });
|
|
684
|
+
nodes.push({
|
|
685
|
+
id: routeId,
|
|
686
|
+
kind: "Route",
|
|
687
|
+
name,
|
|
688
|
+
file: rel,
|
|
689
|
+
qualifiedName: name,
|
|
690
|
+
tier: "deep",
|
|
691
|
+
range: rangeOf(member, sf),
|
|
692
|
+
});
|
|
693
|
+
edges.push({
|
|
694
|
+
from: routeId,
|
|
695
|
+
to: handlerId,
|
|
696
|
+
kind: "References",
|
|
697
|
+
provenance: "heuristic",
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
n.forEachChild(visit);
|
|
704
|
+
};
|
|
705
|
+
visit(sf);
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Wire a route to its handler argument(s): an inline arrow/function becomes a
|
|
709
|
+
* synthesized handler Function node (named by the route, registered so its body
|
|
710
|
+
* attributes to it), while a named handler reference resolves to its node. Each
|
|
711
|
+
* gets a heuristic References edge from the route. Shared by every route style.
|
|
712
|
+
* (ama-rme.1, ama-rme.10)
|
|
713
|
+
*/
|
|
714
|
+
emitRouteHandlers(routeId, name, handlers, rel, sf, declToId, checker, nodes, edges, root) {
|
|
715
|
+
let inlineCount = 0;
|
|
716
|
+
for (const handler of handlers) {
|
|
717
|
+
if (ts.isArrowFunction(handler) || ts.isFunctionExpression(handler)) {
|
|
718
|
+
const handlerName = `${name} handler${inlineCount === 0 ? "" : ` ${inlineCount + 1}`}`;
|
|
719
|
+
inlineCount++;
|
|
720
|
+
const handlerId = symbolId({ file: rel, qualifiedName: handlerName });
|
|
721
|
+
nodes.push({
|
|
722
|
+
id: handlerId,
|
|
723
|
+
kind: "Function",
|
|
724
|
+
name: handlerName,
|
|
725
|
+
file: rel,
|
|
726
|
+
qualifiedName: handlerName,
|
|
727
|
+
tier: "deep",
|
|
728
|
+
range: rangeOf(handler, sf),
|
|
729
|
+
});
|
|
730
|
+
declToId.set(handler, handlerId);
|
|
731
|
+
edges.push({ from: routeId, to: handlerId, kind: "References", provenance: "heuristic" });
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
const to = resolveValueRef(handler, checker, declToId, root);
|
|
735
|
+
if (to && to !== routeId) {
|
|
736
|
+
edges.push({ from: routeId, to, kind: "References", provenance: "heuristic" });
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Synthesize a Function node for an inline arrow/function-expression passed as an
|
|
742
|
+
* argument to a *string-named* call whose result is itself consumed —
|
|
743
|
+
* `register("work", wrap("work", () => …))`. The leading string literal names the
|
|
744
|
+
* node (`"work handler"`); registering the arrow in `declToId` before
|
|
745
|
+
* `collectCalls` makes the callback body's calls attribute to it instead of
|
|
746
|
+
* leaking to the enclosing function (so per-handler blast radius is precise).
|
|
747
|
+
*
|
|
748
|
+
* Runs after `collectRoutes`, so a route's inline handler — already registered —
|
|
749
|
+
* is skipped. The "result is consumed" gate (the call is not a bare expression
|
|
750
|
+
* statement) is what separates a handler-producing wrapper like `tap(name, fn)`
|
|
751
|
+
* from a fire-and-forget test block like `it(name, fn)` / `describe(name, fn)`:
|
|
752
|
+
* only the former becomes a node, so the graph isn't flooded with one node per
|
|
753
|
+
* test case. (ama-y9q)
|
|
754
|
+
*
|
|
755
|
+
* The handler need not be a *direct* argument: it may be nested inside a second
|
|
756
|
+
* wrapper whose own first argument is not a string — `tap("search",
|
|
757
|
+
* queryTool(session, () => …))`. `collectHandlerArrows` digs through such
|
|
758
|
+
* wrapper-call arguments (stopping at the first function, so a handler's body is
|
|
759
|
+
* never mistaken for another handler), keying every one by the outer name. (ama-63x)
|
|
760
|
+
*/
|
|
761
|
+
collectCallbackHandlers(node, enclosingId, rel, sf, declToId, nodes, edges) {
|
|
762
|
+
node.forEachChild((child) => {
|
|
763
|
+
const first = ts.isCallExpression(child) ? child.arguments[0] : undefined;
|
|
764
|
+
if (ts.isCallExpression(child) &&
|
|
765
|
+
!ts.isExpressionStatement(child.parent) &&
|
|
766
|
+
first !== undefined &&
|
|
767
|
+
ts.isStringLiteralLike(first)) {
|
|
768
|
+
let inlineCount = 0;
|
|
769
|
+
for (const arg of collectHandlerArrows(child.arguments)) {
|
|
770
|
+
if (declToId.has(arg))
|
|
771
|
+
continue; // already a node (e.g. a route handler)
|
|
772
|
+
const handlerName = `${first.text} handler${inlineCount === 0 ? "" : ` ${inlineCount + 1}`}`;
|
|
773
|
+
inlineCount++;
|
|
774
|
+
const handlerId = symbolId({ file: rel, qualifiedName: handlerName });
|
|
775
|
+
nodes.push({
|
|
776
|
+
id: handlerId,
|
|
777
|
+
kind: "Function",
|
|
778
|
+
name: handlerName,
|
|
779
|
+
file: rel,
|
|
780
|
+
qualifiedName: handlerName,
|
|
781
|
+
tier: "deep",
|
|
782
|
+
range: rangeOf(arg, sf),
|
|
783
|
+
});
|
|
784
|
+
declToId.set(arg, handlerId);
|
|
785
|
+
if (enclosingId) {
|
|
786
|
+
edges.push({
|
|
787
|
+
from: enclosingId,
|
|
788
|
+
to: handlerId,
|
|
789
|
+
kind: "References",
|
|
790
|
+
provenance: "heuristic",
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
const childId = declToId.get(child);
|
|
796
|
+
// Mirror collectCalls' enclosing rule so a synthesized handler nested inside
|
|
797
|
+
// another becomes the `from` of the inner one's reference edge.
|
|
798
|
+
const nextEnclosing = childId &&
|
|
799
|
+
(ts.isFunctionDeclaration(child) ||
|
|
800
|
+
ts.isMethodDeclaration(child) ||
|
|
801
|
+
ts.isConstructorDeclaration(child) ||
|
|
802
|
+
ts.isVariableDeclaration(child) ||
|
|
803
|
+
ts.isPropertyAssignment(child) ||
|
|
804
|
+
ts.isArrowFunction(child) ||
|
|
805
|
+
ts.isFunctionExpression(child))
|
|
806
|
+
? childId
|
|
807
|
+
: enclosingId;
|
|
808
|
+
this.collectCallbackHandlers(child, nextEnclosing, rel, sf, declToId, nodes, edges);
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* File-based routing: a route file at a framework convention path exports HTTP
|
|
813
|
+
* method handlers and the URL comes from the *path* (not a call). Next.js App
|
|
814
|
+
* Router (`app/**/route.ts`) and SvelteKit (`src/routes/**/+server.ts`) — each
|
|
815
|
+
* exported `GET`/`POST`/… function becomes a `<METHOD> <path>` Route referencing
|
|
816
|
+
* it. Heuristic: the route is inferred from filesystem convention. (ama-rme.7)
|
|
817
|
+
*/
|
|
818
|
+
collectFileRoutes(sf, rel, declToId, checker, nodes, edges, root) {
|
|
819
|
+
// Marker-file conventions (route.ts/+server.ts → directory path, ama-rme.7)
|
|
820
|
+
// and filename conventions (pages//server/ → filename path, ama-w7g).
|
|
821
|
+
const marker = fileRoutePath(rel);
|
|
822
|
+
const named = filenameRoutePath(rel);
|
|
823
|
+
const routePath = marker ?? named?.path;
|
|
824
|
+
if (routePath === undefined)
|
|
825
|
+
return;
|
|
826
|
+
const emit = (methodName, decl) => {
|
|
827
|
+
if (!ROUTE_METHODS.has(methodName.toLowerCase()))
|
|
828
|
+
return;
|
|
829
|
+
const handlerId = declToId.get(decl);
|
|
830
|
+
if (!handlerId)
|
|
831
|
+
return;
|
|
832
|
+
const name = `${methodName} ${routePath}`;
|
|
833
|
+
const routeId = symbolId({ file: rel, qualifiedName: name });
|
|
834
|
+
nodes.push({
|
|
835
|
+
id: routeId,
|
|
836
|
+
kind: "Route",
|
|
837
|
+
name,
|
|
838
|
+
file: rel,
|
|
839
|
+
qualifiedName: name,
|
|
840
|
+
tier: "deep",
|
|
841
|
+
range: rangeOf(decl, sf),
|
|
842
|
+
});
|
|
843
|
+
edges.push({ from: routeId, to: handlerId, kind: "References", provenance: "heuristic" });
|
|
844
|
+
};
|
|
845
|
+
for (const stmt of sf.statements) {
|
|
846
|
+
if (!isExported(stmt))
|
|
847
|
+
continue;
|
|
848
|
+
if (ts.isFunctionDeclaration(stmt) && stmt.name) {
|
|
849
|
+
emit(stmt.name.text, stmt);
|
|
850
|
+
}
|
|
851
|
+
else if (ts.isVariableStatement(stmt)) {
|
|
852
|
+
for (const d of stmt.declarationList.declarations) {
|
|
853
|
+
if (ts.isIdentifier(d.name) &&
|
|
854
|
+
d.initializer &&
|
|
855
|
+
(ts.isArrowFunction(d.initializer) || ts.isFunctionExpression(d.initializer))) {
|
|
856
|
+
emit(d.name.text, d);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
// A default-export request handler (Next.js Pages Router, Nuxt) — an any-method
|
|
862
|
+
// route. Page components (a default export outside an `api` dir) are excluded
|
|
863
|
+
// by `allowDefault`. (ama-w7g)
|
|
864
|
+
if (named?.allowDefault) {
|
|
865
|
+
const handler = findDefaultExportHandler(sf);
|
|
866
|
+
if (handler) {
|
|
867
|
+
const handlerNode = "decl" in handler ? handler.decl : handler.expr;
|
|
868
|
+
const name = `ALL ${named.path}`;
|
|
869
|
+
const routeId = symbolId({ file: rel, qualifiedName: name });
|
|
870
|
+
nodes.push({
|
|
871
|
+
id: routeId,
|
|
872
|
+
kind: "Route",
|
|
873
|
+
name,
|
|
874
|
+
file: rel,
|
|
875
|
+
qualifiedName: name,
|
|
876
|
+
tier: "deep",
|
|
877
|
+
range: rangeOf(handlerNode, sf),
|
|
878
|
+
});
|
|
879
|
+
if ("decl" in handler) {
|
|
880
|
+
const handlerId = declToId.get(handler.decl);
|
|
881
|
+
if (handlerId) {
|
|
882
|
+
edges.push({
|
|
883
|
+
from: routeId,
|
|
884
|
+
to: handlerId,
|
|
885
|
+
kind: "References",
|
|
886
|
+
provenance: "heuristic",
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
else {
|
|
891
|
+
this.emitRouteHandlers(routeId, name, [handler.expr], rel, sf, declToId, checker, nodes, edges, root);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Synthesize call edges for the EventEmitter pattern: an `emitter.emit("ch")`
|
|
898
|
+
* invokes every handler registered with `.on("ch", h)` (or once/addListener)
|
|
899
|
+
* for the same channel string. Heuristic — matched by channel name, not proven
|
|
900
|
+
* dispatch — so the synthesized edges carry `provenance: "heuristic"`. Runs
|
|
901
|
+
* after collectCallbackHandlers so inline `.on("ch", () => …)` arrows (already
|
|
902
|
+
* synthesized into handler nodes there) are connectable too. (ama-hft.14)
|
|
903
|
+
*/
|
|
904
|
+
collectEvents(sf, declToId, checker, edges, root) {
|
|
905
|
+
// Pass 1: channel -> the handler node(s) registered for it.
|
|
906
|
+
const handlers = new Map();
|
|
907
|
+
const collectRegistrations = (node) => {
|
|
908
|
+
node.forEachChild((child) => {
|
|
909
|
+
if (ts.isCallExpression(child) &&
|
|
910
|
+
ts.isPropertyAccessExpression(child.expression) &&
|
|
911
|
+
ON_METHODS.has(child.expression.name.text)) {
|
|
912
|
+
const [channel, handler] = child.arguments;
|
|
913
|
+
if (channel && ts.isStringLiteralLike(channel) && handler) {
|
|
914
|
+
const handlerId = eventHandlerId(handler, declToId, checker, root);
|
|
915
|
+
if (handlerId) {
|
|
916
|
+
const list = handlers.get(channel.text) ?? [];
|
|
917
|
+
list.push(handlerId);
|
|
918
|
+
handlers.set(channel.text, list);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
collectRegistrations(child);
|
|
923
|
+
});
|
|
924
|
+
};
|
|
925
|
+
collectRegistrations(sf);
|
|
926
|
+
if (handlers.size === 0)
|
|
927
|
+
return; // nothing listens — no edges to synthesize
|
|
928
|
+
// Pass 2: each `emit("ch")` calls every handler registered for "ch".
|
|
929
|
+
const collectEmits = (node, enclosingId) => {
|
|
930
|
+
node.forEachChild((child) => {
|
|
931
|
+
if (enclosingId &&
|
|
932
|
+
ts.isCallExpression(child) &&
|
|
933
|
+
ts.isPropertyAccessExpression(child.expression) &&
|
|
934
|
+
child.expression.name.text === "emit") {
|
|
935
|
+
const channel = child.arguments[0];
|
|
936
|
+
if (channel && ts.isStringLiteralLike(channel)) {
|
|
937
|
+
for (const handlerId of handlers.get(channel.text) ?? []) {
|
|
938
|
+
if (handlerId !== enclosingId) {
|
|
939
|
+
edges.push({
|
|
940
|
+
from: enclosingId,
|
|
941
|
+
to: handlerId,
|
|
942
|
+
kind: "Calls",
|
|
943
|
+
provenance: "heuristic",
|
|
944
|
+
at: locationOf(child),
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
const childId = declToId.get(child);
|
|
951
|
+
const nextEnclosing = childId &&
|
|
952
|
+
(ts.isFunctionDeclaration(child) ||
|
|
953
|
+
ts.isMethodDeclaration(child) ||
|
|
954
|
+
ts.isConstructorDeclaration(child) ||
|
|
955
|
+
ts.isVariableDeclaration(child) ||
|
|
956
|
+
ts.isPropertyAssignment(child) ||
|
|
957
|
+
ts.isArrowFunction(child) ||
|
|
958
|
+
ts.isFunctionExpression(child))
|
|
959
|
+
? childId
|
|
960
|
+
: enclosingId;
|
|
961
|
+
collectEmits(child, nextEnclosing);
|
|
962
|
+
});
|
|
963
|
+
};
|
|
964
|
+
collectEmits(sf, undefined);
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Find Express mounts — `app.use("/prefix", router, …)` — and map each mounted
|
|
968
|
+
* argument's declaration to the prefix. Runs over every file before route
|
|
969
|
+
* detection so a router defined in one file and mounted in another composes.
|
|
970
|
+
*/
|
|
971
|
+
collectMounts(sf, checker, mountPrefixes) {
|
|
972
|
+
const visit = (n) => {
|
|
973
|
+
if (ts.isCallExpression(n) &&
|
|
974
|
+
ts.isPropertyAccessExpression(n.expression) &&
|
|
975
|
+
n.expression.name.text === "use") {
|
|
976
|
+
const [first, ...rest] = n.arguments;
|
|
977
|
+
if (first !== undefined && ts.isStringLiteralLike(first) && first.text.startsWith("/")) {
|
|
978
|
+
for (const arg of rest) {
|
|
979
|
+
if (ts.isIdentifier(arg) || ts.isPropertyAccessExpression(arg)) {
|
|
980
|
+
const decl = valueDeclOf(arg, checker);
|
|
981
|
+
// Harmless if `arg` is middleware, not a router: it just won't have routes.
|
|
982
|
+
if (decl)
|
|
983
|
+
mountPrefixes.set(decl, first.text);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
n.forEachChild(visit);
|
|
989
|
+
};
|
|
990
|
+
visit(sf);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
/** HTTP-verb methods that mark an Express/Nest-style route registration call. */
|
|
994
|
+
const ROUTE_METHODS = new Set(["get", "post", "put", "delete", "patch", "options", "head", "all"]);
|
|
995
|
+
/** A decorator's callee name and its first string-literal argument, if any. */
|
|
996
|
+
function decoratorInfo(dec) {
|
|
997
|
+
const expr = dec.expression;
|
|
998
|
+
const callee = ts.isCallExpression(expr) ? expr.expression : expr;
|
|
999
|
+
const name = ts.isIdentifier(callee)
|
|
1000
|
+
? callee.text
|
|
1001
|
+
: ts.isPropertyAccessExpression(callee)
|
|
1002
|
+
? callee.name.text
|
|
1003
|
+
: "";
|
|
1004
|
+
let arg;
|
|
1005
|
+
if (ts.isCallExpression(expr)) {
|
|
1006
|
+
const first = expr.arguments[0];
|
|
1007
|
+
if (first && ts.isStringLiteralLike(first))
|
|
1008
|
+
arg = first.text;
|
|
1009
|
+
}
|
|
1010
|
+
return { name, arg };
|
|
1011
|
+
}
|
|
1012
|
+
/** The value declaration an expression resolves to (alias-followed), or undefined. */
|
|
1013
|
+
function valueDeclOf(expr, checker) {
|
|
1014
|
+
let symbol = checker.getSymbolAtLocation(expr);
|
|
1015
|
+
if (!symbol)
|
|
1016
|
+
return undefined;
|
|
1017
|
+
if (symbol.flags & ts.SymbolFlags.Alias)
|
|
1018
|
+
symbol = checker.getAliasedSymbol(symbol);
|
|
1019
|
+
return symbol.valueDeclaration ?? symbol.declarations?.[0];
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* The inline handler callbacks reachable from a registration call's arguments:
|
|
1023
|
+
* direct arrow/function args, plus arrows nested inside wrapper calls —
|
|
1024
|
+
* `tap("name", queryTool(session, () => …))`. Descends through call-argument
|
|
1025
|
+
* positions but stops at the first function in each branch: an arrow's body is its
|
|
1026
|
+
* own scope (its `.map`/`.then` callbacks are not handlers), and the wrapper's
|
|
1027
|
+
* non-call args (`session`) carry nothing. (ama-63x)
|
|
1028
|
+
*/
|
|
1029
|
+
function collectHandlerArrows(args) {
|
|
1030
|
+
const out = [];
|
|
1031
|
+
const dig = (expr) => {
|
|
1032
|
+
if (ts.isArrowFunction(expr) || ts.isFunctionExpression(expr)) {
|
|
1033
|
+
out.push(expr); // a handler — do not descend into its body
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
if (ts.isCallExpression(expr)) {
|
|
1037
|
+
for (const a of expr.arguments)
|
|
1038
|
+
dig(a);
|
|
1039
|
+
}
|
|
1040
|
+
};
|
|
1041
|
+
for (const a of args)
|
|
1042
|
+
dig(a);
|
|
1043
|
+
return out;
|
|
1044
|
+
}
|
|
1045
|
+
/** Join a controller prefix and a method sub-path into a leading-slash route path. */
|
|
1046
|
+
function joinRoutePath(prefix, sub) {
|
|
1047
|
+
const parts = [prefix, sub]
|
|
1048
|
+
.filter((p) => p !== undefined && p.length > 0)
|
|
1049
|
+
.map((p) => p.replace(/^\/+|\/+$/g, ""))
|
|
1050
|
+
.filter((p) => p.length > 0);
|
|
1051
|
+
return `/${parts.join("/")}`;
|
|
1052
|
+
}
|
|
1053
|
+
/** Map a declaration node to a (kind, name) pair, or undefined if it isn't one. */
|
|
1054
|
+
function describe(node) {
|
|
1055
|
+
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
1056
|
+
const component = isComponentName(node.name.text) && returnsJsx(node);
|
|
1057
|
+
return { kind: component ? "Component" : "Function", name: node.name.text };
|
|
1058
|
+
}
|
|
1059
|
+
if (ts.isClassDeclaration(node) && node.name) {
|
|
1060
|
+
return { kind: "Class", name: node.name.text };
|
|
1061
|
+
}
|
|
1062
|
+
if (ts.isInterfaceDeclaration(node)) {
|
|
1063
|
+
return { kind: "Interface", name: node.name.text };
|
|
1064
|
+
}
|
|
1065
|
+
if (ts.isEnumDeclaration(node)) {
|
|
1066
|
+
return { kind: "Enum", name: node.name.text };
|
|
1067
|
+
}
|
|
1068
|
+
if (ts.isTypeAliasDeclaration(node)) {
|
|
1069
|
+
return { kind: "TypeAlias", name: node.name.text };
|
|
1070
|
+
}
|
|
1071
|
+
// A `namespace N {}` / `module N {}` or an ambient `declare module "pkg" {}` —
|
|
1072
|
+
// the declared-but-never-emitted Module kind. The name is an Identifier
|
|
1073
|
+
// (namespace) or a StringLiteral (ambient module); both expose `.text`. (ama-hft.13)
|
|
1074
|
+
if (ts.isModuleDeclaration(node)) {
|
|
1075
|
+
return { kind: "Module", name: node.name.text };
|
|
1076
|
+
}
|
|
1077
|
+
if ((ts.isMethodDeclaration(node) || ts.isMethodSignature(node)) && ts.isIdentifier(node.name)) {
|
|
1078
|
+
return { kind: "Method", name: node.name.text };
|
|
1079
|
+
}
|
|
1080
|
+
// A constructor is a Method named "constructor" (qualified `Cls.constructor`),
|
|
1081
|
+
// so its body's wiring — calls, references, param-type usages — attributes to it
|
|
1082
|
+
// instead of being dropped at the class boundary. (ama-vz8)
|
|
1083
|
+
if (ts.isConstructorDeclaration(node)) {
|
|
1084
|
+
return { kind: "Method", name: "constructor" };
|
|
1085
|
+
}
|
|
1086
|
+
if ((ts.isPropertyDeclaration(node) || ts.isPropertySignature(node)) &&
|
|
1087
|
+
ts.isIdentifier(node.name)) {
|
|
1088
|
+
return { kind: "Property", name: node.name.text };
|
|
1089
|
+
}
|
|
1090
|
+
if ((ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node)) &&
|
|
1091
|
+
ts.isIdentifier(node.name)) {
|
|
1092
|
+
// A get/set pair shares one member name -> one Property node (ids dedup).
|
|
1093
|
+
return { kind: "Property", name: node.name.text };
|
|
1094
|
+
}
|
|
1095
|
+
if (ts.isVariableDeclaration(node) &&
|
|
1096
|
+
node.initializer !== undefined &&
|
|
1097
|
+
(ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer)) &&
|
|
1098
|
+
ts.isIdentifier(node.name)) {
|
|
1099
|
+
const component = isComponentName(node.name.text) && returnsJsx(node.initializer);
|
|
1100
|
+
return { kind: component ? "Component" : "Function", name: node.name.text };
|
|
1101
|
+
}
|
|
1102
|
+
// A Vue component: `const X = defineComponent({ … })`. Before the Variable
|
|
1103
|
+
// catch-all (its initializer is a call, not an object literal). (ama-rme.9)
|
|
1104
|
+
if (ts.isVariableDeclaration(node) &&
|
|
1105
|
+
ts.isIdentifier(node.name) &&
|
|
1106
|
+
node.initializer !== undefined &&
|
|
1107
|
+
ts.isCallExpression(node.initializer) &&
|
|
1108
|
+
isDefineComponentCall(node.initializer)) {
|
|
1109
|
+
return { kind: "Component", name: node.name.text };
|
|
1110
|
+
}
|
|
1111
|
+
// A function-valued object-literal property (`{ run: () => … }`) — a method in
|
|
1112
|
+
// all but syntax. Method shorthand (`{ run() {} }`) is already a MethodDeclaration.
|
|
1113
|
+
if (ts.isPropertyAssignment(node) &&
|
|
1114
|
+
(ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer)) &&
|
|
1115
|
+
ts.isIdentifier(node.name)) {
|
|
1116
|
+
return { kind: "Method", name: node.name.text };
|
|
1117
|
+
}
|
|
1118
|
+
// Any other module-level variable binding (`const MAX_RETRIES = 3`, `const SET =
|
|
1119
|
+
// new Set(...)`, `const LABELS = [...] as const`) — a Variable node so it's
|
|
1120
|
+
// searchable, snippet-able, and listed in a file's outline. Function-valued and
|
|
1121
|
+
// object-literal initializers are handled above / by `visit` (their members
|
|
1122
|
+
// become nodes; the object const itself stays a non-node, the ama-zkr rule).
|
|
1123
|
+
// `visit` only reaches top-level / class-member / object-member declarations, so
|
|
1124
|
+
// locals inside function bodies never become Variable nodes.
|
|
1125
|
+
if (ts.isVariableDeclaration(node) &&
|
|
1126
|
+
ts.isIdentifier(node.name) &&
|
|
1127
|
+
!(node.initializer !== undefined && ts.isObjectLiteralExpression(node.initializer))) {
|
|
1128
|
+
return { kind: "Variable", name: node.name.text };
|
|
1129
|
+
}
|
|
1130
|
+
return undefined;
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* Resolve a call's callee to a graph node id, following import aliases. Accepts
|
|
1134
|
+
* a `new` expression too: its `.expression` is the constructed class, which
|
|
1135
|
+
* resolves the same way (so construction counts as a call site).
|
|
1136
|
+
*/
|
|
1137
|
+
/** Collapse repeated Calls/Instantiates edges between the same (from, to) into a
|
|
1138
|
+
* single edge that records *every* call site in `sites`, so find_callers/callees
|
|
1139
|
+
* can report all of them rather than just the first the store would keep. Other
|
|
1140
|
+
* edge kinds pass through untouched. (ama-hft.10) */
|
|
1141
|
+
function accumulateCallSites(edges) {
|
|
1142
|
+
const callEdges = new Map();
|
|
1143
|
+
const result = [];
|
|
1144
|
+
for (const edge of edges) {
|
|
1145
|
+
if (edge.kind !== "Calls" && edge.kind !== "Instantiates") {
|
|
1146
|
+
result.push(edge);
|
|
1147
|
+
continue;
|
|
1148
|
+
}
|
|
1149
|
+
const key = `${edge.from}${edge.to}${edge.kind}`;
|
|
1150
|
+
const existing = callEdges.get(key);
|
|
1151
|
+
if (!existing) {
|
|
1152
|
+
callEdges.set(key, edge);
|
|
1153
|
+
result.push(edge);
|
|
1154
|
+
}
|
|
1155
|
+
else if (edge.at) {
|
|
1156
|
+
if (!existing.at)
|
|
1157
|
+
existing.at = edge.at;
|
|
1158
|
+
else {
|
|
1159
|
+
existing.sites ??= [existing.at];
|
|
1160
|
+
existing.sites.push(edge.at);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
return result;
|
|
1165
|
+
}
|
|
1166
|
+
/** Built-in higher-order methods that invoke a function argument — Array
|
|
1167
|
+
* iteration and Promise chaining. A named function passed to one of these is
|
|
1168
|
+
* attributed a heuristic Calls edge (the method name is a pattern match, not a
|
|
1169
|
+
* proof it calls the arg, hence heuristic). (ama-hft.15) */
|
|
1170
|
+
const HIGHER_ORDER_METHODS = new Set([
|
|
1171
|
+
"map",
|
|
1172
|
+
"forEach",
|
|
1173
|
+
"filter",
|
|
1174
|
+
"reduce",
|
|
1175
|
+
"reduceRight",
|
|
1176
|
+
"flatMap",
|
|
1177
|
+
"some",
|
|
1178
|
+
"every",
|
|
1179
|
+
"find",
|
|
1180
|
+
"findIndex",
|
|
1181
|
+
"findLast",
|
|
1182
|
+
"findLastIndex",
|
|
1183
|
+
"sort",
|
|
1184
|
+
"then",
|
|
1185
|
+
"catch",
|
|
1186
|
+
"finally",
|
|
1187
|
+
]);
|
|
1188
|
+
/** EventEmitter registration methods: `.on/.once/.addListener("ch", handler)` and
|
|
1189
|
+
* their prepend variants all bind a handler to a channel. (ama-hft.14) */
|
|
1190
|
+
const ON_METHODS = new Set(["on", "once", "addListener", "prependListener", "prependOnceListener"]);
|
|
1191
|
+
/** The node a `.on("ch", handler)` argument refers to: a named function/method
|
|
1192
|
+
* (resolved via the checker) or an inline arrow already synthesized into a
|
|
1193
|
+
* handler node by collectCallbackHandlers. (ama-hft.14) */
|
|
1194
|
+
function eventHandlerId(arg, declToId, checker, root) {
|
|
1195
|
+
if (ts.isIdentifier(arg) || ts.isPropertyAccessExpression(arg)) {
|
|
1196
|
+
return resolveValueRef(arg, checker, declToId, root);
|
|
1197
|
+
}
|
|
1198
|
+
if (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg)) {
|
|
1199
|
+
return declToId.get(arg);
|
|
1200
|
+
}
|
|
1201
|
+
return undefined;
|
|
1202
|
+
}
|
|
1203
|
+
/** The initializer of an object-literal property by key — reads route-config
|
|
1204
|
+
* fields (method/path/url/handler) for object-style routing. (ama-rme.10) */
|
|
1205
|
+
function objectProp(obj, key) {
|
|
1206
|
+
for (const prop of obj.properties) {
|
|
1207
|
+
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.text === key) {
|
|
1208
|
+
return prop.initializer;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
return undefined;
|
|
1212
|
+
}
|
|
1213
|
+
/** HTTP method(s) from a route config's `method` value — a single string ("GET")
|
|
1214
|
+
* or an array (["GET", "POST"]). (ama-rme.10) */
|
|
1215
|
+
function routeMethods(expr) {
|
|
1216
|
+
if (!expr)
|
|
1217
|
+
return [];
|
|
1218
|
+
if (ts.isStringLiteralLike(expr))
|
|
1219
|
+
return [expr.text];
|
|
1220
|
+
if (ts.isArrayLiteralExpression(expr)) {
|
|
1221
|
+
return expr.elements.filter(ts.isStringLiteralLike).map((e) => e.text);
|
|
1222
|
+
}
|
|
1223
|
+
return [];
|
|
1224
|
+
}
|
|
1225
|
+
/** tRPC procedure builders — a router property `key: proc.query(handler)`. (ama-rme.11) */
|
|
1226
|
+
const PROCEDURE_TYPES = new Set(["query", "mutation", "subscription"]);
|
|
1227
|
+
/** GraphQL root types in a resolver map. (ama-rme.11) */
|
|
1228
|
+
const GRAPHQL_ROOTS = new Set(["Query", "Mutation", "Subscription"]);
|
|
1229
|
+
/** Whether an expression looks like a route/resolver handler — an inline
|
|
1230
|
+
* function or a reference to one. Excludes string/config args so `db.query(sql)`
|
|
1231
|
+
* isn't mistaken for a tRPC procedure. (ama-rme.11) */
|
|
1232
|
+
function isHandlerExpr(expr) {
|
|
1233
|
+
return (ts.isArrowFunction(expr) ||
|
|
1234
|
+
ts.isFunctionExpression(expr) ||
|
|
1235
|
+
ts.isIdentifier(expr) ||
|
|
1236
|
+
ts.isPropertyAccessExpression(expr));
|
|
1237
|
+
}
|
|
1238
|
+
/** Convert a file-route directory segment to a URL segment: `[id]` → `:id`,
|
|
1239
|
+
* `[...slug]` → `*`, `[[opt]]` → `:opt`; a `(group)` is dropped (no URL effect);
|
|
1240
|
+
* else verbatim. (ama-rme.7) */
|
|
1241
|
+
function routeSegment(seg) {
|
|
1242
|
+
if (seg.startsWith("(") && seg.endsWith(")"))
|
|
1243
|
+
return undefined;
|
|
1244
|
+
if (seg.startsWith("[...") && seg.endsWith("]"))
|
|
1245
|
+
return "*";
|
|
1246
|
+
if (seg.startsWith("[[") && seg.endsWith("]]"))
|
|
1247
|
+
return `:${seg.slice(2, -2)}`;
|
|
1248
|
+
if (seg.startsWith("[") && seg.endsWith("]"))
|
|
1249
|
+
return `:${seg.slice(1, -1)}`;
|
|
1250
|
+
return seg;
|
|
1251
|
+
}
|
|
1252
|
+
/** The URL path a file-based route file maps to, or undefined if it isn't one:
|
|
1253
|
+
* Next.js App Router `app/**/route.ts` and SvelteKit `src/routes/**/+server.ts`
|
|
1254
|
+
* — the path is the directories between the routes root and the marker file. (ama-rme.7) */
|
|
1255
|
+
function fileRoutePath(rel) {
|
|
1256
|
+
const parts = rel.split("/");
|
|
1257
|
+
const file = parts[parts.length - 1] ?? "";
|
|
1258
|
+
let rootIdx = -1;
|
|
1259
|
+
if (file.startsWith("route."))
|
|
1260
|
+
rootIdx = parts.lastIndexOf("app");
|
|
1261
|
+
else if (file.startsWith("+server."))
|
|
1262
|
+
rootIdx = parts.lastIndexOf("routes");
|
|
1263
|
+
if (rootIdx < 0)
|
|
1264
|
+
return undefined;
|
|
1265
|
+
const segs = parts
|
|
1266
|
+
.slice(rootIdx + 1, parts.length - 1)
|
|
1267
|
+
.map(routeSegment)
|
|
1268
|
+
.filter((s) => s !== undefined);
|
|
1269
|
+
return `/${segs.join("/")}`;
|
|
1270
|
+
}
|
|
1271
|
+
/** Module extensions a filename-routed endpoint can use. (ama-w7g) */
|
|
1272
|
+
const FILE_ROUTE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs"]);
|
|
1273
|
+
/** The URL path for a *filename*-routed module (the route includes the filename),
|
|
1274
|
+
* plus whether a default export should count as a handler. Next.js Pages Router
|
|
1275
|
+
* & Astro live under `pages/`/`src/pages/`; Nuxt under `server/api/` (keeps the
|
|
1276
|
+
* `/api` prefix) or `server/routes/` (stripped). `index` maps to its directory.
|
|
1277
|
+
* `allowDefault` is true only in an API context — a `pages/about.tsx` default
|
|
1278
|
+
* export is a page component, not a route. (ama-w7g) */
|
|
1279
|
+
function filenameRoutePath(rel) {
|
|
1280
|
+
const parts = rel.split("/");
|
|
1281
|
+
const file = parts[parts.length - 1] ?? "";
|
|
1282
|
+
const dot = file.lastIndexOf(".");
|
|
1283
|
+
if (dot < 0 || !FILE_ROUTE_EXTS.has(file.slice(dot)))
|
|
1284
|
+
return undefined;
|
|
1285
|
+
const base = file.slice(0, dot);
|
|
1286
|
+
if (base.startsWith("_"))
|
|
1287
|
+
return undefined; // Next.js specials: _app, _document
|
|
1288
|
+
const pagesIdx = parts.lastIndexOf("pages");
|
|
1289
|
+
const serverIdx = parts.lastIndexOf("server");
|
|
1290
|
+
let rootIdx;
|
|
1291
|
+
let allowDefault;
|
|
1292
|
+
if (pagesIdx >= 0) {
|
|
1293
|
+
rootIdx = pagesIdx;
|
|
1294
|
+
allowDefault = parts[pagesIdx + 1] === "api"; // Next API routes live under pages/api
|
|
1295
|
+
}
|
|
1296
|
+
else if (serverIdx >= 0 && parts[serverIdx + 1] === "routes") {
|
|
1297
|
+
rootIdx = serverIdx + 1; // server/routes — `routes` stripped from the URL
|
|
1298
|
+
allowDefault = true;
|
|
1299
|
+
}
|
|
1300
|
+
else if (serverIdx >= 0 && parts[serverIdx + 1] === "api") {
|
|
1301
|
+
rootIdx = serverIdx; // server/api — `api` kept in the URL
|
|
1302
|
+
allowDefault = true;
|
|
1303
|
+
}
|
|
1304
|
+
else {
|
|
1305
|
+
return undefined;
|
|
1306
|
+
}
|
|
1307
|
+
const dirs = parts.slice(rootIdx + 1, parts.length - 1);
|
|
1308
|
+
const leaf = base === "index" ? [] : [base];
|
|
1309
|
+
const segs = [...dirs, ...leaf].map(routeSegment).filter((s) => s !== undefined);
|
|
1310
|
+
return { path: `/${segs.join("/")}`, allowDefault };
|
|
1311
|
+
}
|
|
1312
|
+
/** Whether a declaration carries the `default` modifier (`export default …`). (ama-w7g) */
|
|
1313
|
+
function isDefaultExported(node) {
|
|
1314
|
+
return (ts.canHaveModifiers(node) &&
|
|
1315
|
+
(ts.getModifiers(node) ?? []).some((m) => m.kind === ts.SyntaxKind.DefaultKeyword));
|
|
1316
|
+
}
|
|
1317
|
+
/** Whether a callee is Nuxt's `defineEventHandler`/`eventHandler` wrapper. (ama-w7g) */
|
|
1318
|
+
function isEventHandlerWrapper(expr) {
|
|
1319
|
+
const name = ts.isIdentifier(expr)
|
|
1320
|
+
? expr.text
|
|
1321
|
+
: ts.isPropertyAccessExpression(expr)
|
|
1322
|
+
? expr.name.text
|
|
1323
|
+
: "";
|
|
1324
|
+
return name === "defineEventHandler" || name === "eventHandler";
|
|
1325
|
+
}
|
|
1326
|
+
/** A module's default-export request handler: a named `export default function`
|
|
1327
|
+
* (linked via declToId) or an `export default <expr>` (arrow/ref, with Nuxt's
|
|
1328
|
+
* `defineEventHandler(...)` unwrapped). Anonymous default functions have no node
|
|
1329
|
+
* to link, so they're skipped. (ama-w7g) */
|
|
1330
|
+
function findDefaultExportHandler(sf) {
|
|
1331
|
+
for (const stmt of sf.statements) {
|
|
1332
|
+
if (ts.isFunctionDeclaration(stmt) && isDefaultExported(stmt)) {
|
|
1333
|
+
return stmt.name ? { decl: stmt } : undefined;
|
|
1334
|
+
}
|
|
1335
|
+
if (ts.isExportAssignment(stmt) && !stmt.isExportEquals) {
|
|
1336
|
+
let expr = stmt.expression;
|
|
1337
|
+
if (ts.isCallExpression(expr) &&
|
|
1338
|
+
isEventHandlerWrapper(expr.expression) &&
|
|
1339
|
+
expr.arguments[0]) {
|
|
1340
|
+
expr = expr.arguments[0];
|
|
1341
|
+
}
|
|
1342
|
+
return { expr };
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
return undefined;
|
|
1346
|
+
}
|
|
1347
|
+
/** Whether a top-level statement carries an `export` modifier. (ama-rme.7) */
|
|
1348
|
+
function isExported(node) {
|
|
1349
|
+
return (ts.canHaveModifiers(node) &&
|
|
1350
|
+
(ts.getModifiers(node) ?? []).some((m) => m.kind === ts.SyntaxKind.ExportKeyword));
|
|
1351
|
+
}
|
|
1352
|
+
/** React requires a component name to be capitalized (so JSX `<Foo/>` isn't a host
|
|
1353
|
+
* element). Used to tell a JSX-returning component from a render helper. (ama-rme.9) */
|
|
1354
|
+
function isComponentName(name) {
|
|
1355
|
+
return /^[A-Z]/.test(name);
|
|
1356
|
+
}
|
|
1357
|
+
/** Whether an expression is a JSX element/fragment (through parentheses). (ama-rme.9) */
|
|
1358
|
+
function isJsxLike(expr) {
|
|
1359
|
+
let e = expr;
|
|
1360
|
+
while (ts.isParenthesizedExpression(e))
|
|
1361
|
+
e = e.expression;
|
|
1362
|
+
return ts.isJsxElement(e) || ts.isJsxFragment(e) || ts.isJsxSelfClosingElement(e);
|
|
1363
|
+
}
|
|
1364
|
+
/** Whether a function returns JSX — an arrow with a JSX concise body, or any
|
|
1365
|
+
* `return <jsx>` in its block (not counting nested functions). (ama-rme.9) */
|
|
1366
|
+
function returnsJsx(fn) {
|
|
1367
|
+
if (ts.isArrowFunction(fn) && fn.body && !ts.isBlock(fn.body))
|
|
1368
|
+
return isJsxLike(fn.body);
|
|
1369
|
+
if (!fn.body)
|
|
1370
|
+
return false;
|
|
1371
|
+
let found = false;
|
|
1372
|
+
const visit = (n) => {
|
|
1373
|
+
if (found)
|
|
1374
|
+
return;
|
|
1375
|
+
if (ts.isReturnStatement(n) && n.expression && isJsxLike(n.expression)) {
|
|
1376
|
+
found = true;
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
// A nested function/class has its own returns — don't attribute them here.
|
|
1380
|
+
if (ts.isFunctionDeclaration(n) ||
|
|
1381
|
+
ts.isFunctionExpression(n) ||
|
|
1382
|
+
ts.isArrowFunction(n) ||
|
|
1383
|
+
ts.isClassDeclaration(n)) {
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
n.forEachChild(visit);
|
|
1387
|
+
};
|
|
1388
|
+
visit(fn.body);
|
|
1389
|
+
return found;
|
|
1390
|
+
}
|
|
1391
|
+
/** Whether a call is `defineComponent(...)` — Vue's component factory. (ama-rme.9) */
|
|
1392
|
+
function isDefineComponentCall(call) {
|
|
1393
|
+
const callee = call.expression;
|
|
1394
|
+
if (ts.isIdentifier(callee))
|
|
1395
|
+
return callee.text === "defineComponent";
|
|
1396
|
+
if (ts.isPropertyAccessExpression(callee))
|
|
1397
|
+
return callee.name.text === "defineComponent";
|
|
1398
|
+
return false;
|
|
1399
|
+
}
|
|
1400
|
+
/** The leftmost identifier of a call's callee — `ts` for `ts.isCallExpression(x)`,
|
|
1401
|
+
* `helper` for `helper()` — used to group unresolved calls by what they target
|
|
1402
|
+
* (a module/object name). `this.X...` groups by `X` (the property/method on
|
|
1403
|
+
* `this`), since the bare `this` root is opaque about where the call is — most of
|
|
1404
|
+
* these are builtin calls on instance fields like `this.items.push()`. Undefined
|
|
1405
|
+
* for call results, super(), etc. (ama-qbn, ama-k9t) */
|
|
1406
|
+
function calleeRoot(call) {
|
|
1407
|
+
let e = call.expression;
|
|
1408
|
+
while (ts.isPropertyAccessExpression(e) || ts.isElementAccessExpression(e)) {
|
|
1409
|
+
if (e.expression.kind === ts.SyntaxKind.ThisKeyword) {
|
|
1410
|
+
return ts.isPropertyAccessExpression(e) ? e.name.text : undefined;
|
|
1411
|
+
}
|
|
1412
|
+
e = e.expression;
|
|
1413
|
+
}
|
|
1414
|
+
if (ts.isIdentifier(e))
|
|
1415
|
+
return e.text;
|
|
1416
|
+
if (e.kind === ts.SyntaxKind.ThisKeyword)
|
|
1417
|
+
return "this";
|
|
1418
|
+
return undefined;
|
|
1419
|
+
}
|
|
1420
|
+
function resolveCallee(call, checker, declToId, root) {
|
|
1421
|
+
let symbol = checker.getSymbolAtLocation(call.expression);
|
|
1422
|
+
if (!symbol) {
|
|
1423
|
+
const decl = checker.getResolvedSignature(call)?.declaration;
|
|
1424
|
+
return decl ? (declToId.get(decl) ?? nodeIdForDecl(decl, root)) : undefined;
|
|
1425
|
+
}
|
|
1426
|
+
if (symbol.flags & ts.SymbolFlags.Alias) {
|
|
1427
|
+
symbol = checker.getAliasedSymbol(symbol);
|
|
1428
|
+
}
|
|
1429
|
+
const decl = symbol.valueDeclaration ?? symbol.declarations?.[0];
|
|
1430
|
+
return decl ? (declToId.get(decl) ?? nodeIdForDecl(decl, root)) : undefined;
|
|
1431
|
+
}
|
|
1432
|
+
/** Resolve a value-position reference (e.g. a decorator's `@Foo`) to a node id. */
|
|
1433
|
+
function resolveValueRef(expr, checker, declToId, root) {
|
|
1434
|
+
let symbol = checker.getSymbolAtLocation(expr);
|
|
1435
|
+
if (!symbol)
|
|
1436
|
+
return undefined;
|
|
1437
|
+
if (symbol.flags & ts.SymbolFlags.Alias) {
|
|
1438
|
+
symbol = checker.getAliasedSymbol(symbol);
|
|
1439
|
+
}
|
|
1440
|
+
const decl = symbol.valueDeclaration ?? symbol.declarations?.[0];
|
|
1441
|
+
return decl ? (declToId.get(decl) ?? nodeIdForDecl(decl, root)) : undefined;
|
|
1442
|
+
}
|
|
1443
|
+
/** Resolve a named property of a type to its declaration's node id — for destructuring,
|
|
1444
|
+
* where the read is a binding name against the destructured value's type rather than a
|
|
1445
|
+
* `.prop` expression. Same in-project filtering as {@link resolveValueRef}. (ama-eda) */
|
|
1446
|
+
function resolveTypeMember(type, name, declToId, root) {
|
|
1447
|
+
const prop = type.getProperty(name);
|
|
1448
|
+
const decl = prop?.valueDeclaration ?? prop?.declarations?.[0];
|
|
1449
|
+
return decl ? (declToId.get(decl) ?? nodeIdForDecl(decl, root)) : undefined;
|
|
1450
|
+
}
|
|
1451
|
+
/** Resolve an identifier read to the node id of the module-level variable it
|
|
1452
|
+
* names, or undefined if it doesn't resolve to a top-level const/let/var. Unlike
|
|
1453
|
+
* the old batch-local variableIds gate, this checks the *decl kind*, so a
|
|
1454
|
+
* cross-file const still resolves in a single-file reindex (where the batch holds
|
|
1455
|
+
* no other file's Variable nodes) — via declToId or, failing that, by location.
|
|
1456
|
+
* (ama-l6k) */
|
|
1457
|
+
function resolveModuleVarRef(expr, checker, declToId, root) {
|
|
1458
|
+
let symbol = checker.getSymbolAtLocation(expr);
|
|
1459
|
+
if (!symbol)
|
|
1460
|
+
return undefined;
|
|
1461
|
+
if (symbol.flags & ts.SymbolFlags.Alias) {
|
|
1462
|
+
symbol = checker.getAliasedSymbol(symbol);
|
|
1463
|
+
}
|
|
1464
|
+
const decl = symbol.valueDeclaration ?? symbol.declarations?.[0];
|
|
1465
|
+
if (!decl || !isModuleVariableDecl(decl))
|
|
1466
|
+
return undefined;
|
|
1467
|
+
return declToId.get(decl) ?? nodeIdForDecl(decl, root);
|
|
1468
|
+
}
|
|
1469
|
+
/** Resolve a heritage type reference (e.g. the `I` in `implements I`) to a node id. */
|
|
1470
|
+
function resolveHeritage(expr, checker, declToId, root) {
|
|
1471
|
+
let symbol = checker.getSymbolAtLocation(expr);
|
|
1472
|
+
if (!symbol)
|
|
1473
|
+
return undefined;
|
|
1474
|
+
if (symbol.flags & ts.SymbolFlags.Alias) {
|
|
1475
|
+
symbol = checker.getAliasedSymbol(symbol);
|
|
1476
|
+
}
|
|
1477
|
+
// Interfaces are type-only, so they have no valueDeclaration — use declarations.
|
|
1478
|
+
const decl = symbol.declarations?.[0];
|
|
1479
|
+
return decl ? (declToId.get(decl) ?? nodeIdForDecl(decl, root)) : undefined;
|
|
1480
|
+
}
|
|
1481
|
+
/** Resolve an imported or re-exported name to its original declaration's node id. */
|
|
1482
|
+
function resolveImport(name, checker, declToId, root) {
|
|
1483
|
+
let symbol = checker.getSymbolAtLocation(name);
|
|
1484
|
+
if (!symbol)
|
|
1485
|
+
return undefined;
|
|
1486
|
+
// Import/re-export bindings are aliases; follow the chain to the real declaration.
|
|
1487
|
+
if (symbol.flags & ts.SymbolFlags.Alias) {
|
|
1488
|
+
symbol = checker.getAliasedSymbol(symbol);
|
|
1489
|
+
}
|
|
1490
|
+
const decl = symbol.valueDeclaration ?? symbol.declarations?.[0];
|
|
1491
|
+
return decl ? (declToId.get(decl) ?? nodeIdForDecl(decl, root)) : undefined;
|
|
1492
|
+
}
|
|
1493
|
+
/** Resolve a type reference's name (e.g. the `Foo` in `x: Foo`) to a node id. */
|
|
1494
|
+
function resolveTypeRef(name, checker, declToId, root) {
|
|
1495
|
+
let symbol = checker.getSymbolAtLocation(name);
|
|
1496
|
+
if (!symbol)
|
|
1497
|
+
return undefined;
|
|
1498
|
+
if (symbol.flags & ts.SymbolFlags.Alias) {
|
|
1499
|
+
symbol = checker.getAliasedSymbol(symbol);
|
|
1500
|
+
}
|
|
1501
|
+
// Types are usually type-only (no valueDeclaration), so prefer declarations.
|
|
1502
|
+
const decl = symbol.declarations?.[0] ?? symbol.valueDeclaration;
|
|
1503
|
+
return decl ? (declToId.get(decl) ?? nodeIdForDecl(decl, root)) : undefined;
|
|
1504
|
+
}
|
|
1505
|
+
/**
|
|
1506
|
+
* The graph id a declaration *would* receive from a structural walk, computed
|
|
1507
|
+
* from its location alone. This lets resolution target a node in a file the
|
|
1508
|
+
* current pass never walked — the cross-file case during single-file
|
|
1509
|
+
* re-indexing, where `declToId` only holds the one changed file. Returns
|
|
1510
|
+
* undefined for declarations a walk would not emit (nested functions, library
|
|
1511
|
+
* code outside `root`), so the graph never asserts an edge it cannot back.
|
|
1512
|
+
*
|
|
1513
|
+
* It mirrors {@link visit}'s reachability exactly: a node exists only as a
|
|
1514
|
+
* top-level declaration, or as a member of a top-level class/interface.
|
|
1515
|
+
*/
|
|
1516
|
+
function nodeIdForDecl(node, root) {
|
|
1517
|
+
// Module references (namespace imports / star re-exports) target the File node.
|
|
1518
|
+
if (ts.isSourceFile(node)) {
|
|
1519
|
+
const rel = repoRel(root, node.fileName);
|
|
1520
|
+
return rel === undefined ? undefined : fileId(rel);
|
|
1521
|
+
}
|
|
1522
|
+
const self = describe(node);
|
|
1523
|
+
if (!self)
|
|
1524
|
+
return undefined;
|
|
1525
|
+
const rel = repoRel(root, node.getSourceFile().fileName);
|
|
1526
|
+
if (rel === undefined)
|
|
1527
|
+
return undefined;
|
|
1528
|
+
const parent = node.parent;
|
|
1529
|
+
if (parent && ts.isSourceFile(parent)) {
|
|
1530
|
+
return symbolId({ file: rel, qualifiedName: self.name });
|
|
1531
|
+
}
|
|
1532
|
+
if (parent &&
|
|
1533
|
+
(ts.isClassDeclaration(parent) || ts.isInterfaceDeclaration(parent)) &&
|
|
1534
|
+
parent.name &&
|
|
1535
|
+
parent.parent &&
|
|
1536
|
+
ts.isSourceFile(parent.parent)) {
|
|
1537
|
+
return symbolId({ file: rel, qualifiedName: `${parent.name.text}.${self.name}` });
|
|
1538
|
+
}
|
|
1539
|
+
// A top-level `const X = …`: VariableDeclaration -> VariableDeclarationList ->
|
|
1540
|
+
// VariableStatement -> SourceFile. Its node id is the module-scoped name, so a
|
|
1541
|
+
// cross-file const resolves in a single-file reindex too. (ama-l6k)
|
|
1542
|
+
if (isModuleVariableDecl(node)) {
|
|
1543
|
+
return symbolId({ file: rel, qualifiedName: self.name });
|
|
1544
|
+
}
|
|
1545
|
+
return undefined;
|
|
1546
|
+
}
|
|
1547
|
+
/** Whether `node` is a top-level `const`/`let`/`var` declaration (module scope):
|
|
1548
|
+
* VariableDeclaration -> VariableDeclarationList -> VariableStatement ->
|
|
1549
|
+
* SourceFile. (ama-l6k) */
|
|
1550
|
+
function isModuleVariableDecl(node) {
|
|
1551
|
+
return (ts.isVariableDeclaration(node) &&
|
|
1552
|
+
ts.isVariableDeclarationList(node.parent) &&
|
|
1553
|
+
ts.isVariableStatement(node.parent.parent) &&
|
|
1554
|
+
ts.isSourceFile(node.parent.parent.parent));
|
|
1555
|
+
}
|
|
1556
|
+
/** Repo-relative path of an absolute file, or undefined if it falls outside the
|
|
1557
|
+
* indexed tree (a different package, or `node_modules`). */
|
|
1558
|
+
function repoRel(root, fileName) {
|
|
1559
|
+
const rel = path.relative(root, fileName);
|
|
1560
|
+
if (!rel || rel.startsWith("..") || path.isAbsolute(rel))
|
|
1561
|
+
return undefined;
|
|
1562
|
+
if (rel.split(path.sep).includes("node_modules"))
|
|
1563
|
+
return undefined;
|
|
1564
|
+
return rel;
|
|
1565
|
+
}
|
|
1566
|
+
/** Every type reference within a type annotation, including those nested in
|
|
1567
|
+
* arrays, unions, and generic type arguments (so `Map<K, Foo>` yields `Foo`). */
|
|
1568
|
+
function typeReferencesIn(node) {
|
|
1569
|
+
const refs = [];
|
|
1570
|
+
const walk = (n) => {
|
|
1571
|
+
if (ts.isTypeReferenceNode(n))
|
|
1572
|
+
refs.push(n);
|
|
1573
|
+
n.forEachChild(walk);
|
|
1574
|
+
};
|
|
1575
|
+
walk(node);
|
|
1576
|
+
return refs;
|
|
1577
|
+
}
|
|
1578
|
+
function rangeOf(node, sf) {
|
|
1579
|
+
const start = sf.getLineAndCharacterOfPosition(node.getStart(sf));
|
|
1580
|
+
const end = sf.getLineAndCharacterOfPosition(node.getEnd());
|
|
1581
|
+
return { startLine: start.line + 1, endLine: end.line + 1 };
|
|
1582
|
+
}
|
|
1583
|
+
/** Whether a constructor parameter is a *parameter property* — it carries an
|
|
1584
|
+
* accessibility (`public`/`private`/`protected`) or `readonly` modifier, which
|
|
1585
|
+
* makes it a real class member, not just an argument. (ama-259) */
|
|
1586
|
+
function isParameterProperty(param) {
|
|
1587
|
+
const mods = ts.canHaveModifiers(param) ? ts.getModifiers(param) : undefined;
|
|
1588
|
+
return (mods?.some((m) => m.kind === ts.SyntaxKind.PublicKeyword ||
|
|
1589
|
+
m.kind === ts.SyntaxKind.PrivateKeyword ||
|
|
1590
|
+
m.kind === ts.SyntaxKind.ProtectedKeyword ||
|
|
1591
|
+
m.kind === ts.SyntaxKind.ReadonlyKeyword) ?? false);
|
|
1592
|
+
}
|
|
1593
|
+
/** A node's 1-based (line, column) start — for tagging an edge with its source
|
|
1594
|
+
* site (a call/new expression's position). (ama-hft.9) */
|
|
1595
|
+
function locationOf(node) {
|
|
1596
|
+
const sf = node.getSourceFile();
|
|
1597
|
+
const { line, character } = sf.getLineAndCharacterOfPosition(node.getStart(sf));
|
|
1598
|
+
return { line: line + 1, column: character + 1 };
|
|
1599
|
+
}
|
|
1600
|
+
//# sourceMappingURL=analyzer.js.map
|