@optave/codegraph 3.8.1 → 3.9.1
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/README.md +12 -7
- package/dist/ast-analysis/engine.d.ts.map +1 -1
- package/dist/ast-analysis/engine.js +121 -48
- package/dist/ast-analysis/engine.js.map +1 -1
- package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
- package/dist/ast-analysis/visitors/ast-store-visitor.js +15 -18
- package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
- package/dist/ast-analysis/visitors/complexity-visitor.d.ts.map +1 -1
- package/dist/ast-analysis/visitors/complexity-visitor.js +50 -1
- package/dist/ast-analysis/visitors/complexity-visitor.js.map +1 -1
- package/dist/cli/commands/branch-compare.d.ts.map +1 -1
- package/dist/cli/commands/branch-compare.js +4 -0
- package/dist/cli/commands/branch-compare.js.map +1 -1
- package/dist/cli/commands/diff-impact.d.ts.map +1 -1
- package/dist/cli/commands/diff-impact.js +2 -1
- package/dist/cli/commands/diff-impact.js.map +1 -1
- package/dist/cli/commands/info.d.ts.map +1 -1
- package/dist/cli/commands/info.js +3 -2
- package/dist/cli/commands/info.js.map +1 -1
- package/dist/db/connection.d.ts +1 -0
- package/dist/db/connection.d.ts.map +1 -1
- package/dist/db/connection.js +22 -4
- package/dist/db/connection.js.map +1 -1
- package/dist/db/repository/base.d.ts +41 -0
- package/dist/db/repository/base.d.ts.map +1 -1
- package/dist/db/repository/base.js +22 -0
- package/dist/db/repository/base.js.map +1 -1
- package/dist/db/repository/index.d.ts +1 -0
- package/dist/db/repository/index.d.ts.map +1 -1
- package/dist/db/repository/index.js.map +1 -1
- package/dist/db/repository/native-repository.d.ts +8 -1
- package/dist/db/repository/native-repository.d.ts.map +1 -1
- package/dist/db/repository/native-repository.js +69 -1
- package/dist/db/repository/native-repository.js.map +1 -1
- package/dist/db/repository/sqlite-repository.d.ts +1 -0
- package/dist/db/repository/sqlite-repository.d.ts.map +1 -1
- package/dist/db/repository/sqlite-repository.js +25 -0
- package/dist/db/repository/sqlite-repository.js.map +1 -1
- package/dist/domain/analysis/dependencies.d.ts +1 -28
- package/dist/domain/analysis/dependencies.d.ts.map +1 -1
- package/dist/domain/analysis/dependencies.js +24 -8
- package/dist/domain/analysis/dependencies.js.map +1 -1
- package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
- package/dist/domain/graph/builder/incremental.js +18 -0
- package/dist/domain/graph/builder/incremental.js.map +1 -1
- package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
- package/dist/domain/graph/builder/pipeline.js +298 -206
- package/dist/domain/graph/builder/pipeline.js.map +1 -1
- package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/build-edges.js +56 -3
- package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
- package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/resolve-imports.js +19 -23
- package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
- package/dist/domain/graph/watcher.d.ts.map +1 -1
- package/dist/domain/graph/watcher.js +99 -95
- package/dist/domain/graph/watcher.js.map +1 -1
- package/dist/domain/parser.d.ts +4 -0
- package/dist/domain/parser.d.ts.map +1 -1
- package/dist/domain/parser.js +130 -61
- package/dist/domain/parser.js.map +1 -1
- package/dist/domain/search/models.d.ts.map +1 -1
- package/dist/domain/search/models.js +7 -5
- package/dist/domain/search/models.js.map +1 -1
- package/dist/extractors/go.js +53 -35
- package/dist/extractors/go.js.map +1 -1
- package/dist/extractors/javascript.js +85 -36
- package/dist/extractors/javascript.js.map +1 -1
- package/dist/features/complexity.d.ts.map +1 -1
- package/dist/features/complexity.js +78 -58
- package/dist/features/complexity.js.map +1 -1
- package/dist/features/dataflow.d.ts.map +1 -1
- package/dist/features/dataflow.js +109 -118
- package/dist/features/dataflow.js.map +1 -1
- package/dist/features/structure.d.ts.map +1 -1
- package/dist/features/structure.js +147 -97
- package/dist/features/structure.js.map +1 -1
- package/dist/graph/algorithms/louvain.d.ts.map +1 -1
- package/dist/graph/algorithms/louvain.js +4 -2
- package/dist/graph/algorithms/louvain.js.map +1 -1
- package/dist/graph/classifiers/roles.d.ts +2 -0
- package/dist/graph/classifiers/roles.d.ts.map +1 -1
- package/dist/graph/classifiers/roles.js +13 -5
- package/dist/graph/classifiers/roles.js.map +1 -1
- package/dist/presentation/communities.d.ts.map +1 -1
- package/dist/presentation/communities.js +38 -34
- package/dist/presentation/communities.js.map +1 -1
- package/dist/presentation/manifesto.d.ts.map +1 -1
- package/dist/presentation/manifesto.js +31 -33
- package/dist/presentation/manifesto.js.map +1 -1
- package/dist/presentation/queries-cli/inspect.d.ts.map +1 -1
- package/dist/presentation/queries-cli/inspect.js +47 -46
- package/dist/presentation/queries-cli/inspect.js.map +1 -1
- package/dist/shared/file-utils.d.ts.map +1 -1
- package/dist/shared/file-utils.js +94 -72
- package/dist/shared/file-utils.js.map +1 -1
- package/dist/types.d.ts +83 -2
- package/dist/types.d.ts.map +1 -1
- package/grammars/tree-sitter-erlang.wasm +0 -0
- package/grammars/tree-sitter-gleam.wasm +0 -0
- package/package.json +9 -9
- package/src/ast-analysis/engine.ts +150 -55
- package/src/ast-analysis/visitors/ast-store-visitor.ts +19 -21
- package/src/ast-analysis/visitors/complexity-visitor.ts +55 -1
- package/src/cli/commands/branch-compare.ts +4 -0
- package/src/cli/commands/diff-impact.ts +2 -1
- package/src/cli/commands/info.ts +3 -2
- package/src/db/connection.ts +24 -5
- package/src/db/repository/base.ts +57 -0
- package/src/db/repository/index.ts +1 -0
- package/src/db/repository/native-repository.ts +92 -1
- package/src/db/repository/sqlite-repository.ts +26 -0
- package/src/domain/analysis/dependencies.ts +24 -6
- package/src/domain/graph/builder/incremental.ts +21 -0
- package/src/domain/graph/builder/pipeline.ts +396 -245
- package/src/domain/graph/builder/stages/build-edges.ts +53 -2
- package/src/domain/graph/builder/stages/resolve-imports.ts +20 -20
- package/src/domain/graph/watcher.ts +118 -98
- package/src/domain/parser.ts +131 -63
- package/src/domain/search/models.ts +11 -5
- package/src/extractors/go.ts +57 -32
- package/src/extractors/javascript.ts +88 -35
- package/src/features/complexity.ts +94 -58
- package/src/features/dataflow.ts +153 -132
- package/src/features/structure.ts +167 -95
- package/src/graph/algorithms/louvain.ts +5 -2
- package/src/graph/classifiers/roles.ts +14 -5
- package/src/presentation/communities.ts +44 -39
- package/src/presentation/manifesto.ts +35 -38
- package/src/presentation/queries-cli/inspect.ts +48 -46
- package/src/shared/file-utils.ts +116 -77
- package/src/types.ts +87 -1
package/src/domain/parser.ts
CHANGED
|
@@ -102,6 +102,12 @@ let _cachedLanguages: Map<string, Language> | null = null;
|
|
|
102
102
|
// Query cache for JS/TS/TSX extractors (populated during createParsers)
|
|
103
103
|
const _queryCache: Map<string, Query> = new Map();
|
|
104
104
|
|
|
105
|
+
// Tracks whether ALL grammars have been loaded (vs. a lazy subset)
|
|
106
|
+
let _allParsersLoaded: boolean = false;
|
|
107
|
+
|
|
108
|
+
// In-flight grammar loads keyed by language id — prevents concurrent duplicate loads
|
|
109
|
+
const _loadingPromises: Map<string, Promise<void>> = new Map();
|
|
110
|
+
|
|
105
111
|
// Extensions that need typeMap backfill (type annotations only exist in TS/TSX)
|
|
106
112
|
const TS_BACKFILL_EXTS = new Set(['.ts', '.tsx']);
|
|
107
113
|
|
|
@@ -137,6 +143,8 @@ const COMMON_QUERY_PATTERNS: string[] = [
|
|
|
137
143
|
'(call_expression function: (identifier) @callfn_name) @callfn_node',
|
|
138
144
|
'(call_expression function: (member_expression) @callmem_fn) @callmem_node',
|
|
139
145
|
'(call_expression function: (subscript_expression) @callsub_fn) @callsub_node',
|
|
146
|
+
'(new_expression constructor: (identifier) @newfn_name) @newfn_node',
|
|
147
|
+
'(new_expression constructor: (member_expression) @newmem_fn) @newmem_node',
|
|
140
148
|
'(expression_statement (assignment_expression left: (member_expression) @assign_left right: (_) @assign_right)) @assign_node',
|
|
141
149
|
];
|
|
142
150
|
|
|
@@ -150,42 +158,87 @@ const TS_EXTRA_PATTERNS: string[] = [
|
|
|
150
158
|
'(type_alias_declaration name: (type_identifier) @type_name) @type_node',
|
|
151
159
|
];
|
|
152
160
|
|
|
153
|
-
|
|
154
|
-
|
|
161
|
+
/**
|
|
162
|
+
* Load a single language grammar and cache the parser + language + query.
|
|
163
|
+
* Uses in-flight deduplication so concurrent callers awaiting the same grammar
|
|
164
|
+
* share a single load rather than producing orphaned WASM instances.
|
|
165
|
+
* Assumes Parser.init() has already been called and _cachedParsers/_cachedLanguages exist.
|
|
166
|
+
*/
|
|
167
|
+
async function loadLanguage(entry: LanguageRegistryEntry): Promise<void> {
|
|
168
|
+
if (_cachedParsers!.has(entry.id)) return;
|
|
169
|
+
const inflight = _loadingPromises.get(entry.id);
|
|
170
|
+
if (inflight) return inflight;
|
|
171
|
+
const p = doLoadLanguage(entry).finally(() => _loadingPromises.delete(entry.id));
|
|
172
|
+
_loadingPromises.set(entry.id, p);
|
|
173
|
+
return p;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function doLoadLanguage(entry: LanguageRegistryEntry): Promise<void> {
|
|
177
|
+
try {
|
|
178
|
+
const lang = await Language.load(grammarPath(entry.grammarFile));
|
|
179
|
+
const parser = new Parser();
|
|
180
|
+
parser.setLanguage(lang);
|
|
181
|
+
_cachedParsers!.set(entry.id, parser);
|
|
182
|
+
_cachedLanguages!.set(entry.id, lang);
|
|
183
|
+
if (entry.extractor === extractSymbols && !_queryCache.has(entry.id)) {
|
|
184
|
+
const isTS = entry.id === 'typescript' || entry.id === 'tsx';
|
|
185
|
+
const patterns = isTS
|
|
186
|
+
? [...COMMON_QUERY_PATTERNS, ...TS_EXTRA_PATTERNS]
|
|
187
|
+
: [...COMMON_QUERY_PATTERNS, JS_CLASS_PATTERN];
|
|
188
|
+
_queryCache.set(entry.id, new Query(lang, patterns.join('\n')));
|
|
189
|
+
}
|
|
190
|
+
} catch (e: unknown) {
|
|
191
|
+
if (entry.required) throw e;
|
|
192
|
+
warn(
|
|
193
|
+
`${entry.id} parser failed to initialize: ${(e as Error).message}. ${entry.id} files will be skipped.`,
|
|
194
|
+
);
|
|
195
|
+
_cachedParsers!.set(entry.id, null);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
155
198
|
|
|
199
|
+
async function initParserRuntime(): Promise<void> {
|
|
156
200
|
if (!_initialized) {
|
|
157
201
|
await Parser.init();
|
|
158
202
|
_initialized = true;
|
|
159
203
|
}
|
|
204
|
+
if (!_cachedParsers) _cachedParsers = new Map();
|
|
205
|
+
if (!_cachedLanguages) _cachedLanguages = new Map();
|
|
206
|
+
}
|
|
160
207
|
|
|
161
|
-
|
|
162
|
-
|
|
208
|
+
/**
|
|
209
|
+
* Load only the WASM grammars needed for the given file paths.
|
|
210
|
+
* Grammars already in cache are reused. This avoids the ~500ms cold-start
|
|
211
|
+
* penalty of loading all 23+ grammars when only 1-2 are needed (e.g. incremental rebuilds).
|
|
212
|
+
*/
|
|
213
|
+
async function ensureParsersForFiles(filePaths: string[]): Promise<Map<string, Parser | null>> {
|
|
214
|
+
await initParserRuntime();
|
|
215
|
+
const needed = new Set<LanguageRegistryEntry>();
|
|
216
|
+
for (const fp of filePaths) {
|
|
217
|
+
const ext = path.extname(fp).toLowerCase();
|
|
218
|
+
const entry = _extToLang.get(ext);
|
|
219
|
+
if (entry && !_cachedParsers!.has(entry.id)) needed.add(entry);
|
|
220
|
+
}
|
|
221
|
+
for (const entry of needed) {
|
|
222
|
+
await loadLanguage(entry);
|
|
223
|
+
}
|
|
224
|
+
return _cachedParsers!;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Load ALL WASM grammars. Used by full builds and feature modules (CFG, dataflow, complexity)
|
|
229
|
+
* that may process files of any language.
|
|
230
|
+
*/
|
|
231
|
+
export async function createParsers(): Promise<Map<string, Parser | null>> {
|
|
232
|
+
if (_cachedParsers && _allParsersLoaded) return _cachedParsers;
|
|
233
|
+
|
|
234
|
+
await initParserRuntime();
|
|
163
235
|
for (const entry of LANGUAGE_REGISTRY) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const parser = new Parser();
|
|
167
|
-
parser.setLanguage(lang);
|
|
168
|
-
parsers.set(entry.id, parser);
|
|
169
|
-
languages.set(entry.id, lang);
|
|
170
|
-
// Compile and cache tree-sitter Query for JS/TS/TSX extractors
|
|
171
|
-
if (entry.extractor === extractSymbols && !_queryCache.has(entry.id)) {
|
|
172
|
-
const isTS = entry.id === 'typescript' || entry.id === 'tsx';
|
|
173
|
-
const patterns = isTS
|
|
174
|
-
? [...COMMON_QUERY_PATTERNS, ...TS_EXTRA_PATTERNS]
|
|
175
|
-
: [...COMMON_QUERY_PATTERNS, JS_CLASS_PATTERN];
|
|
176
|
-
_queryCache.set(entry.id, new Query(lang, patterns.join('\n')));
|
|
177
|
-
}
|
|
178
|
-
} catch (e: unknown) {
|
|
179
|
-
if (entry.required) throw e;
|
|
180
|
-
warn(
|
|
181
|
-
`${entry.id} parser failed to initialize: ${(e as Error).message}. ${entry.id} files will be skipped.`,
|
|
182
|
-
);
|
|
183
|
-
parsers.set(entry.id, null);
|
|
236
|
+
if (!_cachedParsers!.has(entry.id)) {
|
|
237
|
+
await loadLanguage(entry);
|
|
184
238
|
}
|
|
185
239
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
return parsers;
|
|
240
|
+
_allParsersLoaded = true;
|
|
241
|
+
return _cachedParsers!;
|
|
189
242
|
}
|
|
190
243
|
|
|
191
244
|
/**
|
|
@@ -217,6 +270,8 @@ export function disposeParsers(): void {
|
|
|
217
270
|
_cachedLanguages = null;
|
|
218
271
|
}
|
|
219
272
|
_initialized = false;
|
|
273
|
+
_allParsersLoaded = false;
|
|
274
|
+
_loadingPromises.clear();
|
|
220
275
|
}
|
|
221
276
|
|
|
222
277
|
export function getParser(parsers: Map<string, Parser | null>, filePath: string): Parser | null {
|
|
@@ -235,20 +290,15 @@ export async function ensureWasmTrees(
|
|
|
235
290
|
fileSymbols: Map<string, any>,
|
|
236
291
|
rootDir: string,
|
|
237
292
|
): Promise<void> {
|
|
238
|
-
//
|
|
239
|
-
|
|
293
|
+
// Single pass: collect absolute paths for files that need parsing
|
|
294
|
+
const filePaths: string[] = [];
|
|
240
295
|
for (const [relPath, symbols] of fileSymbols) {
|
|
241
|
-
if (!symbols._tree) {
|
|
242
|
-
|
|
243
|
-
if (_extToLang.has(ext)) {
|
|
244
|
-
needsParse = true;
|
|
245
|
-
break;
|
|
246
|
-
}
|
|
296
|
+
if (!symbols._tree && _extToLang.has(path.extname(relPath).toLowerCase())) {
|
|
297
|
+
filePaths.push(path.join(rootDir, relPath));
|
|
247
298
|
}
|
|
248
299
|
}
|
|
249
|
-
if (
|
|
250
|
-
|
|
251
|
-
const parsers = await createParsers();
|
|
300
|
+
if (filePaths.length === 0) return;
|
|
301
|
+
const parsers = await ensureParsersForFiles(filePaths);
|
|
252
302
|
|
|
253
303
|
for (const [relPath, symbols] of fileSymbols) {
|
|
254
304
|
if (symbols._tree) continue;
|
|
@@ -337,17 +387,19 @@ function patchImports(imports: any[]): void {
|
|
|
337
387
|
}
|
|
338
388
|
}
|
|
339
389
|
|
|
340
|
-
/** Normalize native typeMap array to a Map instance.
|
|
390
|
+
/** Normalize native typeMap array to a Map instance.
|
|
391
|
+
* Uses first-wins semantics at equal confidence to match the WASM/JS extractor. */
|
|
341
392
|
function patchTypeMap(r: any): void {
|
|
342
393
|
if (!r.typeMap) {
|
|
343
394
|
r.typeMap = new Map();
|
|
344
395
|
} else if (!(r.typeMap instanceof Map)) {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
{ type: e.typeName, confidence: 0.9 }
|
|
349
|
-
|
|
350
|
-
|
|
396
|
+
const map = new Map<string, TypeMapEntry>();
|
|
397
|
+
for (const e of r.typeMap as Array<{ name: string; typeName: string }>) {
|
|
398
|
+
if (!map.has(e.name)) {
|
|
399
|
+
map.set(e.name, { type: e.typeName, confidence: (e as any).confidence ?? 0.9 });
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
r.typeMap = map;
|
|
351
403
|
}
|
|
352
404
|
}
|
|
353
405
|
|
|
@@ -640,10 +692,17 @@ for (const entry of LANGUAGE_REGISTRY) {
|
|
|
640
692
|
export const SUPPORTED_EXTENSIONS: Set<string> = new Set(_extToLang.keys());
|
|
641
693
|
|
|
642
694
|
/**
|
|
643
|
-
* WASM-based typeMap backfill for
|
|
695
|
+
* WASM-based typeMap backfill for TS/TSX files parsed by the native engine.
|
|
696
|
+
* Serves two purposes:
|
|
697
|
+
* 1. Compatibility with older native binaries that don't emit typeMap (< 3.2.0).
|
|
698
|
+
* 2. Workaround for native parser scope-collision bugs — when the same variable
|
|
699
|
+
* name appears at multiple scopes, native type extraction can produce
|
|
700
|
+
* incorrect results. WASM's JS-based extractor handles scope traversal
|
|
701
|
+
* more accurately. TODO: Remove purpose (2) once the Rust extractor handles
|
|
702
|
+
* nested scopes correctly.
|
|
703
|
+
*
|
|
644
704
|
* Uses tree-sitter AST extraction instead of regex to avoid false positives from
|
|
645
705
|
* matches inside comments and string literals.
|
|
646
|
-
* TODO: Remove once all published native binaries include typeMap extraction (>= 3.2.0)
|
|
647
706
|
*/
|
|
648
707
|
async function backfillTypeMap(
|
|
649
708
|
filePath: string,
|
|
@@ -658,7 +717,7 @@ async function backfillTypeMap(
|
|
|
658
717
|
return { typeMap: new Map(), backfilled: false };
|
|
659
718
|
}
|
|
660
719
|
}
|
|
661
|
-
const parsers = await
|
|
720
|
+
const parsers = await ensureParsersForFiles([filePath]);
|
|
662
721
|
const extracted = wasmExtractSymbols(parsers, filePath, code);
|
|
663
722
|
try {
|
|
664
723
|
if (!extracted || extracted.symbols.typeMap.size === 0) {
|
|
@@ -720,23 +779,26 @@ export async function parseFileAuto(
|
|
|
720
779
|
const result = native.parseFile(filePath, source, !!opts.dataflow, opts.ast !== false);
|
|
721
780
|
if (!result) return null;
|
|
722
781
|
const patched = patchNativeResult(result);
|
|
723
|
-
//
|
|
724
|
-
//
|
|
725
|
-
|
|
782
|
+
// Always backfill typeMap for TS/TSX from WASM — native parser's type
|
|
783
|
+
// extraction can produce incorrect scope-collision results. Non-TS files
|
|
784
|
+
// are skipped to stay consistent with the batch path (backfillTypeMapBatch).
|
|
785
|
+
if (TS_BACKFILL_EXTS.has(path.extname(filePath))) {
|
|
726
786
|
const { typeMap, backfilled } = await backfillTypeMap(filePath, source);
|
|
727
|
-
|
|
728
|
-
|
|
787
|
+
if (backfilled) {
|
|
788
|
+
patched.typeMap = typeMap;
|
|
789
|
+
patched._typeMapBackfilled = true;
|
|
790
|
+
}
|
|
729
791
|
}
|
|
730
792
|
return patched;
|
|
731
793
|
}
|
|
732
794
|
|
|
733
795
|
// WASM path
|
|
734
|
-
const parsers = await
|
|
796
|
+
const parsers = await ensureParsersForFiles([filePath]);
|
|
735
797
|
const extracted = wasmExtractSymbols(parsers, filePath, source);
|
|
736
798
|
return extracted ? extracted.symbols : null;
|
|
737
799
|
}
|
|
738
800
|
|
|
739
|
-
/** Backfill typeMap via WASM for files
|
|
801
|
+
/** Backfill typeMap via WASM for TS/TSX files parsed by the native engine. */
|
|
740
802
|
async function backfillTypeMapBatch(
|
|
741
803
|
needsTypeMap: { filePath: string; relPath: string }[],
|
|
742
804
|
result: Map<string, ExtractorOutput>,
|
|
@@ -746,7 +808,7 @@ async function backfillTypeMapBatch(
|
|
|
746
808
|
);
|
|
747
809
|
if (tsFiles.length === 0) return;
|
|
748
810
|
|
|
749
|
-
const parsers = await
|
|
811
|
+
const parsers = await ensureParsersForFiles(tsFiles.map((f) => f.filePath));
|
|
750
812
|
for (const { filePath, relPath } of tsFiles) {
|
|
751
813
|
let extracted: WasmExtractResult | null | undefined;
|
|
752
814
|
try {
|
|
@@ -778,7 +840,7 @@ async function parseFilesWasm(
|
|
|
778
840
|
rootDir: string,
|
|
779
841
|
): Promise<Map<string, ExtractorOutput>> {
|
|
780
842
|
const result = new Map<string, ExtractorOutput>();
|
|
781
|
-
const parsers = await
|
|
843
|
+
const parsers = await ensureParsersForFiles(filePaths);
|
|
782
844
|
for (const filePath of filePaths) {
|
|
783
845
|
let code: string;
|
|
784
846
|
try {
|
|
@@ -819,7 +881,12 @@ export async function parseFilesAuto(
|
|
|
819
881
|
const patched = patchNativeResult(r);
|
|
820
882
|
const relPath = path.relative(rootDir, r.file).split(path.sep).join('/');
|
|
821
883
|
result.set(relPath, patched);
|
|
822
|
-
|
|
884
|
+
// Always backfill TS/TSX type maps from WASM — the native parser's type
|
|
885
|
+
// extraction can produce incorrect results when the same variable name
|
|
886
|
+
// appears at multiple scopes (e.g. `node: TreeSitterNode` in one function
|
|
887
|
+
// vs `node: NodeRow` in another). The WASM JS extractor handles scope
|
|
888
|
+
// traversal order more accurately.
|
|
889
|
+
if (TS_BACKFILL_EXTS.has(path.extname(r.file))) {
|
|
823
890
|
needsTypeMap.push({ filePath: r.file, relPath });
|
|
824
891
|
}
|
|
825
892
|
}
|
|
@@ -877,12 +944,13 @@ export async function parseFileIncremental(
|
|
|
877
944
|
const result = cache.parseFile(filePath, source);
|
|
878
945
|
if (!result) return null;
|
|
879
946
|
const patched = patchNativeResult(result);
|
|
880
|
-
//
|
|
881
|
-
|
|
882
|
-
if (patched.typeMap.size === 0 && TS_BACKFILL_EXTS.has(path.extname(filePath))) {
|
|
947
|
+
// Always backfill typeMap for TS/TSX from WASM (see parseFileAuto comment).
|
|
948
|
+
if (TS_BACKFILL_EXTS.has(path.extname(filePath))) {
|
|
883
949
|
const { typeMap, backfilled } = await backfillTypeMap(filePath, source);
|
|
884
|
-
|
|
885
|
-
|
|
950
|
+
if (backfilled) {
|
|
951
|
+
patched.typeMap = typeMap;
|
|
952
|
+
patched._typeMapBackfilled = true;
|
|
953
|
+
}
|
|
886
954
|
}
|
|
887
955
|
return patched;
|
|
888
956
|
}
|
|
@@ -74,6 +74,7 @@ export const MODELS: Record<string, ModelConfig> = {
|
|
|
74
74
|
export const EMBEDDING_STRATEGIES: readonly string[] = ['structured', 'source'];
|
|
75
75
|
|
|
76
76
|
export const DEFAULT_MODEL: string = 'nomic-v1.5';
|
|
77
|
+
const NPM_BIN = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
77
78
|
const BATCH_SIZE_MAP: Record<string, number> = {
|
|
78
79
|
minilm: 32,
|
|
79
80
|
'jina-small': 16,
|
|
@@ -106,13 +107,15 @@ export function promptInstall(packageName: string): Promise<boolean> {
|
|
|
106
107
|
if (!process.stdin.isTTY) {
|
|
107
108
|
info(`Installing ${packageName} (optional dependency for semantic search)…`);
|
|
108
109
|
try {
|
|
109
|
-
execFileSync(
|
|
110
|
+
execFileSync(NPM_BIN, ['install', '--no-save', packageName], {
|
|
110
111
|
stdio: 'inherit',
|
|
111
112
|
timeout: 300_000,
|
|
112
113
|
});
|
|
113
114
|
return Promise.resolve(true);
|
|
114
115
|
} catch (err) {
|
|
115
|
-
info(
|
|
116
|
+
info(
|
|
117
|
+
`Auto-install of ${packageName} failed (${err instanceof Error ? err.message : String(err)}). Install it manually with:\n npm install ${packageName}`,
|
|
118
|
+
);
|
|
116
119
|
return Promise.resolve(false);
|
|
117
120
|
}
|
|
118
121
|
}
|
|
@@ -125,12 +128,15 @@ export function promptInstall(packageName: string): Promise<boolean> {
|
|
|
125
128
|
rl.close();
|
|
126
129
|
if (answer.trim().toLowerCase() !== 'y') return resolve(false);
|
|
127
130
|
try {
|
|
128
|
-
execFileSync(
|
|
131
|
+
execFileSync(NPM_BIN, ['install', packageName], {
|
|
129
132
|
stdio: 'inherit',
|
|
130
133
|
timeout: 300_000,
|
|
131
134
|
});
|
|
132
135
|
resolve(true);
|
|
133
|
-
} catch {
|
|
136
|
+
} catch (err) {
|
|
137
|
+
info(
|
|
138
|
+
`Install of ${packageName} failed (${err instanceof Error ? err.message : String(err)}). Install it manually with:\n npm install ${packageName}`,
|
|
139
|
+
);
|
|
134
140
|
resolve(false);
|
|
135
141
|
}
|
|
136
142
|
},
|
|
@@ -188,7 +194,7 @@ async function loadModel(modelKey?: string): Promise<{ extractor: unknown; confi
|
|
|
188
194
|
pipeline = transformers.pipeline;
|
|
189
195
|
|
|
190
196
|
info(`Loading embedding model: ${config.name} (${config.dim}d)...`);
|
|
191
|
-
const pipelineOpts = config.quantized ? {
|
|
197
|
+
const pipelineOpts = config.quantized ? { dtype: 'q8' } : {};
|
|
192
198
|
try {
|
|
193
199
|
extractor =
|
|
194
200
|
await // biome-ignore lint/complexity/noBannedTypes: dynamically loaded transformers pipeline is untyped
|
package/src/extractors/go.ts
CHANGED
|
@@ -266,44 +266,69 @@ function handleTypedIdentifiers(
|
|
|
266
266
|
}
|
|
267
267
|
|
|
268
268
|
/** Infer type from a single RHS expression in a short var declaration. */
|
|
269
|
-
|
|
269
|
+
/** x := Struct{...} — composite literal (confidence 1.0). */
|
|
270
|
+
function inferCompositeLiteral(
|
|
270
271
|
varNode: TreeSitterNode,
|
|
271
272
|
rhs: TreeSitterNode,
|
|
272
273
|
typeMap: Map<string, { type: string; confidence: number }>,
|
|
273
|
-
):
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
if (
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
274
|
+
): boolean {
|
|
275
|
+
if (rhs.type !== 'composite_literal') return false;
|
|
276
|
+
const typeNode = rhs.childForFieldName('type');
|
|
277
|
+
if (!typeNode) return false;
|
|
278
|
+
const typeName = extractGoTypeName(typeNode);
|
|
279
|
+
if (typeName) setTypeMapEntry(typeMap, varNode.text, typeName, 1.0);
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** x := &Struct{...} — address-of composite literal (confidence 1.0). */
|
|
284
|
+
function inferAddressOfComposite(
|
|
285
|
+
varNode: TreeSitterNode,
|
|
286
|
+
rhs: TreeSitterNode,
|
|
287
|
+
typeMap: Map<string, { type: string; confidence: number }>,
|
|
288
|
+
): boolean {
|
|
289
|
+
if (rhs.type !== 'unary_expression') return false;
|
|
290
|
+
const operand = rhs.childForFieldName('operand');
|
|
291
|
+
if (!operand || operand.type !== 'composite_literal') return false;
|
|
292
|
+
const typeNode = operand.childForFieldName('type');
|
|
293
|
+
if (!typeNode) return false;
|
|
294
|
+
const typeName = extractGoTypeName(typeNode);
|
|
295
|
+
if (typeName) setTypeMapEntry(typeMap, varNode.text, typeName, 1.0);
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** x := NewFoo() or x := pkg.NewFoo() — factory function (confidence 0.7). */
|
|
300
|
+
function inferFactoryCall(
|
|
301
|
+
varNode: TreeSitterNode,
|
|
302
|
+
rhs: TreeSitterNode,
|
|
303
|
+
typeMap: Map<string, { type: string; confidence: number }>,
|
|
304
|
+
): boolean {
|
|
305
|
+
if (rhs.type !== 'call_expression') return false;
|
|
306
|
+
const fn = rhs.childForFieldName('function');
|
|
307
|
+
if (!fn) return false;
|
|
308
|
+
|
|
309
|
+
if (fn.type === 'selector_expression') {
|
|
310
|
+
const field = fn.childForFieldName('field');
|
|
311
|
+
if (field?.text.startsWith('New')) {
|
|
312
|
+
const typeName = field.text.slice(3);
|
|
304
313
|
if (typeName) setTypeMapEntry(typeMap, varNode.text, typeName, 0.7);
|
|
314
|
+
return true;
|
|
305
315
|
}
|
|
316
|
+
} else if (fn.type === 'identifier' && fn.text.startsWith('New')) {
|
|
317
|
+
const typeName = fn.text.slice(3);
|
|
318
|
+
if (typeName) setTypeMapEntry(typeMap, varNode.text, typeName, 0.7);
|
|
319
|
+
return true;
|
|
306
320
|
}
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function inferShortVarType(
|
|
325
|
+
varNode: TreeSitterNode,
|
|
326
|
+
rhs: TreeSitterNode,
|
|
327
|
+
typeMap: Map<string, { type: string; confidence: number }>,
|
|
328
|
+
): void {
|
|
329
|
+
if (inferCompositeLiteral(varNode, rhs, typeMap)) return;
|
|
330
|
+
if (inferAddressOfComposite(varNode, rhs, typeMap)) return;
|
|
331
|
+
inferFactoryCall(varNode, rhs, typeMap);
|
|
307
332
|
}
|
|
308
333
|
|
|
309
334
|
/** Handle short_var_declaration: x := Struct{}, x := &Struct{}, x := NewFoo(). */
|
|
@@ -202,6 +202,48 @@ function handleExportCapture(
|
|
|
202
202
|
}
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
function handleInterfaceCapture(
|
|
206
|
+
c: Record<string, TreeSitterNode>,
|
|
207
|
+
definitions: Definition[],
|
|
208
|
+
): void {
|
|
209
|
+
const ifaceNode = c.iface_node!;
|
|
210
|
+
const ifaceName = c.iface_name!.text;
|
|
211
|
+
definitions.push({
|
|
212
|
+
name: ifaceName,
|
|
213
|
+
kind: 'interface',
|
|
214
|
+
line: ifaceNode.startPosition.row + 1,
|
|
215
|
+
endLine: nodeEndLine(ifaceNode),
|
|
216
|
+
});
|
|
217
|
+
const body =
|
|
218
|
+
ifaceNode.childForFieldName('body') ||
|
|
219
|
+
findChild(ifaceNode, 'interface_body') ||
|
|
220
|
+
findChild(ifaceNode, 'object_type');
|
|
221
|
+
if (body) extractInterfaceMethods(body, ifaceName, definitions);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function handleTypeCapture(c: Record<string, TreeSitterNode>, definitions: Definition[]): void {
|
|
225
|
+
const typeNode = c.type_node!;
|
|
226
|
+
definitions.push({
|
|
227
|
+
name: c.type_name!.text,
|
|
228
|
+
kind: 'type',
|
|
229
|
+
line: typeNode.startPosition.row + 1,
|
|
230
|
+
endLine: nodeEndLine(typeNode),
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function handleImportCapture(c: Record<string, TreeSitterNode>, imports: Import[]): void {
|
|
235
|
+
const impNode = c.imp_node!;
|
|
236
|
+
const isTypeOnly = impNode.text.startsWith('import type');
|
|
237
|
+
const modPath = c.imp_source!.text.replace(/['"]/g, '');
|
|
238
|
+
const names = extractImportNames(impNode);
|
|
239
|
+
imports.push({
|
|
240
|
+
source: modPath,
|
|
241
|
+
names,
|
|
242
|
+
line: impNode.startPosition.row + 1,
|
|
243
|
+
typeOnly: isTypeOnly,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
205
247
|
/** Dispatch a single query match to the appropriate handler. */
|
|
206
248
|
function dispatchQueryMatch(
|
|
207
249
|
c: Record<string, TreeSitterNode>,
|
|
@@ -220,35 +262,11 @@ function dispatchQueryMatch(
|
|
|
220
262
|
} else if (c.meth_node) {
|
|
221
263
|
handleMethodCapture(c, definitions);
|
|
222
264
|
} else if (c.iface_node) {
|
|
223
|
-
|
|
224
|
-
definitions.push({
|
|
225
|
-
name: ifaceName,
|
|
226
|
-
kind: 'interface',
|
|
227
|
-
line: c.iface_node.startPosition.row + 1,
|
|
228
|
-
endLine: nodeEndLine(c.iface_node),
|
|
229
|
-
});
|
|
230
|
-
const body =
|
|
231
|
-
c.iface_node.childForFieldName('body') ||
|
|
232
|
-
findChild(c.iface_node, 'interface_body') ||
|
|
233
|
-
findChild(c.iface_node, 'object_type');
|
|
234
|
-
if (body) extractInterfaceMethods(body, ifaceName, definitions);
|
|
265
|
+
handleInterfaceCapture(c, definitions);
|
|
235
266
|
} else if (c.type_node) {
|
|
236
|
-
definitions
|
|
237
|
-
name: c.type_name!.text,
|
|
238
|
-
kind: 'type',
|
|
239
|
-
line: c.type_node.startPosition.row + 1,
|
|
240
|
-
endLine: nodeEndLine(c.type_node),
|
|
241
|
-
});
|
|
267
|
+
handleTypeCapture(c, definitions);
|
|
242
268
|
} else if (c.imp_node) {
|
|
243
|
-
|
|
244
|
-
const modPath = c.imp_source!.text.replace(/['"]/g, '');
|
|
245
|
-
const names = extractImportNames(c.imp_node);
|
|
246
|
-
imports.push({
|
|
247
|
-
source: modPath,
|
|
248
|
-
names,
|
|
249
|
-
line: c.imp_node.startPosition.row + 1,
|
|
250
|
-
typeOnly: isTypeOnly,
|
|
251
|
-
});
|
|
269
|
+
handleImportCapture(c, imports);
|
|
252
270
|
} else if (c.exp_node) {
|
|
253
271
|
handleExportCapture(c, exps, imports);
|
|
254
272
|
} else if (c.callfn_node) {
|
|
@@ -264,6 +282,14 @@ function dispatchQueryMatch(
|
|
|
264
282
|
} else if (c.callsub_node) {
|
|
265
283
|
const callInfo = extractCallInfo(c.callsub_fn!, c.callsub_node);
|
|
266
284
|
if (callInfo) calls.push(callInfo);
|
|
285
|
+
} else if (c.newfn_node) {
|
|
286
|
+
calls.push({
|
|
287
|
+
name: c.newfn_name!.text,
|
|
288
|
+
line: c.newfn_node.startPosition.row + 1,
|
|
289
|
+
});
|
|
290
|
+
} else if (c.newmem_node) {
|
|
291
|
+
const callInfo = extractCallInfo(c.newmem_fn!, c.newmem_node);
|
|
292
|
+
if (callInfo) calls.push(callInfo);
|
|
267
293
|
} else if (c.assign_node) {
|
|
268
294
|
handleCommonJSAssignment(c.assign_left!, c.assign_right!, c.assign_node, imports);
|
|
269
295
|
}
|
|
@@ -502,6 +528,9 @@ function walkJavaScriptNode(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
|
502
528
|
case 'call_expression':
|
|
503
529
|
handleCallExpr(node, ctx);
|
|
504
530
|
break;
|
|
531
|
+
case 'new_expression':
|
|
532
|
+
handleNewExpr(node, ctx);
|
|
533
|
+
break;
|
|
505
534
|
case 'import_statement':
|
|
506
535
|
handleImportStmt(node, ctx);
|
|
507
536
|
break;
|
|
@@ -689,6 +718,17 @@ function handleCallExpr(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
|
689
718
|
}
|
|
690
719
|
}
|
|
691
720
|
|
|
721
|
+
function handleNewExpr(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
722
|
+
const ctor = node.childForFieldName('constructor') || node.child(1);
|
|
723
|
+
if (!ctor) return;
|
|
724
|
+
if (ctor.type === 'identifier') {
|
|
725
|
+
ctx.calls.push({ name: ctor.text, line: node.startPosition.row + 1 });
|
|
726
|
+
} else if (ctor.type === 'member_expression') {
|
|
727
|
+
const callInfo = extractCallInfo(ctor, node);
|
|
728
|
+
if (callInfo) ctx.calls.push(callInfo);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
692
732
|
/** Handle a dynamic import() call expression and add to imports if static. */
|
|
693
733
|
function handleDynamicImportCall(node: TreeSitterNode, imports: Import[]): void {
|
|
694
734
|
const args = node.childForFieldName('arguments') || findChild(node, 'arguments');
|
|
@@ -997,21 +1037,34 @@ function handleVarDeclaratorTypeMap(
|
|
|
997
1037
|
const nameN = node.childForFieldName('name');
|
|
998
1038
|
if (!nameN || nameN.type !== 'identifier') return;
|
|
999
1039
|
|
|
1000
|
-
// Type annotation: const x: Foo = …
|
|
1001
1040
|
const typeAnno = findChild(node, 'type_annotation');
|
|
1041
|
+
const valueN = node.childForFieldName('value');
|
|
1042
|
+
|
|
1043
|
+
// Constructor on the same declaration wins over annotation: the runtime type is
|
|
1044
|
+
// what matters for call resolution (e.g. `const x: Base = new Derived()` should
|
|
1045
|
+
// resolve `x.render()` to `Derived.render`, not `Base.render`).
|
|
1046
|
+
// When no constructor is present, annotation still takes precedence over factory.
|
|
1047
|
+
if (valueN?.type === 'new_expression') {
|
|
1048
|
+
const ctorType = extractNewExprTypeName(valueN);
|
|
1049
|
+
if (ctorType) {
|
|
1050
|
+
setTypeMapEntry(typeMap, nameN.text, ctorType, 1.0);
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// Type annotation: const x: Foo = … → confidence 0.9
|
|
1002
1056
|
if (typeAnno) {
|
|
1003
1057
|
const typeName = extractSimpleTypeName(typeAnno);
|
|
1004
|
-
if (typeName)
|
|
1058
|
+
if (typeName) {
|
|
1059
|
+
setTypeMapEntry(typeMap, nameN.text, typeName, 0.9);
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1005
1062
|
}
|
|
1006
1063
|
|
|
1007
|
-
const valueN = node.childForFieldName('value');
|
|
1008
1064
|
if (!valueN) return;
|
|
1009
1065
|
|
|
1010
|
-
// Constructor
|
|
1011
|
-
if (valueN.type === 'new_expression')
|
|
1012
|
-
const ctorType = extractNewExprTypeName(valueN);
|
|
1013
|
-
if (ctorType) setTypeMapEntry(typeMap, nameN.text, ctorType, 1.0);
|
|
1014
|
-
}
|
|
1066
|
+
// Constructor already handled above — only factory path remains.
|
|
1067
|
+
if (valueN.type === 'new_expression') return;
|
|
1015
1068
|
// Factory method: const x = Foo.create() → confidence 0.7
|
|
1016
1069
|
else if (valueN.type === 'call_expression') {
|
|
1017
1070
|
const fn = valueN.childForFieldName('function');
|