From 255972283c08c05b276a473a51bf168eba616223 Mon Sep 17 00:00:00 2001 From: bompus Date: Wed, 28 Oct 2026 12:56:07 -0600 Subject: [PATCH 01/30] fix(resolve): port upstream's lone same-file match confidence (cde4cd74) A lone same-file definition chosen by findBestMatch scored by directory proximity (0.7/0.4), losing to a framework's naming guess in another file. is_decisive_same_file_match scores it 0.9 when it is the only same-file candidate, is not the caller itself, and, as a member, belongs to an owner the caller is inside. Ports upstream cde4cd74 (#1938). same-file-match-confidence: 7/7. --- codegraph-kernel/src/resolve/names.rs | 57 ++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/codegraph-kernel/src/resolve/names.rs b/codegraph-kernel/src/resolve/names.rs index d8652be24..87be01270 100644 --- a/codegraph-kernel/src/resolve/names.rs +++ b/codegraph-kernel/src/resolve/names.rs @@ -950,15 +950,70 @@ impl KernelResolver { } if self.is_cross_file_reachable(&best, r)? { let proximity = compute_path_proximity(&r.file_path, &best.file_path); + let confidence = if self.is_decisive_same_file_match(&best, r, &candidates)? { + 0.9 + } else if proximity >= 30 { + 0.7 + } else { + 0.4 + }; return Ok(Some(KCand { node: best, - confidence: if proximity >= 30 { 0.7 } else { 0.4 }, + confidence, resolved_by: "exact-match", })); } Ok(None) } + /// Whether `findBestMatch` picked `best` because it is THE definition in + /// the call site's own file, so the edge deserves a same-file confidence + /// rather than the directory-proximity one. Proximity measures shared + /// folders, so a same-file match in a shallow path scored 0.4 like a guess + /// across unrelated modules. Decisive only when: + /// - `best` is the ONLY same-file candidate — several are separated by + /// line distance, which is not evidence; + /// - a member of a type, module or namespace belongs to one the caller is + /// inside — a bare call does not reach a sibling class's method just + /// because it is written in that file. A method's owner is always such a + /// scope; another kind's is judged when the file declares it as anything + /// but a function; and + /// - `best` is not the caller itself. + /// + /// (name-matcher.ts isDecisiveSameFileMatch, upstream cde4cd74.) + fn is_decisive_same_file_match( + &self, + best: &Arc, + r: &ResolveRefIn, + candidates: &[Arc], + ) -> Res { + if best.file_path != r.file_path || best.id == r.from_node_id { + return Ok(false); + } + if candidates.iter().any(|c| c.id != best.id && c.file_path == r.file_path) { + return Ok(false); + } + let sep = best.qualified_name.rfind("::").unwrap_or(0); + if sep == 0 { + return Ok(best.kind != "method"); + } + let owner = &best.qualified_name[..sep]; + if best.kind != "method" { + let declared = self.nodes_in_file(&best.file_path)?; + if declared + .iter() + .filter(|n| n.qualified_name == owner) + .all(|n| n.kind == "function" || n.kind == "method") + { + return Ok(true); + } + } + let Some(from) = self.node_by_id(&r.from_node_id)? else { + return Ok(false); + }; + Ok(from.qualified_name == owner || from.qualified_name.starts_with(&format!("{owner}::"))) + } + /// The candidates a PHP `$this->m()` / `self::m()` / `parent::m()` call /// allows: methods of the class hierarchy around the call site /// (matchByExactName / matchFuzzy, name-matcher.ts). From 54c134f1c45155aab95221317b1d7c975e9997e8 Mon Sep 17 00:00:00 2001 From: bompus Date: Wed, 28 Oct 2026 12:56:07 -0600 Subject: [PATCH 02/30] chore(resolve): dump the kernel profile at close(), not only at drop The resolver is not dropped before process exit on the CLI index path, so CODEGRAPH_KERNEL_PROF=1 accumulated timings that never printed. close() is the deterministic teardown point. --- codegraph-kernel/src/resolve/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/codegraph-kernel/src/resolve/mod.rs b/codegraph-kernel/src/resolve/mod.rs index ad22ab441..8684e19ea 100644 --- a/codegraph-kernel/src/resolve/mod.rs +++ b/codegraph-kernel/src/resolve/mod.rs @@ -1049,6 +1049,7 @@ impl KernelResolver { #[cfg_attr(feature = "addon", napi)] pub fn close(&mut self) { self.debug_stats("close"); + prof_dump("close"); self.release_conn(); self.table.take(); self.file_cache.clear(); From 4f0b844ce5773fab48772bd26642fe1ab8694e9c Mon Sep 17 00:00:00 2001 From: bompus Date: Wed, 28 Oct 2026 12:58:28 -0600 Subject: [PATCH 03/30] test(ts): assert the fork's native error-tree path for export type * MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upstream's kernel defers a file whose parse tree has errors, so the test expected tryKernelExtract to return null. The fork's tsjs walker extracts the recovered tree natively (cba92567) and keeps the re-export signature as written — assert that contract instead. --- __tests__/ts-export-type-star.test.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/__tests__/ts-export-type-star.test.ts b/__tests__/ts-export-type-star.test.ts index 4c43299bb..69c9e4407 100644 --- a/__tests__/ts-export-type-star.test.ts +++ b/__tests__/ts-export-type-star.test.ts @@ -159,12 +159,19 @@ describe('a type-only star re-export parses in both engines', () => { } } - it.runIf(kernelAvailable)('keeps a re-export as written in a file the kernel hands to the wasm parser', () => { - // `export const = ;` is an error, so the kernel defers the whole file. + it.runIf(kernelAvailable)('keeps a re-export as written in a file with parse errors', () => { + // `export const = ;` is an error. Upstream's kernel defers such a file to + // the wasm parser; the fork's extracts the recovered native tree and flags + // it not-parsed-clean, so this path asserts the signature survives instead + // of the defer. const source = 'export type * from "./types.js";\nexport const = ;\n'; delete process.env.CODEGRAPH_KERNEL; process.env.CODEGRAPH_KERNEL_LANGS = 'all'; - expect(tryKernelExtract('src/broken.ts', source, 'typescript')).toBeNull(); + const kernelResult = tryKernelExtract('src/broken.ts', source, 'typescript'); + expect(kernelResult).not.toBeNull(); + expect(kernelResult?.nodes.filter((n) => n.kind === 'import').map((n) => n.signature)).toEqual([ + 'export type * from "./types.js";', + ]); const result = extractFromSource('src/broken.ts', source, 'typescript'); expect(result.nodes.filter((n) => n.kind === 'import').map((n) => n.signature)).toEqual([ 'export type * from "./types.js";', From 2f0c7087e2db5ff025e4985f3213bd10bedf076d Mon Sep 17 00:00:00 2001 From: bompus Date: Wed, 28 Oct 2026 13:00:12 -0600 Subject: [PATCH 04/30] fix(resolve): port upstream's indexed import-path spelling (8651990a) An alias, workspace or direct-path join kept a specifier's lexical spelling, so `@/widgets/Chart/` probed `Chart//index.tsx` and `src/widgets/./Chart` probed `./` segments no files row is keyed by. probe_extensions now spells each candidate as the index keys it for the ES-module family; other languages keep the path as found. Ports upstream 8651990a (#2792). import-path-spelling: 5/5. --- codegraph-kernel/src/resolve/imports.rs | 30 ++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/codegraph-kernel/src/resolve/imports.rs b/codegraph-kernel/src/resolve/imports.rs index cb1f7f23a..22437224a 100644 --- a/codegraph-kernel/src/resolve/imports.rs +++ b/codegraph-kernel/src/resolve/imports.rs @@ -181,15 +181,20 @@ impl KernelResolver { /// The first existing file among `base` + each of the language's /// extensions, then `base` itself, then the source an emitted `.js`/`.d.ts` - /// path was compiled from. + /// path was compiled from — each spelled as the index keys it + /// (indexedSpelling, upstream 8651990a / #2577). pub(super) fn probe_extensions(&self, base: &str, language: &str) -> Option { extension_resolution(language) .iter() .filter(|ext| !base.is_empty() || ext.starts_with('/')) .map(|ext| if base.is_empty() { ext.trim_start_matches('/').to_string() } else { format!("{base}{ext}") }) + .map(|candidate| indexed_spelling(&candidate, language)) .find(|candidate| self.file_exists(candidate)) - .or_else(|| self.file_exists(base).then(|| base.to_string())) - .or_else(|| self.find_source_for_emitted(base, language)) + .or_else(|| { + let as_is = indexed_spelling(base, language); + self.file_exists(&as_is).then_some(as_is) + }) + .or_else(|| self.find_source_for_emitted(&indexed_spelling(base, language), language)) } /// resolveAliasedImport (import-resolver.ts): tsconfig paths → workspace @@ -1763,6 +1768,25 @@ fn plain_import_names(line: &str, source: &str) -> Option { Some(clause.split(',').any(|name| name.trim() == source)) } +/// indexedSpelling (import-resolver.ts, upstream 8651990a / #2577): a path as +/// the index keys its files. The alias, workspace and direct-path branches +/// join an import specifier as written, so `@/widgets/Chart/` made +/// `src/widgets/Chart//index.tsx` and `src/widgets/./Chart` made +/// `src/widgets/./Chart/index.tsx` — spellings no file node or `files` row is +/// keyed by. An ES-module specifier is read lexically, so `.` and empty +/// segments drop and `..` folds; a trailing slash still names a folder. Other +/// languages keep the path as found — a C include's `a/../b.h` follows the +/// folder `a` on disk, symlinks included. +fn indexed_spelling(file_path: &str, language: &str) -> String { + let dirty = file_path.contains("//") + || file_path.split('/').any(|seg| seg == "." || seg == ".."); + if is_esm_import_language(language) && dirty { + pos_normalize(file_path) + } else { + file_path.to_string() + } +} + #[cfg(test)] mod plain_import_tests { use super::plain_import_names; From ee5b5c1ab5bb640051204ceea109e2867af03ed2 Mon Sep 17 00:00:00 2001 From: bompus Date: Wed, 28 Oct 2026 13:30:11 -0600 Subject: [PATCH 05/30] fix(resolve): port upstream's Dart library-privacy rule (f7820b58) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A Dart `_name` is private to its library — the library file and every file its `part` directives name — not to its file. private_is_file_local drops `dart`; is_visible_across_files gets the dart arm (a private candidate is seen only from a file of its own library); and the exact-name pre-pick filter declines a private candidate another library declares so the caller's own library's declaration wins. Ports upstream f7820b58 (#2832). dart-part-privacy: 10/12 — the two remaining failures resolve a typed receiver's private method through resolve_method_on_type's member path, whose visibility gate still needs tracing (upstream's dart arm at name-matcher.ts ~13147 / preferOwnDartLibrary is the next suspect). --- codegraph-kernel/src/resolve/names.rs | 12 ++++++++++++ codegraph-kernel/src/resolve/tables.rs | 5 +++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/codegraph-kernel/src/resolve/names.rs b/codegraph-kernel/src/resolve/names.rs index 87be01270..24569a7f2 100644 --- a/codegraph-kernel/src/resolve/names.rs +++ b/codegraph-kernel/src/resolve/names.rs @@ -375,6 +375,14 @@ impl KernelResolver { if lang == "go" { return Ok(go_name_visible(candidate, r)); } + // A Dart `_name` is its library's, which spans the library file and + // its parts: riverpod_generator's `class Counter extends _$Counter` + // names the class it wrote into `main.g.dart` (upstream f7820b58). + if lang == "dart" { + return Ok(candidate.visibility.as_deref() != Some("private") + || (r.language == "dart" + && self.dart_same_library(&r.file_path, &candidate.file_path))); + } if lang == "rust" { if candidate.visibility.as_deref() != Some("private") { return Ok(true); @@ -784,6 +792,10 @@ impl KernelResolver { // tsdb's `[]*memSeries` is head.go's, not `tsdb/agent`'s, which scored the // same by directory proximity and was then vetoed as invisible. && (n.language != "go" || go_name_visible(&n, r)) + // A Dart `_name` another library declares: the reference's + // own library's declaration wins instead of the namesake + // picked and then declined (upstream f7820b58 / #2466). + && !(r.language == "dart" && n.language == "dart" && n.visibility.as_deref() == Some("private") && n.file_path != r.file_path && !self.dart_same_library(&r.file_path, &n.file_path)) && !(matches!(r.language.as_str(),"lua"|"luau") && r.reference_kind=="calls" && !r.reference_name.contains(['.',':']) && n.kind=="method") && !(r.language=="python" && r.reference_kind=="calls" && n.file_path!=r.file_path && self.is_receiver_less_call(r)? && self.python_locally_bound(&r.reference_name,r)? && !self.python_fixture_reachable(&n,&r.file_path)) { kept.push(n); diff --git a/codegraph-kernel/src/resolve/tables.rs b/codegraph-kernel/src/resolve/tables.rs index e34a7ce77..32368bc74 100644 --- a/codegraph-kernel/src/resolve/tables.rs +++ b/codegraph-kernel/src/resolve/tables.rs @@ -696,9 +696,10 @@ pub(super) fn is_importable_kind(kind: &str) -> bool { !matches!(kind, "property" | "field" | "method" | "enum_member" | "parameter") } -/// PRIVATE_IS_FILE_LOCAL (name-matcher.ts). +/// PRIVATE_IS_FILE_LOCAL (name-matcher.ts). A Dart `_name` is its library's, +/// which can span several files: see is_visible_across_files (f7820b58). pub(super) fn private_is_file_local(lang: &str) -> bool { - matches!(lang, "kotlin" | "java" | "csharp" | "swift" | "scala" | "dart" | "php") + matches!(lang, "kotlin" | "java" | "csharp" | "swift" | "scala" | "php") } /// NO_NESTED_FUNCTIONS (name-matcher.ts). From 593c3aeb7b4b995d8a2b44e5db17383bd06937e3 Mon Sep 17 00:00:00 2001 From: bompus Date: Wed, 28 Oct 2026 13:53:54 -0600 Subject: [PATCH 06/30] fix(resolve): vet every strategy's pick through is_visible_across_files finish() applied name_post_guard but not the file/library-locality veto resolveOne's nameResult gets at index.ts:1867, so typed-receiver picks (instance-method) linked private members of another library. With the dart arm in place this completes f7820b58: dart-part-privacy 12/12. --- codegraph-kernel/src/resolve/pipeline.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/codegraph-kernel/src/resolve/pipeline.rs b/codegraph-kernel/src/resolve/pipeline.rs index b4237a37d..ff7c40435 100644 --- a/codegraph-kernel/src/resolve/pipeline.rs +++ b/codegraph-kernel/src/resolve/pipeline.rs @@ -1192,6 +1192,10 @@ if super::lang_scope::is_dart_member_read(r) { // package-level value in another file is not an unknown package name. if r.language == "go" && !is_binding_receiver_call(r) && self.go_written_qualifier(r)?.is_some() && self.go_ref_qualifier(r)?.is_none() { return Ok(ResolveOutcome::unresolved()); } if !self.name_post_guard(&winner,r)? { return Ok(ResolveOutcome::unresolved()); } + // A definition its language makes file- or library-local cannot be + // what a name in another file means, whichever strategy chose it — + // name-matcher.ts isVisibleAcrossFiles at index.ts:1867 (#1730). + if !self.is_visible_across_files(&winner.node, r)? { return Ok(ResolveOutcome::unresolved()); } if winner.node.id == r.from_node_id && is_inheritance_ref(&r.reference_kind) { let Some(other) = self.other_supertype_named(r)? else { return Ok(ResolveOutcome::unresolved()); }; winner=other; } From 4d65256c1436ded41b44b7d180eb1573bdaacbbf Mon Sep 17 00:00:00 2001 From: bompus Date: Wed, 28 Oct 2026 14:19:55 -0600 Subject: [PATCH 07/30] fix(library): indexFiles keeps other writers out until its files are stored MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The file lock was released as soon as indexing started — the promise returned unawaited, the try ended, the finally let the lock go. Awaited, the lock now holds until the files are stored, as indexAll and sync hold it (upstream 865ddf4a / #2845). --- src/codegraph.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/codegraph.ts b/src/codegraph.ts index 7fb507b06..21bce07e1 100644 --- a/src/codegraph.ts +++ b/src/codegraph.ts @@ -946,7 +946,11 @@ export class CodeGraph { return { success: false, filesIndexed: 0, filesSkipped: 0, filesErrored: 0, nodesCreated: 0, edgesCreated: 0, errors: [{ message: 'Could not acquire file lock - another process may be indexing', severity: 'error' as const }], durationMs: 0 }; } try { - return this.orchestrator.indexFiles(filePaths); + // Awaited, so the write lock is held until the files are stored, as + // indexAll and sync hold it: returned unawaited, the promise left the + // `try` at once and the `finally` let the lock go as soon as indexing + // started, so another process could write meanwhile (#2845). + return await this.orchestrator.indexFiles(filePaths); } finally { this.fileLock.release(); } From 5d6d997ea8d2ba3eefa32507be12f4b818da8f76 Mon Sep 17 00:00:00 2001 From: bompus Date: Wed, 28 Oct 2026 14:20:50 -0600 Subject: [PATCH 08/30] fix(resolve): let a framework own a reference past the name strategies settleKernelOutcome merged kernel candidates beside the frameworks', so a project namesake could answer a call Nuxt's navigateTo owns (upstream 58eb57a3 / #2826). With ownsReference set, the frameworks' best answer wins or the ref stays unresolved. --- src/resolution/index.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/resolution/index.ts b/src/resolution/index.ts index f556037d0..acf3775df 100644 --- a/src/resolution/index.ts +++ b/src/resolution/index.ts @@ -1461,6 +1461,14 @@ export class ReferenceResolver { candidates.push(result); } } + // A reference a framework answers alone ends with the frameworks' answer + // (see FrameworkResolver.ownsReference): Nuxt's `navigateTo(x)` with a + // destination no route serves is no namesake's call. + if (this.frameworksFor(ref.language).some((f) => f.ownsReference?.(ref, this.context))) { + if (candidates.length === 0) return this.applyResolveTail(null, ref); + const owned = candidates.reduce((best, curr) => (curr.confidence > best.confidence ? curr : best)); + return this.applyResolveTail(this.gateTargetKind(owned, ref), ref); + } // The kernel's ≥0.9 import verdict outranks <0.9 framework candidates — // resolveOneInner early-returns it and discards them. A verdict reported // without a candidate list is likewise already the merged winner. From 35ecabf764a0e5248fb36d5bd894c0cd84658cb3 Mon Sep 17 00:00:00 2001 From: bompus Date: Wed, 28 Oct 2026 14:22:18 -0600 Subject: [PATCH 09/30] =?UTF-8?q?test(vue):=20assert=20the=20fork=20contra?= =?UTF-8?q?ct=20for=20tsx=20blocks=20=E2=80=94=20grammar=20always=20compil?= =?UTF-8?q?ed=20in?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upstream's test asserted wasm-preload sets (9ee6718a / #2824); the fork has no wasm loader, so a tsx