@spikedpunch/mast 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/MAST_SPEC.md +2926 -0
- package/README.md +842 -0
- package/assets/skill.md +45 -0
- package/dist/ast/extract.d.ts +65 -0
- package/dist/ast/extract.d.ts.map +1 -0
- package/dist/ast/extract.js +147 -0
- package/dist/ast/extract.js.map +1 -0
- package/dist/ast/extractor.d.ts +85 -0
- package/dist/ast/extractor.d.ts.map +1 -0
- package/dist/ast/extractor.js +2 -0
- package/dist/ast/extractor.js.map +1 -0
- package/dist/ast/extractors/markdown.d.ts +40 -0
- package/dist/ast/extractors/markdown.d.ts.map +1 -0
- package/dist/ast/extractors/markdown.js +175 -0
- package/dist/ast/extractors/markdown.js.map +1 -0
- package/dist/ast/extractors/typescript.d.ts +179 -0
- package/dist/ast/extractors/typescript.d.ts.map +1 -0
- package/dist/ast/extractors/typescript.js +1439 -0
- package/dist/ast/extractors/typescript.js.map +1 -0
- package/dist/ast/parser.d.ts +5 -0
- package/dist/ast/parser.d.ts.map +1 -0
- package/dist/ast/parser.js +21 -0
- package/dist/ast/parser.js.map +1 -0
- package/dist/ast/types.d.ts +586 -0
- package/dist/ast/types.d.ts.map +1 -0
- package/dist/ast/types.js +3 -0
- package/dist/ast/types.js.map +1 -0
- package/dist/cli/docs-cmd.d.ts +22 -0
- package/dist/cli/docs-cmd.d.ts.map +1 -0
- package/dist/cli/docs-cmd.js +100 -0
- package/dist/cli/docs-cmd.js.map +1 -0
- package/dist/cli/index-cmd.d.ts +72 -0
- package/dist/cli/index-cmd.d.ts.map +1 -0
- package/dist/cli/index-cmd.js +263 -0
- package/dist/cli/index-cmd.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 +4 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.d.ts +14 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +64 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/install-hooks.d.ts +3 -0
- package/dist/cli/install-hooks.d.ts.map +1 -0
- package/dist/cli/install-hooks.js +34 -0
- package/dist/cli/install-hooks.js.map +1 -0
- package/dist/cli/metrics-cmd.d.ts +3 -0
- package/dist/cli/metrics-cmd.d.ts.map +1 -0
- package/dist/cli/metrics-cmd.js +164 -0
- package/dist/cli/metrics-cmd.js.map +1 -0
- package/dist/cli/program.d.ts +14 -0
- package/dist/cli/program.d.ts.map +1 -0
- package/dist/cli/program.js +44 -0
- package/dist/cli/program.js.map +1 -0
- package/dist/cli/query.d.ts +46 -0
- package/dist/cli/query.d.ts.map +1 -0
- package/dist/cli/query.js +153 -0
- package/dist/cli/query.js.map +1 -0
- package/dist/cli/search-cmd.d.ts +33 -0
- package/dist/cli/search-cmd.d.ts.map +1 -0
- package/dist/cli/search-cmd.js +114 -0
- package/dist/cli/search-cmd.js.map +1 -0
- package/dist/cli/serve.d.ts +3 -0
- package/dist/cli/serve.d.ts.map +1 -0
- package/dist/cli/serve.js +19 -0
- package/dist/cli/serve.js.map +1 -0
- package/dist/cli/skill-install.d.ts +39 -0
- package/dist/cli/skill-install.d.ts.map +1 -0
- package/dist/cli/skill-install.js +57 -0
- package/dist/cli/skill-install.js.map +1 -0
- package/dist/cli/status.d.ts +35 -0
- package/dist/cli/status.d.ts.map +1 -0
- package/dist/cli/status.js +117 -0
- package/dist/cli/status.js.map +1 -0
- package/dist/cli/upgrade-cmd.d.ts +48 -0
- package/dist/cli/upgrade-cmd.d.ts.map +1 -0
- package/dist/cli/upgrade-cmd.js +129 -0
- package/dist/cli/upgrade-cmd.js.map +1 -0
- package/dist/cli/version.d.ts +3 -0
- package/dist/cli/version.d.ts.map +1 -0
- package/dist/cli/version.js +19 -0
- package/dist/cli/version.js.map +1 -0
- package/dist/env.d.ts +17 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +17 -0
- package/dist/env.js.map +1 -0
- package/dist/graph/checker-resolver.d.ts +156 -0
- package/dist/graph/checker-resolver.d.ts.map +1 -0
- package/dist/graph/checker-resolver.js +458 -0
- package/dist/graph/checker-resolver.js.map +1 -0
- package/dist/graph/db.d.ts +318 -0
- package/dist/graph/db.d.ts.map +1 -0
- package/dist/graph/db.js +266 -0
- package/dist/graph/db.js.map +1 -0
- package/dist/graph/index.d.ts +4 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/index.js +5 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/graph/local-type-env.d.ts +45 -0
- package/dist/graph/local-type-env.d.ts.map +1 -0
- package/dist/graph/local-type-env.js +59 -0
- package/dist/graph/local-type-env.js.map +1 -0
- package/dist/graph/path-range.d.ts +40 -0
- package/dist/graph/path-range.d.ts.map +1 -0
- package/dist/graph/path-range.js +42 -0
- package/dist/graph/path-range.js.map +1 -0
- package/dist/graph/populate.d.ts +263 -0
- package/dist/graph/populate.d.ts.map +1 -0
- package/dist/graph/populate.js +984 -0
- package/dist/graph/populate.js.map +1 -0
- package/dist/graph/queries.d.ts +114 -0
- package/dist/graph/queries.d.ts.map +1 -0
- package/dist/graph/queries.js +415 -0
- package/dist/graph/queries.js.map +1 -0
- package/dist/graph/sqliteBatch.d.ts +48 -0
- package/dist/graph/sqliteBatch.d.ts.map +1 -0
- package/dist/graph/sqliteBatch.js +93 -0
- package/dist/graph/sqliteBatch.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 +72 -0
- package/dist/indexer/freshness.js.map +1 -0
- package/dist/indexer/import-resolver.d.ts +50 -0
- package/dist/indexer/import-resolver.d.ts.map +1 -0
- package/dist/indexer/import-resolver.js +318 -0
- package/dist/indexer/import-resolver.js.map +1 -0
- package/dist/indexer/index.d.ts +249 -0
- package/dist/indexer/index.d.ts.map +1 -0
- package/dist/indexer/index.js +560 -0
- package/dist/indexer/index.js.map +1 -0
- package/dist/indexer/walker.d.ts +60 -0
- package/dist/indexer/walker.d.ts.map +1 -0
- package/dist/indexer/walker.js +128 -0
- package/dist/indexer/walker.js.map +1 -0
- package/dist/indexer/watcher.d.ts +80 -0
- package/dist/indexer/watcher.d.ts.map +1 -0
- package/dist/indexer/watcher.js +151 -0
- package/dist/indexer/watcher.js.map +1 -0
- package/dist/mcp/context.d.ts +23 -0
- package/dist/mcp/context.d.ts.map +1 -0
- package/dist/mcp/context.js +2 -0
- package/dist/mcp/context.js.map +1 -0
- package/dist/mcp/register-tools.d.ts +21 -0
- package/dist/mcp/register-tools.d.ts.map +1 -0
- package/dist/mcp/register-tools.js +42 -0
- package/dist/mcp/register-tools.js.map +1 -0
- package/dist/mcp/server.d.ts +58 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +177 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/staleness.d.ts +72 -0
- package/dist/mcp/staleness.d.ts.map +1 -0
- package/dist/mcp/staleness.js +213 -0
- package/dist/mcp/staleness.js.map +1 -0
- package/dist/mcp/startup.d.ts +40 -0
- package/dist/mcp/startup.d.ts.map +1 -0
- package/dist/mcp/startup.js +114 -0
- package/dist/mcp/startup.js.map +1 -0
- package/dist/mcp/tools/_helpers.d.ts +26 -0
- package/dist/mcp/tools/_helpers.d.ts.map +1 -0
- package/dist/mcp/tools/_helpers.js +54 -0
- package/dist/mcp/tools/_helpers.js.map +1 -0
- package/dist/mcp/tools/callers.d.ts +4 -0
- package/dist/mcp/tools/callers.d.ts.map +1 -0
- package/dist/mcp/tools/callers.js +112 -0
- package/dist/mcp/tools/callers.js.map +1 -0
- package/dist/mcp/tools/dependencies.d.ts +4 -0
- package/dist/mcp/tools/dependencies.d.ts.map +1 -0
- package/dist/mcp/tools/dependencies.js +36 -0
- package/dist/mcp/tools/dependencies.js.map +1 -0
- package/dist/mcp/tools/efficiency.d.ts +4 -0
- package/dist/mcp/tools/efficiency.d.ts.map +1 -0
- package/dist/mcp/tools/efficiency.js +39 -0
- package/dist/mcp/tools/efficiency.js.map +1 -0
- package/dist/mcp/tools/exports.d.ts +4 -0
- package/dist/mcp/tools/exports.d.ts.map +1 -0
- package/dist/mcp/tools/exports.js +56 -0
- package/dist/mcp/tools/exports.js.map +1 -0
- package/dist/mcp/tools/implementors.d.ts +4 -0
- package/dist/mcp/tools/implementors.d.ts.map +1 -0
- package/dist/mcp/tools/implementors.js +47 -0
- package/dist/mcp/tools/implementors.js.map +1 -0
- package/dist/mcp/tools/project-skeleton.d.ts +4 -0
- package/dist/mcp/tools/project-skeleton.d.ts.map +1 -0
- package/dist/mcp/tools/project-skeleton.js +61 -0
- package/dist/mcp/tools/project-skeleton.js.map +1 -0
- package/dist/mcp/tools/reindex.d.ts +14 -0
- package/dist/mcp/tools/reindex.d.ts.map +1 -0
- package/dist/mcp/tools/reindex.js +32 -0
- package/dist/mcp/tools/reindex.js.map +1 -0
- package/dist/mcp/tools/rename-impact.d.ts +4 -0
- package/dist/mcp/tools/rename-impact.d.ts.map +1 -0
- package/dist/mcp/tools/rename-impact.js +124 -0
- package/dist/mcp/tools/rename-impact.js.map +1 -0
- package/dist/mcp/tools/search.d.ts +4 -0
- package/dist/mcp/tools/search.d.ts.map +1 -0
- package/dist/mcp/tools/search.js +69 -0
- package/dist/mcp/tools/search.js.map +1 -0
- package/dist/mcp/tools/signature.d.ts +4 -0
- package/dist/mcp/tools/signature.d.ts.map +1 -0
- package/dist/mcp/tools/signature.js +127 -0
- package/dist/mcp/tools/signature.js.map +1 -0
- package/dist/mcp/tools/status.d.ts +4 -0
- package/dist/mcp/tools/status.d.ts.map +1 -0
- package/dist/mcp/tools/status.js +30 -0
- package/dist/mcp/tools/status.js.map +1 -0
- package/dist/search/declex.d.ts +109 -0
- package/dist/search/declex.d.ts.map +1 -0
- package/dist/search/declex.js +242 -0
- package/dist/search/declex.js.map +1 -0
- package/dist/search/fts.d.ts +77 -0
- package/dist/search/fts.d.ts.map +1 -0
- package/dist/search/fts.js +223 -0
- package/dist/search/fts.js.map +1 -0
- package/dist/search/fused.d.ts +110 -0
- package/dist/search/fused.d.ts.map +1 -0
- package/dist/search/fused.js +323 -0
- package/dist/search/fused.js.map +1 -0
- package/dist/search/potential-matches.d.ts +88 -0
- package/dist/search/potential-matches.d.ts.map +1 -0
- package/dist/search/potential-matches.js +83 -0
- package/dist/search/potential-matches.js.map +1 -0
- package/dist/search/scope.d.ts +66 -0
- package/dist/search/scope.d.ts.map +1 -0
- package/dist/search/scope.js +71 -0
- package/dist/search/scope.js.map +1 -0
- package/dist/store/config.d.ts +57 -0
- package/dist/store/config.d.ts.map +1 -0
- package/dist/store/config.js +208 -0
- package/dist/store/config.js.map +1 -0
- package/dist/store/lock.d.ts +101 -0
- package/dist/store/lock.d.ts.map +1 -0
- package/dist/store/lock.js +182 -0
- package/dist/store/lock.js.map +1 -0
- package/dist/store/lockMetrics.d.ts +52 -0
- package/dist/store/lockMetrics.d.ts.map +1 -0
- package/dist/store/lockMetrics.js +29 -0
- package/dist/store/lockMetrics.js.map +1 -0
- package/dist/store/sqliteChunkStore.d.ts +44 -0
- package/dist/store/sqliteChunkStore.d.ts.map +1 -0
- package/dist/store/sqliteChunkStore.js +135 -0
- package/dist/store/sqliteChunkStore.js.map +1 -0
- package/dist/telemetry/lockMetricsSummary.d.ts +42 -0
- package/dist/telemetry/lockMetricsSummary.d.ts.map +1 -0
- package/dist/telemetry/lockMetricsSummary.js +139 -0
- package/dist/telemetry/lockMetricsSummary.js.map +1 -0
- package/dist/telemetry/metrics.d.ts +140 -0
- package/dist/telemetry/metrics.d.ts.map +1 -0
- package/dist/telemetry/metrics.js +312 -0
- package/dist/telemetry/metrics.js.map +1 -0
- package/dist/telemetry/tokenizer.d.ts +125 -0
- package/dist/telemetry/tokenizer.d.ts.map +1 -0
- package/dist/telemetry/tokenizer.js +174 -0
- package/dist/telemetry/tokenizer.js.map +1 -0
- package/package.json +81 -0
|
@@ -0,0 +1,1439 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { extname } from 'node:path';
|
|
3
|
+
import { parseSource } from '../parser.js';
|
|
4
|
+
import { LocalTypeEnvironment } from '../../graph/local-type-env.js';
|
|
5
|
+
import { getImportResolver } from '../../indexer/import-resolver.js';
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// TypeScriptExtractor
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
/**
|
|
10
|
+
* TypeScript/JavaScript chunk extractor.
|
|
11
|
+
*
|
|
12
|
+
* Implements §10.1 chunking strategy and §10.2 signature extraction.
|
|
13
|
+
* Handles class decomposition (shell + method chunks) and the two-pass
|
|
14
|
+
* walk for `is_exported` detection. tree-sitter parsing is internal —
|
|
15
|
+
* callers only see the `LanguageExtractor` contract.
|
|
16
|
+
*/
|
|
17
|
+
export class TypeScriptExtractor {
|
|
18
|
+
language = 'typescript';
|
|
19
|
+
extensions = ['.ts', '.tsx', '.js', '.jsx'];
|
|
20
|
+
extract(src, filePath, fileMtime, options) {
|
|
21
|
+
const extension = extname(filePath);
|
|
22
|
+
const tree = parseSource(src, extension);
|
|
23
|
+
const rawChunks = this.extractChunks(tree, src, filePath, fileMtime, options.contextLines, options.chunkSplitThreshold);
|
|
24
|
+
// extractChunks stamps the extension-derived language on each chunk, but
|
|
25
|
+
// the extractor's own `language` is always 'typescript' — derive the
|
|
26
|
+
// file-level value from the extension so `.js`/`.jsx` filters work.
|
|
27
|
+
const language = languageFromExt(extension);
|
|
28
|
+
const chunks = rawChunks;
|
|
29
|
+
// Resolve each import specifier to a real indexed file (§13.7): relative
|
|
30
|
+
// probing, tsconfig aliases, workspace packages, symlink realpath.
|
|
31
|
+
const resolver = getImportResolver(options.projectRoot);
|
|
32
|
+
const imports = extractImports(tree, filePath).map((imp) => {
|
|
33
|
+
const r = resolver.resolve(imp.module, filePath);
|
|
34
|
+
return { module: imp.module, symbols: imp.symbols, isExternal: r.isExternal, resolvedPath: r.resolvedPath };
|
|
35
|
+
});
|
|
36
|
+
// Re-exports (§10.1): named ones become exported marker symbols (kind
|
|
37
|
+
// 'export') plus RE_EXPORTS edges so rename impact can find barrels; star
|
|
38
|
+
// ones become file-level re_export_files records (no per-symbol identity).
|
|
39
|
+
const reExports = extractReExports(tree);
|
|
40
|
+
const markerSymbols = reExports.named.map((r) => ({
|
|
41
|
+
name: r.exportedName,
|
|
42
|
+
kind: 'export',
|
|
43
|
+
line: r.line,
|
|
44
|
+
isExported: true,
|
|
45
|
+
declarationHash: null,
|
|
46
|
+
bodyHash: null,
|
|
47
|
+
}));
|
|
48
|
+
const reExportEdges = reExports.named.map((r) => ({
|
|
49
|
+
fromName: r.exportedName,
|
|
50
|
+
toName: r.sourceName,
|
|
51
|
+
edgeType: 'RE_EXPORTS',
|
|
52
|
+
// Same resolver call already made for imports/starReExports above — the
|
|
53
|
+
// file evidence `insertEdges` needs to scope this edge to the module the
|
|
54
|
+
// re-export actually names (Task 0 fix, see EdgeRecord.toResolvedPath).
|
|
55
|
+
toResolvedPath: resolver.resolve(r.module, filePath).resolvedPath,
|
|
56
|
+
}));
|
|
57
|
+
const starReExports = reExports.stars.map((s) => ({
|
|
58
|
+
module: s.module,
|
|
59
|
+
resolvedPath: resolver.resolve(s.module, filePath).resolvedPath,
|
|
60
|
+
line: s.line,
|
|
61
|
+
}));
|
|
62
|
+
const symbols = [...symbolsFromChunks(chunks), ...markerSymbols];
|
|
63
|
+
const edges = [...extractEdges(tree, filePath, src), ...reExportEdges];
|
|
64
|
+
// F5 (Stage 3): qualified compounds ("Class.method") for identifier_fts.
|
|
65
|
+
// `searchIdentifiers` phrase-quotes its query term, and identifier_fts'
|
|
66
|
+
// unicode61 tokenizer treats '.' as a separator, so a query for a
|
|
67
|
+
// qualified method name only matches a row whose identifiers column has
|
|
68
|
+
// the class/method tokens ADJACENT — which the bare-identifier bag below
|
|
69
|
+
// essentially never produces (the chunk text rarely repeats "Class"
|
|
70
|
+
// immediately followed by "method"). Deriving the compounds from `edges`
|
|
71
|
+
// (rather than re-walking the AST) rides the SAME resolution already
|
|
72
|
+
// computed by `extractEdges`/`LocalTypeEnvironment` — no parallel
|
|
73
|
+
// mechanism, and a receiver `extractEdges` could not statically link
|
|
74
|
+
// (DI/factory calls, §10.3.1's documented gap) correctly contributes
|
|
75
|
+
// nothing here either, since no POTENTIAL_CALL edge exists for it.
|
|
76
|
+
const qualifiedMentionsByFromName = new Map();
|
|
77
|
+
for (const e of edges) {
|
|
78
|
+
// `toName` is qualified ("Type.method") only for a receiver-based call
|
|
79
|
+
// (LocalTypeEnvironment.resolveCall's `${binding.type}.${method}` path)
|
|
80
|
+
// — a bare call (import/same_file resolution) has no receiver and
|
|
81
|
+
// therefore no dot. PARENT_OF/EXTENDS/etc. use a different fromName
|
|
82
|
+
// scope (the class itself, not a method body) and must not leak in.
|
|
83
|
+
if (e.edgeType !== 'POTENTIAL_CALL' || !e.toName.includes('.'))
|
|
84
|
+
continue;
|
|
85
|
+
const mentions = qualifiedMentionsByFromName.get(e.fromName);
|
|
86
|
+
if (mentions === undefined)
|
|
87
|
+
qualifiedMentionsByFromName.set(e.fromName, [e.toName]);
|
|
88
|
+
else
|
|
89
|
+
mentions.push(e.toName);
|
|
90
|
+
}
|
|
91
|
+
const identifierRows = chunks.flatMap((chunk) => {
|
|
92
|
+
const base = extractIdentifiers(chunk.content);
|
|
93
|
+
// Declaration self-discoverability: a method chunk's own qualified
|
|
94
|
+
// name (constructor/getter/setter forms are already qualified, since
|
|
95
|
+
// `symbol_name` is always `${className}.${methodName}` for chunk_type
|
|
96
|
+
// 'method' — see the `chunks.push` call above).
|
|
97
|
+
const ownQualified = chunk.chunk_type === 'method' && chunk.symbol_name !== null
|
|
98
|
+
? [chunk.symbol_name]
|
|
99
|
+
: [];
|
|
100
|
+
const mentionQualified = chunk.symbol_name !== null
|
|
101
|
+
? qualifiedMentionsByFromName.get(chunk.symbol_name) ?? []
|
|
102
|
+
: [];
|
|
103
|
+
const identifiers = appendQualifiedCompounds(base, [...ownQualified, ...mentionQualified]);
|
|
104
|
+
// ONE row per chunk, in chunk order, even when the bag is empty. The empty
|
|
105
|
+
// ones are dropped in `extractFile` AFTER `remapIdentifierRows` has used the
|
|
106
|
+
// positional correspondence to re-key them (D040) — dropping them here is what
|
|
107
|
+
// made the correspondence partial, and a partial correspondence cannot be
|
|
108
|
+
// reconstructed from `chunk_id` alone once two chunks share one.
|
|
109
|
+
return [{ chunk_id: chunk.chunk_id, identifiers }];
|
|
110
|
+
});
|
|
111
|
+
return { language, chunks, symbols, imports, edges, identifierRows, starReExports };
|
|
112
|
+
}
|
|
113
|
+
extractChunks(parsedTree, src, filePath, fileMtime, contextLines, chunkSplitThreshold) {
|
|
114
|
+
const lines = src.split('\n');
|
|
115
|
+
const lang = languageFromExt(extname(filePath));
|
|
116
|
+
const topLevel = nodeChildren(parsedTree.rootNode);
|
|
117
|
+
// -------------------------------------------------------------------------
|
|
118
|
+
// Pass 1: build symbol-name → exported flag map.
|
|
119
|
+
// `export function foo()` / `export class Foo` → name recorded as exported.
|
|
120
|
+
// Bare declarations → name recorded as not-yet-exported (may be updated pass 2).
|
|
121
|
+
// -------------------------------------------------------------------------
|
|
122
|
+
const exportedNames = new Set();
|
|
123
|
+
const knownDeclNames = new Set();
|
|
124
|
+
for (const node of topLevel) {
|
|
125
|
+
if (nodeType(node) === 'export_statement') {
|
|
126
|
+
const decl = getWrappedDeclaration(node);
|
|
127
|
+
if (decl !== null) {
|
|
128
|
+
const name = getDeclName(decl);
|
|
129
|
+
if (name !== null) {
|
|
130
|
+
exportedNames.add(name);
|
|
131
|
+
knownDeclNames.add(name);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
const name = getDeclName(node);
|
|
137
|
+
if (name !== null)
|
|
138
|
+
knownDeclNames.add(name);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// -------------------------------------------------------------------------
|
|
142
|
+
// Pass 2: update is_exported via `export { foo }` (no `from` clause).
|
|
143
|
+
// -------------------------------------------------------------------------
|
|
144
|
+
for (const node of topLevel) {
|
|
145
|
+
if (nodeType(node) !== 'export_statement')
|
|
146
|
+
continue;
|
|
147
|
+
if (hasFromClause(node))
|
|
148
|
+
continue;
|
|
149
|
+
if (getWrappedDeclaration(node) !== null)
|
|
150
|
+
continue;
|
|
151
|
+
const exportClause = findChildByType(node, 'export_clause');
|
|
152
|
+
if (exportClause === null)
|
|
153
|
+
continue;
|
|
154
|
+
for (const child of nodeNamedChildren(exportClause)) {
|
|
155
|
+
if (nodeType(child) !== 'export_specifier')
|
|
156
|
+
continue;
|
|
157
|
+
const localName = child.childForFieldName('name')?.text;
|
|
158
|
+
const alias = child.childForFieldName('alias')?.text;
|
|
159
|
+
// `export { foo as bar }` exports `foo` UNDER `bar`, not as `foo` — the
|
|
160
|
+
// alias is emitted as its own exported chunk in pass 3, so the local
|
|
161
|
+
// name is not marked exported here.
|
|
162
|
+
if (alias !== undefined && alias !== localName)
|
|
163
|
+
continue;
|
|
164
|
+
if (localName !== undefined && knownDeclNames.has(localName)) {
|
|
165
|
+
exportedNames.add(localName);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// -------------------------------------------------------------------------
|
|
170
|
+
// Emit chunks
|
|
171
|
+
// -------------------------------------------------------------------------
|
|
172
|
+
const chunks = [];
|
|
173
|
+
for (const node of topLevel) {
|
|
174
|
+
const t = nodeType(node);
|
|
175
|
+
if (t === 'comment' || t === 'import_statement')
|
|
176
|
+
continue;
|
|
177
|
+
let declNode = node;
|
|
178
|
+
let isExported = false;
|
|
179
|
+
if (t === 'export_statement') {
|
|
180
|
+
const decl = getWrappedDeclaration(node);
|
|
181
|
+
if (decl === null)
|
|
182
|
+
continue; // export { } or export * from — no direct chunk
|
|
183
|
+
declNode = decl;
|
|
184
|
+
isExported = true;
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
const name = getDeclName(node);
|
|
188
|
+
isExported = name !== null && exportedNames.has(name);
|
|
189
|
+
}
|
|
190
|
+
emitChunksForNode(declNode, isExported, chunks, lines, src, filePath, fileMtime, contextLines, chunkSplitThreshold, lang);
|
|
191
|
+
}
|
|
192
|
+
// -------------------------------------------------------------------------
|
|
193
|
+
// Pass 3: local re-export aliases — `export { foo as bar }` exposes foo's
|
|
194
|
+
// declaration under the name `bar`. Emit a chunk for `bar` mirroring foo's
|
|
195
|
+
// so it is discoverable (mast_exports / mast_search / mast_signature).
|
|
196
|
+
// -------------------------------------------------------------------------
|
|
197
|
+
for (const { local, alias, line } of localExportAliases(topLevel)) {
|
|
198
|
+
const target = chunks.find((c) => c.symbol_name === local && c.chunk_type !== 'method');
|
|
199
|
+
if (target === undefined)
|
|
200
|
+
continue;
|
|
201
|
+
chunks.push({
|
|
202
|
+
...target,
|
|
203
|
+
chunk_id: sha256(`${filePath}:${line}:${alias}`),
|
|
204
|
+
symbol_name: alias,
|
|
205
|
+
is_exported: true,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
return chunks;
|
|
209
|
+
}
|
|
210
|
+
declarationHash(node, src) {
|
|
211
|
+
return declHashOf(node, src);
|
|
212
|
+
}
|
|
213
|
+
bodyHash(node, src) {
|
|
214
|
+
return bodyHashOf(node, src);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
// Chunk emission
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
function emitChunksForNode(node, isExported, chunks, lines, src, filePath, fileMtime, contextLines, chunkSplitThreshold, language) {
|
|
221
|
+
const t = nodeType(node);
|
|
222
|
+
const startLine = nodeStartLine(node);
|
|
223
|
+
const endLine = nodeEndLine(node);
|
|
224
|
+
switch (t) {
|
|
225
|
+
case 'function_declaration':
|
|
226
|
+
case 'generator_function_declaration': {
|
|
227
|
+
const name = node.childForFieldName('name')?.text ?? null;
|
|
228
|
+
pushChunks(chunks, {
|
|
229
|
+
chunkType: 'function',
|
|
230
|
+
symbolName: name,
|
|
231
|
+
parentSymbol: null,
|
|
232
|
+
isExported,
|
|
233
|
+
startLine,
|
|
234
|
+
endLine,
|
|
235
|
+
lines,
|
|
236
|
+
filePath,
|
|
237
|
+
fileMtime,
|
|
238
|
+
contextLines,
|
|
239
|
+
chunkSplitThreshold,
|
|
240
|
+
language,
|
|
241
|
+
declarationHash: declHashOf(node, src),
|
|
242
|
+
bodyHash: bodyHashOf(node, src),
|
|
243
|
+
});
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
case 'lexical_declaration':
|
|
247
|
+
case 'variable_declaration': {
|
|
248
|
+
const declarator = findChildByType(node, 'variable_declarator');
|
|
249
|
+
if (declarator === null)
|
|
250
|
+
break;
|
|
251
|
+
const valNode = declarator.childForFieldName('value');
|
|
252
|
+
const isFunc = valNode !== null &&
|
|
253
|
+
(nodeType(valNode) === 'arrow_function' ||
|
|
254
|
+
nodeType(valNode) === 'function' ||
|
|
255
|
+
nodeType(valNode) === 'generator_function');
|
|
256
|
+
const name = declarator.childForFieldName('name')?.text ?? null;
|
|
257
|
+
pushChunks(chunks, {
|
|
258
|
+
chunkType: isFunc ? 'function' : 'block',
|
|
259
|
+
symbolName: name,
|
|
260
|
+
parentSymbol: null,
|
|
261
|
+
isExported,
|
|
262
|
+
startLine,
|
|
263
|
+
endLine,
|
|
264
|
+
lines,
|
|
265
|
+
filePath,
|
|
266
|
+
fileMtime,
|
|
267
|
+
contextLines,
|
|
268
|
+
chunkSplitThreshold,
|
|
269
|
+
language,
|
|
270
|
+
declarationHash: declHashOf(node, src),
|
|
271
|
+
bodyHash: bodyHashOf(node, src),
|
|
272
|
+
});
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
case 'class_declaration':
|
|
276
|
+
case 'abstract_class_declaration': {
|
|
277
|
+
const className = node.childForFieldName('name')?.text ?? null;
|
|
278
|
+
const bodyNode = node.childForFieldName('body') ?? findChildByType(node, 'class_body');
|
|
279
|
+
// Synthesized class_shell chunk
|
|
280
|
+
const shellContent = synthesiseClassShell(node, bodyNode, src);
|
|
281
|
+
chunks.push({
|
|
282
|
+
chunk_id: chunkId(filePath, startLine),
|
|
283
|
+
file_path: filePath,
|
|
284
|
+
start_line: startLine,
|
|
285
|
+
end_line: endLine,
|
|
286
|
+
content: shellContent,
|
|
287
|
+
chunk_type: 'class_shell',
|
|
288
|
+
symbol_name: className,
|
|
289
|
+
parent_symbol: null,
|
|
290
|
+
is_exported: isExported,
|
|
291
|
+
language,
|
|
292
|
+
file_mtime: fileMtime,
|
|
293
|
+
declaration_hash: declHashOf(node, src),
|
|
294
|
+
body_hash: bodyNode !== null ? classShellBodyHashOf(bodyNode, src) : sha256(''),
|
|
295
|
+
});
|
|
296
|
+
// Method chunks
|
|
297
|
+
if (bodyNode !== null && className !== null) {
|
|
298
|
+
for (const member of nodeNamedChildren(bodyNode)) {
|
|
299
|
+
const mt = nodeType(member);
|
|
300
|
+
if (mt !== 'method_definition' && mt !== 'abstract_method_signature')
|
|
301
|
+
continue;
|
|
302
|
+
const methodName = member.childForFieldName('name')?.text ?? null;
|
|
303
|
+
if (methodName === null)
|
|
304
|
+
continue;
|
|
305
|
+
const isPrivate = hasPrivateModifier(member);
|
|
306
|
+
const methodExported = isExported && !isPrivate;
|
|
307
|
+
const mStart = nodeStartLine(member);
|
|
308
|
+
const mEnd = nodeEndLine(member);
|
|
309
|
+
const content = expandContent(lines, mStart, mEnd, contextLines);
|
|
310
|
+
chunks.push({
|
|
311
|
+
chunk_id: chunkId(filePath, mStart),
|
|
312
|
+
file_path: filePath,
|
|
313
|
+
start_line: mStart,
|
|
314
|
+
end_line: mEnd,
|
|
315
|
+
content,
|
|
316
|
+
chunk_type: 'method',
|
|
317
|
+
symbol_name: `${className}.${methodName}`,
|
|
318
|
+
parent_symbol: className,
|
|
319
|
+
is_exported: methodExported,
|
|
320
|
+
language,
|
|
321
|
+
file_mtime: fileMtime,
|
|
322
|
+
declaration_hash: declHashOf(member, src),
|
|
323
|
+
body_hash: bodyHashOf(member, src),
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
case 'interface_declaration': {
|
|
330
|
+
const name = node.childForFieldName('name')?.text ?? null;
|
|
331
|
+
pushChunks(chunks, {
|
|
332
|
+
chunkType: 'interface',
|
|
333
|
+
symbolName: name,
|
|
334
|
+
parentSymbol: null,
|
|
335
|
+
isExported,
|
|
336
|
+
startLine,
|
|
337
|
+
endLine,
|
|
338
|
+
lines,
|
|
339
|
+
filePath,
|
|
340
|
+
fileMtime,
|
|
341
|
+
contextLines,
|
|
342
|
+
chunkSplitThreshold,
|
|
343
|
+
language,
|
|
344
|
+
declarationHash: declHashOf(node, src),
|
|
345
|
+
bodyHash: bodyHashOf(node, src),
|
|
346
|
+
});
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
case 'type_alias_declaration': {
|
|
350
|
+
const name = node.childForFieldName('name')?.text ?? null;
|
|
351
|
+
pushChunks(chunks, {
|
|
352
|
+
chunkType: 'type',
|
|
353
|
+
symbolName: name,
|
|
354
|
+
parentSymbol: null,
|
|
355
|
+
isExported,
|
|
356
|
+
startLine,
|
|
357
|
+
endLine,
|
|
358
|
+
lines,
|
|
359
|
+
filePath,
|
|
360
|
+
fileMtime,
|
|
361
|
+
contextLines,
|
|
362
|
+
chunkSplitThreshold,
|
|
363
|
+
language,
|
|
364
|
+
declarationHash: declHashOf(node, src),
|
|
365
|
+
bodyHash: bodyHashOf(node, src),
|
|
366
|
+
});
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
default: {
|
|
370
|
+
// Skip zero-content nodes. `declare module 'x';` parses as an
|
|
371
|
+
// `ambient_declaration` (the real content) PLUS a sibling
|
|
372
|
+
// `empty_statement` for the trailing `;` — both fall into this default
|
|
373
|
+
// branch, and `expandContent` then makes the `;` chunk byte-identical
|
|
374
|
+
// to the real declaration's, colliding on chunk_id (GITNEXUS_COMPARISON.md
|
|
375
|
+
// §15.3; measured: directus/app/src/shims.d.ts, 40 chunks/24 unique ids).
|
|
376
|
+
// Filtering here kills the collision at its source — a disambiguator
|
|
377
|
+
// alone would instead legitimize junk `;`-only chunks. The broader
|
|
378
|
+
// whitespace-only check catches the same failure mode for any other
|
|
379
|
+
// node type tree-sitter might emit with no real source text.
|
|
380
|
+
if (t === 'empty_statement' || src.slice(node.startIndex, node.endIndex).trim() === '') {
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
// Everything else at top level → block (skip if we have no meaningful content)
|
|
384
|
+
const name = getDeclName(node);
|
|
385
|
+
pushChunks(chunks, {
|
|
386
|
+
chunkType: 'block',
|
|
387
|
+
symbolName: name,
|
|
388
|
+
parentSymbol: null,
|
|
389
|
+
isExported,
|
|
390
|
+
startLine,
|
|
391
|
+
endLine,
|
|
392
|
+
lines,
|
|
393
|
+
filePath,
|
|
394
|
+
fileMtime,
|
|
395
|
+
contextLines,
|
|
396
|
+
chunkSplitThreshold,
|
|
397
|
+
language,
|
|
398
|
+
});
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Push one chunk — or N overlapping sub-chunks when the declaration exceeds
|
|
405
|
+
* `chunkSplitThreshold` lines (§10.1 split rule). The first sub-chunk always
|
|
406
|
+
* starts at the declaration header so signature extraction works from sub-chunk 0.
|
|
407
|
+
*/
|
|
408
|
+
function pushChunks(chunks, opts) {
|
|
409
|
+
const { startLine, endLine, filePath, fileMtime, lines, contextLines, language } = opts;
|
|
410
|
+
const lineCount = endLine - startLine + 1;
|
|
411
|
+
if (lineCount <= opts.chunkSplitThreshold) {
|
|
412
|
+
chunks.push({
|
|
413
|
+
chunk_id: chunkId(filePath, startLine),
|
|
414
|
+
file_path: filePath,
|
|
415
|
+
start_line: startLine,
|
|
416
|
+
end_line: endLine,
|
|
417
|
+
content: expandContent(lines, startLine, endLine, contextLines),
|
|
418
|
+
chunk_type: opts.chunkType,
|
|
419
|
+
symbol_name: opts.symbolName,
|
|
420
|
+
parent_symbol: opts.parentSymbol,
|
|
421
|
+
is_exported: opts.isExported,
|
|
422
|
+
language,
|
|
423
|
+
file_mtime: fileMtime,
|
|
424
|
+
declaration_hash: opts.declarationHash,
|
|
425
|
+
body_hash: opts.bodyHash,
|
|
426
|
+
});
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
// Split into overlapping sub-chunks with OVERLAP_LINES overlap.
|
|
430
|
+
const OVERLAP_LINES = 10;
|
|
431
|
+
const windowSize = opts.chunkSplitThreshold;
|
|
432
|
+
let subStart = startLine;
|
|
433
|
+
let subIndex = 0;
|
|
434
|
+
while (subStart <= endLine) {
|
|
435
|
+
const subEnd = Math.min(subStart + windowSize - 1, endLine);
|
|
436
|
+
chunks.push({
|
|
437
|
+
// Qualify sub-chunk IDs with sub-index to keep them unique.
|
|
438
|
+
chunk_id: sha256(`${filePath}:${startLine}:${subIndex}`),
|
|
439
|
+
file_path: filePath,
|
|
440
|
+
start_line: subStart,
|
|
441
|
+
end_line: subEnd,
|
|
442
|
+
content: expandContent(lines, subStart, subEnd, contextLines),
|
|
443
|
+
chunk_type: opts.chunkType,
|
|
444
|
+
symbol_name: opts.symbolName,
|
|
445
|
+
parent_symbol: opts.parentSymbol,
|
|
446
|
+
is_exported: opts.isExported,
|
|
447
|
+
language,
|
|
448
|
+
file_mtime: fileMtime,
|
|
449
|
+
declaration_hash: opts.declarationHash,
|
|
450
|
+
body_hash: opts.bodyHash,
|
|
451
|
+
});
|
|
452
|
+
if (subEnd >= endLine)
|
|
453
|
+
break;
|
|
454
|
+
subStart = subEnd - OVERLAP_LINES + 1;
|
|
455
|
+
subIndex++;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
// ---------------------------------------------------------------------------
|
|
459
|
+
// Class shell synthesis
|
|
460
|
+
// ---------------------------------------------------------------------------
|
|
461
|
+
/**
|
|
462
|
+
* Synthesise the class_shell content: class declaration header + member
|
|
463
|
+
* signatures (TSDoc + signature line, no bodies) + closing brace.
|
|
464
|
+
*/
|
|
465
|
+
export function synthesiseClassShell(classNode, bodyNode, src) {
|
|
466
|
+
// Header: class declaration up to (not including) the opening `{`
|
|
467
|
+
const header = bodyNode !== null
|
|
468
|
+
? src.slice(classNode.startIndex, bodyNode.startIndex).trimEnd()
|
|
469
|
+
: src.slice(classNode.startIndex, classNode.endIndex);
|
|
470
|
+
if (bodyNode === null)
|
|
471
|
+
return header;
|
|
472
|
+
const memberLines = [header + ' {'];
|
|
473
|
+
for (const member of nodeNamedChildren(bodyNode)) {
|
|
474
|
+
if (!isClassShellMember(nodeType(member)))
|
|
475
|
+
continue;
|
|
476
|
+
// Leading TSDoc/comment
|
|
477
|
+
const doc = getLeadingComment(member, bodyNode, src);
|
|
478
|
+
if (doc !== null)
|
|
479
|
+
memberLines.push(' ' + doc.trim());
|
|
480
|
+
// Signature without body
|
|
481
|
+
memberLines.push(' ' + extractSignatureText(member, src).trim());
|
|
482
|
+
}
|
|
483
|
+
memberLines.push('}');
|
|
484
|
+
return memberLines.join('\n');
|
|
485
|
+
}
|
|
486
|
+
// ---------------------------------------------------------------------------
|
|
487
|
+
// Signature / doc extraction
|
|
488
|
+
// ---------------------------------------------------------------------------
|
|
489
|
+
/**
|
|
490
|
+
* Extract declaration text up to (not including) the body block.
|
|
491
|
+
* For interfaces and type aliases the full node is the signature.
|
|
492
|
+
*/
|
|
493
|
+
export function extractSignatureText(node, src) {
|
|
494
|
+
const bodyNode = node.childForFieldName('body') ?? findChildByType(node, 'statement_block');
|
|
495
|
+
if (bodyNode === null) {
|
|
496
|
+
return src.slice(node.startIndex, node.endIndex);
|
|
497
|
+
}
|
|
498
|
+
return src.slice(node.startIndex, bodyNode.startIndex).trimEnd();
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Extract a body-free signature (plus structured params and return type) for
|
|
502
|
+
* every top-level symbol and class method in `tree`. Used by `mast_signature`
|
|
503
|
+
* and `mast_exports` so they report the declaration, not the function body.
|
|
504
|
+
*/
|
|
505
|
+
export function extractSignatures(tree, src) {
|
|
506
|
+
const out = [];
|
|
507
|
+
const root = tree.rootNode;
|
|
508
|
+
for (const node of nodeChildren(root)) {
|
|
509
|
+
const isExport = nodeType(node) === 'export_statement';
|
|
510
|
+
const decl = isExport ? getWrappedDeclaration(node) : node;
|
|
511
|
+
if (decl === null)
|
|
512
|
+
continue;
|
|
513
|
+
// Doc precedes the export wrapper when there is one.
|
|
514
|
+
const docHost = isExport ? node : decl;
|
|
515
|
+
const t = nodeType(decl);
|
|
516
|
+
switch (t) {
|
|
517
|
+
case 'function_declaration':
|
|
518
|
+
case 'generator_function_declaration': {
|
|
519
|
+
const name = decl.childForFieldName('name')?.text;
|
|
520
|
+
if (name !== undefined)
|
|
521
|
+
out.push(signatureFor(name, decl, decl, root, src));
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
case 'lexical_declaration':
|
|
525
|
+
case 'variable_declaration': {
|
|
526
|
+
const declarator = findChildByType(decl, 'variable_declarator');
|
|
527
|
+
const value = declarator?.childForFieldName('value') ?? null;
|
|
528
|
+
const name = declarator?.childForFieldName('name')?.text ?? null;
|
|
529
|
+
if (name !== null && value !== null && nodeType(value) === 'arrow_function') {
|
|
530
|
+
out.push(arrowSignatureFor(name, decl, value, docHost, root, src));
|
|
531
|
+
}
|
|
532
|
+
break;
|
|
533
|
+
}
|
|
534
|
+
case 'interface_declaration':
|
|
535
|
+
case 'type_alias_declaration': {
|
|
536
|
+
const name = decl.childForFieldName('name')?.text;
|
|
537
|
+
if (name !== undefined) {
|
|
538
|
+
out.push({
|
|
539
|
+
name,
|
|
540
|
+
line: nodeStartLine(decl),
|
|
541
|
+
signature: extractSignatureText(decl, src),
|
|
542
|
+
params: [],
|
|
543
|
+
returnType: null,
|
|
544
|
+
doc: getLeadingComment(docHost, root, src),
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
break;
|
|
548
|
+
}
|
|
549
|
+
case 'class_declaration':
|
|
550
|
+
case 'abstract_class_declaration': {
|
|
551
|
+
const className = decl.childForFieldName('name')?.text;
|
|
552
|
+
if (className === undefined)
|
|
553
|
+
break;
|
|
554
|
+
out.push({
|
|
555
|
+
name: className,
|
|
556
|
+
line: nodeStartLine(decl),
|
|
557
|
+
signature: extractSignatureText(decl, src),
|
|
558
|
+
params: [],
|
|
559
|
+
returnType: null,
|
|
560
|
+
doc: getLeadingComment(docHost, root, src),
|
|
561
|
+
});
|
|
562
|
+
const body = decl.childForFieldName('body') ?? findChildByType(decl, 'class_body');
|
|
563
|
+
if (body !== null) {
|
|
564
|
+
for (const member of nodeNamedChildren(body)) {
|
|
565
|
+
const mt = nodeType(member);
|
|
566
|
+
if (mt !== 'method_definition' && mt !== 'abstract_method_signature')
|
|
567
|
+
continue;
|
|
568
|
+
const mName = member.childForFieldName('name')?.text;
|
|
569
|
+
if (mName !== undefined)
|
|
570
|
+
out.push(signatureFor(`${className}.${mName}`, member, member, body, src));
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
break;
|
|
574
|
+
}
|
|
575
|
+
default:
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
// Local re-export aliases (`export { foo as bar }`) expose foo's signature
|
|
580
|
+
// under `bar`, so mast_signature/mast_exports can look it up by export name.
|
|
581
|
+
for (const { local, alias } of localExportAliases(nodeChildren(root))) {
|
|
582
|
+
const base = out.find((s) => s.name === local);
|
|
583
|
+
if (base !== undefined)
|
|
584
|
+
out.push({ ...base, name: alias });
|
|
585
|
+
}
|
|
586
|
+
return out;
|
|
587
|
+
}
|
|
588
|
+
/** Signature info for a function/method node. */
|
|
589
|
+
function signatureFor(name, node, docHost, docParent, src) {
|
|
590
|
+
return {
|
|
591
|
+
name,
|
|
592
|
+
line: nodeStartLine(node),
|
|
593
|
+
signature: extractSignatureText(node, src),
|
|
594
|
+
params: paramsOf(node, src),
|
|
595
|
+
returnType: returnTypeOf(node, src),
|
|
596
|
+
doc: getLeadingComment(docHost, docParent, src),
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
/** Signature info for an arrow-function const (`export const f = (...) => ...`). */
|
|
600
|
+
function arrowSignatureFor(name, declNode, arrow, docHost, docParent, src) {
|
|
601
|
+
const body = arrow.childForFieldName('body');
|
|
602
|
+
// Declaration text up to the arrow body, with a trailing `=>` trimmed.
|
|
603
|
+
const raw = body !== null
|
|
604
|
+
? src.slice(declNode.startIndex, body.startIndex).trimEnd()
|
|
605
|
+
: src.slice(declNode.startIndex, declNode.endIndex);
|
|
606
|
+
return {
|
|
607
|
+
name,
|
|
608
|
+
line: nodeStartLine(declNode),
|
|
609
|
+
signature: raw.replace(/=>\s*$/, '').trimEnd(),
|
|
610
|
+
params: paramsOf(arrow, src),
|
|
611
|
+
returnType: returnTypeOf(arrow, src),
|
|
612
|
+
doc: getLeadingComment(docHost, docParent, src),
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
/** Structured parameters from a node's `formal_parameters`. */
|
|
616
|
+
function paramsOf(node, src) {
|
|
617
|
+
const fp = node.childForFieldName('parameters') ?? findChildByType(node, 'formal_parameters');
|
|
618
|
+
if (fp === null)
|
|
619
|
+
return [];
|
|
620
|
+
const params = [];
|
|
621
|
+
for (const p of nodeNamedChildren(fp)) {
|
|
622
|
+
const pt = nodeType(p);
|
|
623
|
+
if (pt !== 'required_parameter' && pt !== 'optional_parameter')
|
|
624
|
+
continue;
|
|
625
|
+
const nameNode = p.childForFieldName('pattern') ?? findChildByType(p, 'identifier');
|
|
626
|
+
const name = nameNode !== null ? src.slice(nameNode.startIndex, nameNode.endIndex) : '';
|
|
627
|
+
params.push({ name, type: typeAnnotationText(p, src) ?? '' });
|
|
628
|
+
}
|
|
629
|
+
return params;
|
|
630
|
+
}
|
|
631
|
+
/** Return-type text from a node's `return_type` annotation, or null. */
|
|
632
|
+
function returnTypeOf(node, src) {
|
|
633
|
+
const rt = node.childForFieldName('return_type');
|
|
634
|
+
if (rt === null)
|
|
635
|
+
return null;
|
|
636
|
+
const typeNode = nodeNamedChildren(rt)[0];
|
|
637
|
+
return typeNode !== undefined ? src.slice(typeNode.startIndex, typeNode.endIndex) : null;
|
|
638
|
+
}
|
|
639
|
+
/** Full type text from a node's `type_annotation` child (e.g. `Promise<void>`). */
|
|
640
|
+
function typeAnnotationText(node, src) {
|
|
641
|
+
const ann = findChildByType(node, 'type_annotation');
|
|
642
|
+
if (ann === null)
|
|
643
|
+
return null;
|
|
644
|
+
const typeNode = nodeNamedChildren(ann)[0];
|
|
645
|
+
return typeNode !== undefined ? src.slice(typeNode.startIndex, typeNode.endIndex) : null;
|
|
646
|
+
}
|
|
647
|
+
// ---------------------------------------------------------------------------
|
|
648
|
+
// Stability hashes (§7.1) — computed from the AST, never from raw chunk text
|
|
649
|
+
// ---------------------------------------------------------------------------
|
|
650
|
+
/** sha256 of a declaration's signature (everything up to, not including, its body). */
|
|
651
|
+
function declHashOf(node, src) {
|
|
652
|
+
return sha256(extractSignatureText(node, src));
|
|
653
|
+
}
|
|
654
|
+
/** sha256 of a declaration's body block; empty-body hash when there is none. */
|
|
655
|
+
function bodyHashOf(node, src) {
|
|
656
|
+
const bodyNode = node.childForFieldName('body') ?? findChildByType(node, 'statement_block');
|
|
657
|
+
if (bodyNode === null)
|
|
658
|
+
return sha256('');
|
|
659
|
+
return sha256(src.slice(bodyNode.startIndex, bodyNode.endIndex));
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Body hash for a `class_shell`: sha256 of the sorted member signatures (each
|
|
663
|
+
* with its leading doc), with NO method bodies (§10.1). This makes the shell
|
|
664
|
+
* re-embed when a method is renamed/added/removed but stay stable when only a
|
|
665
|
+
* method body changes — that change is captured by the method chunk's own hash.
|
|
666
|
+
*/
|
|
667
|
+
/**
|
|
668
|
+
* Class-body members whose signatures make up the `class_shell` outline (§10.1).
|
|
669
|
+
* Shared by `synthesiseClassShell` (the outline text) and `classShellBodyHashOf`
|
|
670
|
+
* (its body hash) so the two cannot disagree on what counts as a member.
|
|
671
|
+
* `property_signature` covers ambient/`declare` class fields; `readonly_type`
|
|
672
|
+
* (a type-level node, not a member) is intentionally excluded.
|
|
673
|
+
*/
|
|
674
|
+
function isClassShellMember(memberType) {
|
|
675
|
+
return (memberType === 'method_definition' ||
|
|
676
|
+
memberType === 'abstract_method_signature' ||
|
|
677
|
+
memberType === 'public_field_definition' ||
|
|
678
|
+
memberType === 'property_signature');
|
|
679
|
+
}
|
|
680
|
+
function classShellBodyHashOf(bodyNode, src) {
|
|
681
|
+
const sigs = [];
|
|
682
|
+
for (const member of nodeNamedChildren(bodyNode)) {
|
|
683
|
+
if (!isClassShellMember(nodeType(member)))
|
|
684
|
+
continue;
|
|
685
|
+
const doc = getLeadingComment(member, bodyNode, src) ?? '';
|
|
686
|
+
sigs.push((doc + '\n' + extractSignatureText(member, src)).trim());
|
|
687
|
+
}
|
|
688
|
+
sigs.sort();
|
|
689
|
+
return sha256(sigs.join('\n'));
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Find the immediately preceding TSDoc or line comment for a node.
|
|
693
|
+
* Returns the comment text, or null if none found.
|
|
694
|
+
*/
|
|
695
|
+
function getLeadingComment(node, parentNode, src) {
|
|
696
|
+
const siblings = nodeChildren(parentNode);
|
|
697
|
+
const nodeStart = node.startIndex;
|
|
698
|
+
// Walk backwards from the node's position to find an adjacent comment.
|
|
699
|
+
let precedingComment = null;
|
|
700
|
+
for (const sibling of siblings) {
|
|
701
|
+
if (sibling.endIndex > nodeStart)
|
|
702
|
+
break;
|
|
703
|
+
if (nodeType(sibling) === 'comment') {
|
|
704
|
+
precedingComment = sibling;
|
|
705
|
+
}
|
|
706
|
+
else {
|
|
707
|
+
// Reset if a non-comment node appears between the comment and our node.
|
|
708
|
+
precedingComment = null;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
if (precedingComment === null)
|
|
712
|
+
return null;
|
|
713
|
+
return src.slice(precedingComment.startIndex, precedingComment.endIndex);
|
|
714
|
+
}
|
|
715
|
+
// ---------------------------------------------------------------------------
|
|
716
|
+
// Node helpers
|
|
717
|
+
// ---------------------------------------------------------------------------
|
|
718
|
+
function nodeType(node) {
|
|
719
|
+
return node.type;
|
|
720
|
+
}
|
|
721
|
+
function nodeStartLine(node) {
|
|
722
|
+
return node.startPosition.row + 1;
|
|
723
|
+
}
|
|
724
|
+
function nodeEndLine(node) {
|
|
725
|
+
return node.endPosition.row + 1;
|
|
726
|
+
}
|
|
727
|
+
function nodeChildren(node) {
|
|
728
|
+
return node.children;
|
|
729
|
+
}
|
|
730
|
+
function nodeNamedChildren(node) {
|
|
731
|
+
return node.namedChildren;
|
|
732
|
+
}
|
|
733
|
+
function findChildByType(node, type) {
|
|
734
|
+
for (const child of nodeChildren(node)) {
|
|
735
|
+
if (nodeType(child) === type)
|
|
736
|
+
return child;
|
|
737
|
+
}
|
|
738
|
+
return null;
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Return the wrapped declaration node for `export function foo(){}` style.
|
|
742
|
+
* Returns null for `export { foo }` and `export * from '...'` forms.
|
|
743
|
+
*/
|
|
744
|
+
function getWrappedDeclaration(exportStmtNode) {
|
|
745
|
+
const fieldDecl = exportStmtNode.childForFieldName('declaration');
|
|
746
|
+
if (fieldDecl !== null)
|
|
747
|
+
return fieldDecl;
|
|
748
|
+
// Fallback: look for a named child that is a declaration-type node
|
|
749
|
+
for (const child of nodeNamedChildren(exportStmtNode)) {
|
|
750
|
+
const t = nodeType(child);
|
|
751
|
+
if (t === 'function_declaration' ||
|
|
752
|
+
t === 'generator_function_declaration' ||
|
|
753
|
+
t === 'class_declaration' ||
|
|
754
|
+
t === 'abstract_class_declaration' ||
|
|
755
|
+
t === 'interface_declaration' ||
|
|
756
|
+
t === 'type_alias_declaration' ||
|
|
757
|
+
t === 'lexical_declaration' ||
|
|
758
|
+
t === 'variable_declaration' ||
|
|
759
|
+
t === 'enum_declaration') {
|
|
760
|
+
return child;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
return null;
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Local re-export aliases: `export { foo as bar }` (no `from` clause) pairs
|
|
767
|
+
* each local name with the exported alias. Used to expose the aliased
|
|
768
|
+
* declaration under its export name in both chunks and signatures.
|
|
769
|
+
*/
|
|
770
|
+
function localExportAliases(topLevel) {
|
|
771
|
+
const aliases = [];
|
|
772
|
+
for (const node of topLevel) {
|
|
773
|
+
if (nodeType(node) !== 'export_statement')
|
|
774
|
+
continue;
|
|
775
|
+
if (hasFromClause(node))
|
|
776
|
+
continue;
|
|
777
|
+
if (getWrappedDeclaration(node) !== null)
|
|
778
|
+
continue;
|
|
779
|
+
const clause = findChildByType(node, 'export_clause');
|
|
780
|
+
if (clause === null)
|
|
781
|
+
continue;
|
|
782
|
+
for (const spec of nodeNamedChildren(clause)) {
|
|
783
|
+
if (nodeType(spec) !== 'export_specifier')
|
|
784
|
+
continue;
|
|
785
|
+
const local = spec.childForFieldName('name')?.text;
|
|
786
|
+
const alias = spec.childForFieldName('alias')?.text;
|
|
787
|
+
if (local !== undefined && alias !== undefined && alias !== local) {
|
|
788
|
+
aliases.push({ local, alias, line: nodeStartLine(spec) });
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
return aliases;
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Return true if the export_statement has a `from` clause (re-exporting from another module).
|
|
796
|
+
*/
|
|
797
|
+
function hasFromClause(node) {
|
|
798
|
+
for (const child of nodeChildren(node)) {
|
|
799
|
+
if (nodeType(child) === 'string')
|
|
800
|
+
return true; // `from './module'` has a string child
|
|
801
|
+
}
|
|
802
|
+
return false;
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Extract `from`-clause re-exports from a file's top level (§10.1).
|
|
806
|
+
*
|
|
807
|
+
* - `export { Foo, Bar as Baz } from './x'` → named records (one per specifier).
|
|
808
|
+
* - `export * from './x'` (incl. `export * as ns from`) → star record; stars
|
|
809
|
+
* carry no per-symbol names, so they map to `re_export_files` rows rather
|
|
810
|
+
* than symbols/edges.
|
|
811
|
+
*
|
|
812
|
+
* Local aliases without `from` are NOT re-exports — they are handled by
|
|
813
|
+
* `localExportAliases`.
|
|
814
|
+
*/
|
|
815
|
+
export function extractReExports(parsedTree) {
|
|
816
|
+
const named = [];
|
|
817
|
+
const stars = [];
|
|
818
|
+
for (const node of nodeChildren(parsedTree.rootNode)) {
|
|
819
|
+
if (nodeType(node) !== 'export_statement')
|
|
820
|
+
continue;
|
|
821
|
+
if (!hasFromClause(node))
|
|
822
|
+
continue;
|
|
823
|
+
const moduleNode = findChildByType(node, 'string');
|
|
824
|
+
if (moduleNode === null)
|
|
825
|
+
continue;
|
|
826
|
+
const module = moduleNode.text.slice(1, -1);
|
|
827
|
+
const clause = findChildByType(node, 'export_clause');
|
|
828
|
+
if (clause === null) {
|
|
829
|
+
// No specifier list + a from clause = `export * from` / `export * as ns from`.
|
|
830
|
+
stars.push({ module, line: nodeStartLine(node) });
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
for (const spec of nodeNamedChildren(clause)) {
|
|
834
|
+
if (nodeType(spec) !== 'export_specifier')
|
|
835
|
+
continue;
|
|
836
|
+
const sourceName = spec.childForFieldName('name')?.text;
|
|
837
|
+
if (sourceName === undefined)
|
|
838
|
+
continue;
|
|
839
|
+
const alias = spec.childForFieldName('alias')?.text;
|
|
840
|
+
named.push({
|
|
841
|
+
exportedName: alias ?? sourceName,
|
|
842
|
+
sourceName,
|
|
843
|
+
line: nodeStartLine(spec),
|
|
844
|
+
module,
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
return { named, stars };
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Get the primary declared name from a declaration node, or null.
|
|
852
|
+
*/
|
|
853
|
+
function getDeclName(node) {
|
|
854
|
+
const t = nodeType(node);
|
|
855
|
+
if (t === 'function_declaration' ||
|
|
856
|
+
t === 'generator_function_declaration' ||
|
|
857
|
+
t === 'class_declaration' ||
|
|
858
|
+
t === 'abstract_class_declaration' ||
|
|
859
|
+
t === 'interface_declaration' ||
|
|
860
|
+
t === 'type_alias_declaration' ||
|
|
861
|
+
t === 'enum_declaration') {
|
|
862
|
+
return node.childForFieldName('name')?.text ?? null;
|
|
863
|
+
}
|
|
864
|
+
if (t === 'lexical_declaration' || t === 'variable_declaration') {
|
|
865
|
+
const declarator = findChildByType(node, 'variable_declarator');
|
|
866
|
+
if (declarator === null)
|
|
867
|
+
return null;
|
|
868
|
+
return declarator.childForFieldName('name')?.text ?? null;
|
|
869
|
+
}
|
|
870
|
+
return null;
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Return true if the class member has a `private` accessibility modifier.
|
|
874
|
+
*/
|
|
875
|
+
function hasPrivateModifier(memberNode) {
|
|
876
|
+
for (const child of nodeChildren(memberNode)) {
|
|
877
|
+
const t = nodeType(child);
|
|
878
|
+
if (t === 'accessibility_modifier') {
|
|
879
|
+
return child.text === 'private';
|
|
880
|
+
}
|
|
881
|
+
// TypeScript grammar uses '#' for private fields (ESNext private syntax)
|
|
882
|
+
if (t === 'private_field_definition')
|
|
883
|
+
return true;
|
|
884
|
+
}
|
|
885
|
+
return false;
|
|
886
|
+
}
|
|
887
|
+
// ---------------------------------------------------------------------------
|
|
888
|
+
// Public helpers (re-exported from Stage 1 scaffold, now implemented)
|
|
889
|
+
// ---------------------------------------------------------------------------
|
|
890
|
+
export function sha256(text) {
|
|
891
|
+
return createHash('sha256').update(text).digest('hex');
|
|
892
|
+
}
|
|
893
|
+
export function chunkId(filePath, startLine) {
|
|
894
|
+
return sha256(`${filePath}:${startLine}`);
|
|
895
|
+
}
|
|
896
|
+
/** Determine language from extension. */
|
|
897
|
+
export function languageFromExt(ext) {
|
|
898
|
+
return ext === '.js' || ext === '.jsx' ? 'javascript' : 'typescript';
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Expand chunk content by `contextLines` around `[startLine, endLine]`.
|
|
902
|
+
* Returns the expanded content slice; `startLine`/`endLine` in the chunk
|
|
903
|
+
* record always reflect AST boundaries, not the expanded region.
|
|
904
|
+
*/
|
|
905
|
+
export function expandContent(lines, startLine, // 1-indexed
|
|
906
|
+
endLine, // 1-indexed, inclusive
|
|
907
|
+
contextLines) {
|
|
908
|
+
const expandedStart = Math.max(1, startLine - contextLines);
|
|
909
|
+
const expandedEnd = Math.min(lines.length, endLine + contextLines);
|
|
910
|
+
return lines.slice(expandedStart - 1, expandedEnd).join('\n');
|
|
911
|
+
}
|
|
912
|
+
// ---------------------------------------------------------------------------
|
|
913
|
+
// Graph data extraction (symbols, imports, identifiers)
|
|
914
|
+
// ---------------------------------------------------------------------------
|
|
915
|
+
/**
|
|
916
|
+
* Derive `SymbolRecord` entries from an already-extracted chunk list.
|
|
917
|
+
* Skips anonymous chunks (`symbol_name === null`) and raw `block` chunks.
|
|
918
|
+
*/
|
|
919
|
+
export function symbolsFromChunks(chunks) {
|
|
920
|
+
const records = [];
|
|
921
|
+
for (const c of chunks) {
|
|
922
|
+
if (c.symbol_name === null || c.chunk_type === 'block')
|
|
923
|
+
continue;
|
|
924
|
+
records.push({
|
|
925
|
+
name: c.symbol_name,
|
|
926
|
+
kind: chunkTypeToKind(c.chunk_type),
|
|
927
|
+
line: c.start_line,
|
|
928
|
+
isExported: c.is_exported,
|
|
929
|
+
// AST-derived hashes attached during extraction (§7.1). Null only if a
|
|
930
|
+
// symbol chunk somehow lacked them (defensive; should not happen).
|
|
931
|
+
declarationHash: c.declaration_hash ?? null,
|
|
932
|
+
bodyHash: c.body_hash ?? null,
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
return records;
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Extract import records from the tree-sitter AST.
|
|
939
|
+
*
|
|
940
|
+
* Handles named imports (`import { foo } from './bar'`). Side-effect imports
|
|
941
|
+
* (`import './side-effect'`), default imports, and namespace imports are
|
|
942
|
+
* recorded with an empty `symbols` array.
|
|
943
|
+
*
|
|
944
|
+
* Specifier-to-file resolution (relative probing, tsconfig aliases, workspace
|
|
945
|
+
* packages) is NOT done here — it needs project context and is applied by
|
|
946
|
+
* `extract.ts` via the import resolver (§13.7).
|
|
947
|
+
*/
|
|
948
|
+
export function extractImports(parsedTree, _filePath) {
|
|
949
|
+
const topLevel = nodeChildren(parsedTree.rootNode);
|
|
950
|
+
const imports = [];
|
|
951
|
+
for (const node of topLevel) {
|
|
952
|
+
if (nodeType(node) !== 'import_statement')
|
|
953
|
+
continue;
|
|
954
|
+
// Module specifier: find the string child (quoted module path).
|
|
955
|
+
const moduleNode = findChildByType(node, 'string');
|
|
956
|
+
if (moduleNode === null)
|
|
957
|
+
continue;
|
|
958
|
+
// Strip surrounding quotes (' or ").
|
|
959
|
+
const module = moduleNode.text.slice(1, -1);
|
|
960
|
+
// Placeholder classification; extract.ts re-resolves authoritatively via
|
|
961
|
+
// the import resolver (§13.7 — tsconfig aliases, workspace packages,
|
|
962
|
+
// extension probing, symlink realpath). `resolvedPath` is filled there.
|
|
963
|
+
const isExternal = !module.startsWith('.') && !module.startsWith('/');
|
|
964
|
+
const resolvedPath = null;
|
|
965
|
+
// Extract named imports from the import_clause.
|
|
966
|
+
const symbols = [];
|
|
967
|
+
const importClause = findChildByType(node, 'import_clause');
|
|
968
|
+
if (importClause !== null) {
|
|
969
|
+
const namedImports = findChildByType(importClause, 'named_imports');
|
|
970
|
+
if (namedImports !== null) {
|
|
971
|
+
for (const specifier of nodeNamedChildren(namedImports)) {
|
|
972
|
+
if (nodeType(specifier) !== 'import_specifier')
|
|
973
|
+
continue;
|
|
974
|
+
const name = specifier.childForFieldName('name')?.text;
|
|
975
|
+
if (name !== undefined)
|
|
976
|
+
symbols.push(name);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
imports.push({ module, symbols, isExternal, resolvedPath });
|
|
981
|
+
}
|
|
982
|
+
return imports;
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* Extract graph edge records from a parsed tree:
|
|
986
|
+
* - IMPLEMENTS — class → interface (`implements` clause)
|
|
987
|
+
* - EXTENDS — class/interface → its base (`extends` clause)
|
|
988
|
+
* - PARENT_OF — class → its method symbols
|
|
989
|
+
* - POTENTIAL_CALL — caller symbol → statically-resolved callee (§10.3.1)
|
|
990
|
+
*
|
|
991
|
+
* Edge records use symbol *names*; `insertEdges` resolves them to ids after all
|
|
992
|
+
* files' symbols are inserted (two-pass requirement). A POTENTIAL_CALL whose
|
|
993
|
+
* callee name is not a known indexed symbol is dropped there — that is how the
|
|
994
|
+
* "only a known symbol becomes a verified edge" rule from §10.3 is enforced.
|
|
995
|
+
*
|
|
996
|
+
* `src` is the raw source, used to attach the call-site line + context.
|
|
997
|
+
*
|
|
998
|
+
* `onCallSite` (D7, Stage 4) is an optional diagnostics seam: when provided,
|
|
999
|
+
* it is invoked exactly once per `call_expression` node returned by
|
|
1000
|
+
* `collectCalls`, classifying the outcome of resolving that call. It exists
|
|
1001
|
+
* to make the invariant "every visited call yields an edge or a recorded
|
|
1002
|
+
* drop-reason" assertable in tests (`call-oracle.test.ts`); it is not
|
|
1003
|
+
* persisted, configured, or surfaced in any tool response — a future
|
|
1004
|
+
* instrumentation hook (E2's registered corpus measurement), not a shipped
|
|
1005
|
+
* feature. Leaving it `undefined` (the default) costs one `undefined`-check
|
|
1006
|
+
* per call site and zero allocation on the production path.
|
|
1007
|
+
*/
|
|
1008
|
+
export function extractEdges(parsedTree, _filePath, src, onCallSite) {
|
|
1009
|
+
const lines = src.split('\n');
|
|
1010
|
+
const topLevel = nodeChildren(parsedTree.rootNode);
|
|
1011
|
+
const edges = [];
|
|
1012
|
+
// File-scoped callables: named imports + same-file top-level symbol names.
|
|
1013
|
+
const importedNames = [];
|
|
1014
|
+
const sameFileNames = [];
|
|
1015
|
+
for (const node of topLevel) {
|
|
1016
|
+
if (nodeType(node) === 'import_statement') {
|
|
1017
|
+
const importClause = findChildByType(node, 'import_clause');
|
|
1018
|
+
const namedImports = importClause !== null ? findChildByType(importClause, 'named_imports') : null;
|
|
1019
|
+
if (namedImports !== null) {
|
|
1020
|
+
for (const spec of nodeNamedChildren(namedImports)) {
|
|
1021
|
+
if (nodeType(spec) !== 'import_specifier')
|
|
1022
|
+
continue;
|
|
1023
|
+
const name = spec.childForFieldName('name')?.text;
|
|
1024
|
+
if (name !== undefined)
|
|
1025
|
+
importedNames.push(name);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
const decl = nodeType(node) === 'export_statement' ? getWrappedDeclaration(node) : node;
|
|
1031
|
+
if (decl === null)
|
|
1032
|
+
continue;
|
|
1033
|
+
const name = getDeclName(decl);
|
|
1034
|
+
if (name !== null)
|
|
1035
|
+
sameFileNames.push(name);
|
|
1036
|
+
}
|
|
1037
|
+
const seedFileScope = (env) => {
|
|
1038
|
+
for (const n of importedNames)
|
|
1039
|
+
env.recordImport(n);
|
|
1040
|
+
for (const n of sameFileNames)
|
|
1041
|
+
env.recordSameFileSymbol(n);
|
|
1042
|
+
};
|
|
1043
|
+
for (const node of topLevel) {
|
|
1044
|
+
const declNode = nodeType(node) === 'export_statement' ? getWrappedDeclaration(node) : node;
|
|
1045
|
+
if (declNode === null)
|
|
1046
|
+
continue;
|
|
1047
|
+
const t = nodeType(declNode);
|
|
1048
|
+
if (t === 'class_declaration' || t === 'abstract_class_declaration') {
|
|
1049
|
+
emitClassEdges(declNode, edges, seedFileScope, lines, onCallSite);
|
|
1050
|
+
}
|
|
1051
|
+
else if (t === 'interface_declaration') {
|
|
1052
|
+
emitInterfaceExtends(declNode, edges);
|
|
1053
|
+
}
|
|
1054
|
+
else if (t === 'function_declaration' || t === 'generator_function_declaration') {
|
|
1055
|
+
const name = declNode.childForFieldName('name')?.text ?? null;
|
|
1056
|
+
const body = declNode.childForFieldName('body');
|
|
1057
|
+
if (name !== null && body !== null) {
|
|
1058
|
+
emitCallEdges(name, declNode.childForFieldName('parameters'), body, edges, seedFileScope, lines, [], onCallSite);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
else if (t === 'lexical_declaration' || t === 'variable_declaration') {
|
|
1062
|
+
const declarator = findChildByType(declNode, 'variable_declarator');
|
|
1063
|
+
const value = declarator?.childForFieldName('value') ?? null;
|
|
1064
|
+
const name = declarator?.childForFieldName('name')?.text ?? null;
|
|
1065
|
+
if (name !== null && value !== null && nodeType(value) === 'arrow_function') {
|
|
1066
|
+
const body = value.childForFieldName('body');
|
|
1067
|
+
if (body !== null) {
|
|
1068
|
+
emitCallEdges(name, value.childForFieldName('parameters'), body, edges, seedFileScope, lines, [], onCallSite);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
return edges;
|
|
1074
|
+
}
|
|
1075
|
+
/** IMPLEMENTS + EXTENDS + PARENT_OF + per-method POTENTIAL_CALL for one class. */
|
|
1076
|
+
function emitClassEdges(classNode, edges, seedFileScope, lines, onCallSite) {
|
|
1077
|
+
const className = classNode.childForFieldName('name')?.text ?? null;
|
|
1078
|
+
if (className === null)
|
|
1079
|
+
return;
|
|
1080
|
+
// tree-sitter-typescript wraps extends/implements in a `class_heritage` node.
|
|
1081
|
+
const heritage = findChildByType(classNode, 'class_heritage');
|
|
1082
|
+
const implClause = heritage !== null
|
|
1083
|
+
? findChildByType(heritage, 'implements_clause')
|
|
1084
|
+
: findChildByType(classNode, 'implements_clause');
|
|
1085
|
+
if (implClause !== null) {
|
|
1086
|
+
for (const typeRef of nodeNamedChildren(implClause)) {
|
|
1087
|
+
const ifaceName = typeRefName(typeRef);
|
|
1088
|
+
if (ifaceName !== null)
|
|
1089
|
+
edges.push({ fromName: className, toName: ifaceName, edgeType: 'IMPLEMENTS' });
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
// EXTENDS: the `extends` clause names the base class. `baseClassName` is
|
|
1093
|
+
// also the F4 `super_method` receiver binding's type below — captured here
|
|
1094
|
+
// rather than re-derived so the binding and the edge agree by construction.
|
|
1095
|
+
const extendsClause = heritage !== null
|
|
1096
|
+
? findChildByType(heritage, 'extends_clause')
|
|
1097
|
+
: findChildByType(classNode, 'extends_clause');
|
|
1098
|
+
let baseClassName = null;
|
|
1099
|
+
if (extendsClause !== null) {
|
|
1100
|
+
for (const typeRef of nodeNamedChildren(extendsClause)) {
|
|
1101
|
+
const name = typeRefName(typeRef);
|
|
1102
|
+
if (name !== null) {
|
|
1103
|
+
baseClassName = name;
|
|
1104
|
+
edges.push({ fromName: className, toName: name, edgeType: 'EXTENDS' });
|
|
1105
|
+
break; // a class extends at most one base
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
const bodyNode = classNode.childForFieldName('body') ?? findChildByType(classNode, 'class_body');
|
|
1110
|
+
if (bodyNode === null)
|
|
1111
|
+
return;
|
|
1112
|
+
// Class-wide receiver bindings: constructor parameter properties + fields,
|
|
1113
|
+
// plus F4's `this`/`super` bindings — riding the same receiver-binding
|
|
1114
|
+
// machinery LocalTypeEnvironment already provides for `field_type` etc.,
|
|
1115
|
+
// per §10.3.1 "Method calls on super and this". No `super` binding when
|
|
1116
|
+
// there is no parent class: an unresolvable super-call must fall through
|
|
1117
|
+
// to the identifier_fts potential set, never guess a target.
|
|
1118
|
+
const classScopeBindings = [
|
|
1119
|
+
...collectClassFieldBindings(bodyNode),
|
|
1120
|
+
{ receiver: 'this', type: className, resolution: 'this_method' },
|
|
1121
|
+
];
|
|
1122
|
+
if (baseClassName !== null) {
|
|
1123
|
+
classScopeBindings.push({ receiver: 'super', type: baseClassName, resolution: 'super_method' });
|
|
1124
|
+
}
|
|
1125
|
+
for (const member of nodeNamedChildren(bodyNode)) {
|
|
1126
|
+
const mt = nodeType(member);
|
|
1127
|
+
if (mt !== 'method_definition' && mt !== 'abstract_method_signature')
|
|
1128
|
+
continue;
|
|
1129
|
+
const methodName = member.childForFieldName('name')?.text ?? null;
|
|
1130
|
+
if (methodName === null)
|
|
1131
|
+
continue;
|
|
1132
|
+
edges.push({ fromName: className, toName: `${className}.${methodName}`, edgeType: 'PARENT_OF' });
|
|
1133
|
+
const body = member.childForFieldName('body');
|
|
1134
|
+
if (body === null)
|
|
1135
|
+
continue; // abstract / no body
|
|
1136
|
+
emitCallEdges(`${className}.${methodName}`, member.childForFieldName('parameters'), body, edges, seedFileScope, lines, classScopeBindings, onCallSite);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
/** interface extends interface(s). */
|
|
1140
|
+
function emitInterfaceExtends(ifaceNode, edges) {
|
|
1141
|
+
const name = ifaceNode.childForFieldName('name')?.text ?? null;
|
|
1142
|
+
if (name === null)
|
|
1143
|
+
return;
|
|
1144
|
+
const extendsClause = findChildByType(ifaceNode, 'extends_type_clause')
|
|
1145
|
+
?? findChildByType(ifaceNode, 'extends_clause');
|
|
1146
|
+
if (extendsClause === null)
|
|
1147
|
+
return;
|
|
1148
|
+
for (const typeRef of nodeNamedChildren(extendsClause)) {
|
|
1149
|
+
const baseName = typeRefName(typeRef);
|
|
1150
|
+
if (baseName !== null)
|
|
1151
|
+
edges.push({ fromName: name, toName: baseName, edgeType: 'EXTENDS' });
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* Line of the callee TOKEN itself — the method name for `obj.method()`, or
|
|
1156
|
+
* the identifier for a bare `foo()`. Deliberately NOT `call.startPosition`:
|
|
1157
|
+
* for a multi-line fluent/chained call (e.g. `program\n .command(...)`),
|
|
1158
|
+
* the call_expression node's own start position is the RECEIVER's line, not
|
|
1159
|
+
* the line the call syntax actually appears on — which previously produced
|
|
1160
|
+
* a `context` string with no parentheses at all (e.g. the trimmed text
|
|
1161
|
+
* `program`), violating `EdgeRecord.context`'s "source text of the
|
|
1162
|
+
* call-site line" contract. Found by the D7 self-oracle test running over
|
|
1163
|
+
* mast's own `src/` corpus (`cli/index-cmd.ts:9`, `program.command(...)`);
|
|
1164
|
+
* see IMPLEMENTATION_PLAN.md's D7 result for the finding.
|
|
1165
|
+
*/
|
|
1166
|
+
function calleeLine(call) {
|
|
1167
|
+
const fn = call.childForFieldName('function') ?? call.namedChildren[0] ?? null;
|
|
1168
|
+
if (fn !== null && nodeType(fn) === 'member_expression') {
|
|
1169
|
+
const property = fn.childForFieldName('property');
|
|
1170
|
+
if (property !== null)
|
|
1171
|
+
return property.startPosition.row + 1;
|
|
1172
|
+
}
|
|
1173
|
+
return call.startPosition.row + 1;
|
|
1174
|
+
}
|
|
1175
|
+
/**
|
|
1176
|
+
* Build the local type environment for a function/method scope and emit one
|
|
1177
|
+
* POTENTIAL_CALL edge per statically-resolvable call site in its body.
|
|
1178
|
+
*/
|
|
1179
|
+
function emitCallEdges(fromName, paramsNode, bodyNode, edges, seedFileScope, lines,
|
|
1180
|
+
// Class-wide bindings for a method scope: field/constructor-parameter
|
|
1181
|
+
// types (`this.x`) plus F4's `this`/`super` bindings. Empty for top-level
|
|
1182
|
+
// function/arrow scopes, which have no enclosing class.
|
|
1183
|
+
classScopeBindings = [], onCallSite) {
|
|
1184
|
+
const env = new LocalTypeEnvironment();
|
|
1185
|
+
seedFileScope(env);
|
|
1186
|
+
for (const b of classScopeBindings)
|
|
1187
|
+
env.recordReceiverType(b.receiver, b.type, b.resolution);
|
|
1188
|
+
if (paramsNode !== null) {
|
|
1189
|
+
for (const b of collectParamBindings(paramsNode))
|
|
1190
|
+
env.recordReceiverType(b.receiver, b.type, b.resolution);
|
|
1191
|
+
}
|
|
1192
|
+
for (const b of collectNewBindings(bodyNode))
|
|
1193
|
+
env.recordReceiverType(b.receiver, b.type, b.resolution);
|
|
1194
|
+
for (const call of collectCalls(bodyNode)) {
|
|
1195
|
+
const parsed = parseCallee(call);
|
|
1196
|
+
if (parsed === null) {
|
|
1197
|
+
onCallSite?.('unparseable_callee');
|
|
1198
|
+
continue;
|
|
1199
|
+
}
|
|
1200
|
+
const resolved = env.resolveCall(parsed.receiver, parsed.method);
|
|
1201
|
+
if (resolved === null) {
|
|
1202
|
+
onCallSite?.(parsed.receiver === null ? 'bare_call_unresolved' : 'unresolved_receiver');
|
|
1203
|
+
continue;
|
|
1204
|
+
}
|
|
1205
|
+
onCallSite?.('edge_emitted');
|
|
1206
|
+
const line = calleeLine(call);
|
|
1207
|
+
edges.push({
|
|
1208
|
+
fromName,
|
|
1209
|
+
toName: resolved.callee,
|
|
1210
|
+
edgeType: 'POTENTIAL_CALL',
|
|
1211
|
+
resolution: resolved.resolution,
|
|
1212
|
+
callLine: line,
|
|
1213
|
+
context: (lines[line - 1] ?? '').trim(),
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
/** Constructor parameter properties + plain field declarations → `this.x` bindings. */
|
|
1218
|
+
function collectClassFieldBindings(classBody) {
|
|
1219
|
+
const bindings = [];
|
|
1220
|
+
for (const member of nodeNamedChildren(classBody)) {
|
|
1221
|
+
const mt = nodeType(member);
|
|
1222
|
+
if (mt === 'public_field_definition' || mt === 'property_signature') {
|
|
1223
|
+
const name = member.childForFieldName('name')?.text ?? null;
|
|
1224
|
+
const type = annotationTypeName(member);
|
|
1225
|
+
if (name !== null && type !== null)
|
|
1226
|
+
bindings.push({ receiver: `this.${name}`, type, resolution: 'field_type' });
|
|
1227
|
+
}
|
|
1228
|
+
else if (mt === 'method_definition' && member.childForFieldName('name')?.text === 'constructor') {
|
|
1229
|
+
const params = member.childForFieldName('parameters');
|
|
1230
|
+
if (params !== null) {
|
|
1231
|
+
for (const param of nodeNamedChildren(params)) {
|
|
1232
|
+
// A constructor parameter property carries an accessibility modifier.
|
|
1233
|
+
if (findChildByType(param, 'accessibility_modifier') === null)
|
|
1234
|
+
continue;
|
|
1235
|
+
const name = findChildByType(param, 'identifier')?.text ?? null;
|
|
1236
|
+
const type = annotationTypeName(param);
|
|
1237
|
+
if (name !== null && type !== null)
|
|
1238
|
+
bindings.push({ receiver: `this.${name}`, type, resolution: 'field_type' });
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
return bindings;
|
|
1244
|
+
}
|
|
1245
|
+
/** Annotated (non-property) parameters → bare-name receiver bindings. */
|
|
1246
|
+
function collectParamBindings(paramsNode) {
|
|
1247
|
+
const bindings = [];
|
|
1248
|
+
for (const param of nodeNamedChildren(paramsNode)) {
|
|
1249
|
+
const pt = nodeType(param);
|
|
1250
|
+
if (pt !== 'required_parameter' && pt !== 'optional_parameter')
|
|
1251
|
+
continue;
|
|
1252
|
+
if (findChildByType(param, 'accessibility_modifier') !== null)
|
|
1253
|
+
continue; // handled as a field
|
|
1254
|
+
const name = findChildByType(param, 'identifier')?.text ?? null;
|
|
1255
|
+
const type = annotationTypeName(param);
|
|
1256
|
+
if (name !== null && type !== null)
|
|
1257
|
+
bindings.push({ receiver: name, type, resolution: 'parameter_type' });
|
|
1258
|
+
}
|
|
1259
|
+
return bindings;
|
|
1260
|
+
}
|
|
1261
|
+
/** `const x = new Foo()` → `x` resolves to type `Foo`. */
|
|
1262
|
+
function collectNewBindings(bodyNode) {
|
|
1263
|
+
const bindings = [];
|
|
1264
|
+
const visit = (node) => {
|
|
1265
|
+
if (nodeType(node) === 'variable_declarator') {
|
|
1266
|
+
const value = node.childForFieldName('value');
|
|
1267
|
+
const name = node.childForFieldName('name')?.text ?? null;
|
|
1268
|
+
if (name !== null && value !== null && nodeType(value) === 'new_expression') {
|
|
1269
|
+
const ctor = value.childForFieldName('constructor') ?? value.namedChildren[0] ?? null;
|
|
1270
|
+
const type = ctor !== null && nodeType(ctor) === 'identifier' ? ctor.text : null;
|
|
1271
|
+
if (type !== null)
|
|
1272
|
+
bindings.push({ receiver: name, type, resolution: 'new_expression' });
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
for (const child of nodeNamedChildren(node))
|
|
1276
|
+
visit(child);
|
|
1277
|
+
};
|
|
1278
|
+
visit(bodyNode);
|
|
1279
|
+
return bindings;
|
|
1280
|
+
}
|
|
1281
|
+
/** Collect call_expression nodes within a body, not descending into nested
|
|
1282
|
+
* named/anonymous functions, generators, methods, or classes (their bodies
|
|
1283
|
+
* are their own scope — non-arrow functions also get their own dynamic
|
|
1284
|
+
* `this`, so a `this.foo()` inside one is never the enclosing class
|
|
1285
|
+
* instance; F4's `this_method`/`super_method` bindings must not leak in.
|
|
1286
|
+
* Arrow functions are deliberately NOT in this list — they inherit the
|
|
1287
|
+
* enclosing scope's `this` and calls inside them must still resolve).
|
|
1288
|
+
*
|
|
1289
|
+
* Exported (D7, Stage 4) so `call-oracle.test.ts` can independently
|
|
1290
|
+
* enumerate the same call sites `emitCallEdges` visits, as the ground
|
|
1291
|
+
* truth for the `onCallSite` accounting invariant — not part of the
|
|
1292
|
+
* extractor's public tool-facing surface. */
|
|
1293
|
+
export function collectCalls(bodyNode) {
|
|
1294
|
+
const calls = [];
|
|
1295
|
+
const visit = (node) => {
|
|
1296
|
+
for (const child of nodeNamedChildren(node)) {
|
|
1297
|
+
const t = nodeType(child);
|
|
1298
|
+
if (t === 'function_declaration' || t === 'method_definition' ||
|
|
1299
|
+
t === 'class_declaration' || t === 'abstract_class_declaration' ||
|
|
1300
|
+
t === 'function_expression' || t === 'generator_function' ||
|
|
1301
|
+
t === 'generator_function_declaration') {
|
|
1302
|
+
continue;
|
|
1303
|
+
}
|
|
1304
|
+
if (t === 'call_expression')
|
|
1305
|
+
calls.push(child);
|
|
1306
|
+
visit(child);
|
|
1307
|
+
}
|
|
1308
|
+
};
|
|
1309
|
+
visit(bodyNode);
|
|
1310
|
+
return calls;
|
|
1311
|
+
}
|
|
1312
|
+
/** Extract `{ receiver, method }` from a call expression, or null if unhandled. */
|
|
1313
|
+
function parseCallee(call) {
|
|
1314
|
+
const fn = call.childForFieldName('function') ?? call.namedChildren[0] ?? null;
|
|
1315
|
+
if (fn === null)
|
|
1316
|
+
return null;
|
|
1317
|
+
if (nodeType(fn) === 'identifier') {
|
|
1318
|
+
return { receiver: null, method: fn.text };
|
|
1319
|
+
}
|
|
1320
|
+
if (nodeType(fn) === 'member_expression') {
|
|
1321
|
+
const method = fn.childForFieldName('property')?.text ?? null;
|
|
1322
|
+
const objectNode = fn.childForFieldName('object');
|
|
1323
|
+
if (method === null || objectNode === null)
|
|
1324
|
+
return null;
|
|
1325
|
+
const receiver = receiverString(objectNode);
|
|
1326
|
+
if (receiver === null)
|
|
1327
|
+
return null;
|
|
1328
|
+
return { receiver, method };
|
|
1329
|
+
}
|
|
1330
|
+
return null;
|
|
1331
|
+
}
|
|
1332
|
+
/**
|
|
1333
|
+
* Unwrap `(await x)` down to `x` for the receiver position (F3). Tree-sitter
|
|
1334
|
+
* parses `(await x).m()`'s object as `parenthesized_expression(
|
|
1335
|
+
* await_expression(x))` — verified via a tree-sitter S-expression dump
|
|
1336
|
+
* against `tree-sitter-typescript`'s current grammar. Only unwraps an
|
|
1337
|
+
* await specifically (not parens in general) — this is syntax unwrapping,
|
|
1338
|
+
* not type inference: `const y = await makeFoo(); y.bar()` still does not
|
|
1339
|
+
* resolve, per §10.3.1's "does NOT catch" list.
|
|
1340
|
+
*/
|
|
1341
|
+
function unwrapAwaitedReceiver(node) {
|
|
1342
|
+
if (nodeType(node) !== 'parenthesized_expression')
|
|
1343
|
+
return node;
|
|
1344
|
+
const inner = node.namedChildren[0];
|
|
1345
|
+
if (inner === undefined || nodeType(inner) !== 'await_expression')
|
|
1346
|
+
return node;
|
|
1347
|
+
const awaited = inner.namedChildren[0];
|
|
1348
|
+
return awaited ?? node;
|
|
1349
|
+
}
|
|
1350
|
+
/** Stringify a member-expression receiver for the conservative resolver. */
|
|
1351
|
+
function receiverString(objectNode) {
|
|
1352
|
+
const node = unwrapAwaitedReceiver(objectNode);
|
|
1353
|
+
const t = nodeType(node);
|
|
1354
|
+
if (t === 'identifier')
|
|
1355
|
+
return node.text;
|
|
1356
|
+
// F4: bare `this`/`super` as the receiver — `this.foo()`/`super.foo()`.
|
|
1357
|
+
// The env bindings for these two literal strings are seeded per-method by
|
|
1358
|
+
// `emitClassEdges`; outside a class scope (or inside a nested function
|
|
1359
|
+
// that shadows `this`, per collectCalls above) no binding exists and
|
|
1360
|
+
// `resolveCall` returns null, same as any other unbound receiver.
|
|
1361
|
+
if (t === 'this')
|
|
1362
|
+
return 'this';
|
|
1363
|
+
if (t === 'super')
|
|
1364
|
+
return 'super';
|
|
1365
|
+
if (t === 'member_expression') {
|
|
1366
|
+
const inner = node.childForFieldName('object');
|
|
1367
|
+
const prop = node.childForFieldName('property')?.text ?? null;
|
|
1368
|
+
if (inner !== null && prop !== null && nodeType(inner) === 'this')
|
|
1369
|
+
return `this.${prop}`;
|
|
1370
|
+
}
|
|
1371
|
+
return null;
|
|
1372
|
+
}
|
|
1373
|
+
/** Name of a type reference node in a heritage clause. */
|
|
1374
|
+
function typeRefName(typeRef) {
|
|
1375
|
+
const t = nodeType(typeRef);
|
|
1376
|
+
if (t === 'type_identifier' || t === 'identifier')
|
|
1377
|
+
return typeRef.text;
|
|
1378
|
+
if (t === 'generic_type')
|
|
1379
|
+
return typeRef.childForFieldName('name')?.text ?? null;
|
|
1380
|
+
return null;
|
|
1381
|
+
}
|
|
1382
|
+
/** Resolve the named type from a node's `type_annotation` child, if simple. */
|
|
1383
|
+
function annotationTypeName(node) {
|
|
1384
|
+
const annotation = findChildByType(node, 'type_annotation');
|
|
1385
|
+
if (annotation === null)
|
|
1386
|
+
return null;
|
|
1387
|
+
for (const child of nodeNamedChildren(annotation)) {
|
|
1388
|
+
const t = nodeType(child);
|
|
1389
|
+
if (t === 'type_identifier')
|
|
1390
|
+
return child.text;
|
|
1391
|
+
if (t === 'generic_type')
|
|
1392
|
+
return child.childForFieldName('name')?.text ?? null;
|
|
1393
|
+
}
|
|
1394
|
+
return null;
|
|
1395
|
+
}
|
|
1396
|
+
/**
|
|
1397
|
+
* Extract all identifier tokens from chunk content for `identifier_fts`.
|
|
1398
|
+
* Returns a whitespace-separated, deduplicated list of identifier strings.
|
|
1399
|
+
* Uses a word-boundary regex — sufficient for the unicode61 tokeniser's
|
|
1400
|
+
* identifier-boundary separators.
|
|
1401
|
+
*/
|
|
1402
|
+
export function extractIdentifiers(content) {
|
|
1403
|
+
const re = /\b[A-Za-z_$][A-Za-z0-9_$]*\b/g;
|
|
1404
|
+
const seen = new Set();
|
|
1405
|
+
let m;
|
|
1406
|
+
while ((m = re.exec(content)) !== null) {
|
|
1407
|
+
seen.add(m[0]);
|
|
1408
|
+
}
|
|
1409
|
+
return [...seen].join(' ');
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* Append qualified compound strings (e.g. "Class.method", F5) to an
|
|
1413
|
+
* already-built identifier bag, deduplicated and appended AFTER the bare
|
|
1414
|
+
* bag — never interleaved — so the qualified compound's own two sub-tokens
|
|
1415
|
+
* (post unicode61 tokenisation, which splits on '.') land adjacent to each
|
|
1416
|
+
* other and are not accidentally split apart by an unrelated bare token.
|
|
1417
|
+
*/
|
|
1418
|
+
function appendQualifiedCompounds(base, extra) {
|
|
1419
|
+
if (extra.length === 0)
|
|
1420
|
+
return base;
|
|
1421
|
+
const seen = new Set(base.length > 0 ? base.split(' ') : []);
|
|
1422
|
+
for (const q of extra)
|
|
1423
|
+
seen.add(q);
|
|
1424
|
+
return [...seen].join(' ');
|
|
1425
|
+
}
|
|
1426
|
+
// ---------------------------------------------------------------------------
|
|
1427
|
+
// Private hash helpers for symbol records
|
|
1428
|
+
// ---------------------------------------------------------------------------
|
|
1429
|
+
function chunkTypeToKind(t) {
|
|
1430
|
+
switch (t) {
|
|
1431
|
+
case 'function': return 'function';
|
|
1432
|
+
case 'class_shell': return 'class';
|
|
1433
|
+
case 'method': return 'method';
|
|
1434
|
+
case 'interface': return 'interface';
|
|
1435
|
+
case 'type': return 'type';
|
|
1436
|
+
default: return 'const';
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
//# sourceMappingURL=typescript.js.map
|