diff --git a/codegraph-kernel/src/resolve/declarations_upstream.rs b/codegraph-kernel/src/resolve/declarations_upstream.rs index 69c2181fc..4aa57d722 100644 --- a/codegraph-kernel/src/resolve/declarations_upstream.rs +++ b/codegraph-kernel/src/resolve/declarations_upstream.rs @@ -465,7 +465,7 @@ impl KernelResolver { candidates: None, file_path: n.file_path.clone(), language: n.language.clone(), - failure_reason: None, + failure_reason: None, generated: None, }; self.ensure_cpp_default_index(&n.file_path, &site)?; let namespaces = self diff --git a/codegraph-kernel/src/resolve/fields.rs b/codegraph-kernel/src/resolve/fields.rs index 519b53f21..b6bf6326a 100644 --- a/codegraph-kernel/src/resolve/fields.rs +++ b/codegraph-kernel/src/resolve/fields.rs @@ -227,7 +227,7 @@ impl KernelResolver { .map(|line| strip_line_comments(line)).collect::>().join("\n"); if owner.kind == "type_alias" { // Alias method sets follow their targets; defined types do not. - let site = ResolveRefIn { from_node_id: owner.id.clone(), reference_name: owner.name.clone(), reference_kind: "references".to_string(), line: owner.start_line, column: owner.start_column, file_path: owner.file_path.clone(), language: "go".to_string(), row_id: None, candidates: None, failure_reason: None }; + let site = ResolveRefIn { from_node_id: owner.id.clone(), reference_name: owner.name.clone(), reference_kind: "references".to_string(), line: owner.start_line, column: owner.start_column, file_path: owner.file_path.clone(), language: "go".to_string(), row_id: None, candidates: None, failure_reason: None, generated: None }; if let Some(tree) = self.parsed_tree(&lines, &site) { let mut declaration = super::iteration::descendant_for_position( tree.root_node(), lines.text(), @@ -542,7 +542,7 @@ impl KernelResolver { candidates: None, file_path: cls.file_path.clone(), language: cls.language.clone(), - failure_reason: None, + failure_reason: None, generated: None, }; let mut owner: Option> = Some(Arc::new(cls.clone())); if let Some(holder) = holder { diff --git a/codegraph-kernel/src/resolve/go_asserted.rs b/codegraph-kernel/src/resolve/go_asserted.rs index 68c15daf0..2641b80f5 100644 --- a/codegraph-kernel/src/resolve/go_asserted.rs +++ b/codegraph-kernel/src/resolve/go_asserted.rs @@ -303,7 +303,7 @@ fn local_at(tree: &tree_sitter::Tree, source: &str, name: &str, line: i64, colum impl KernelResolver { pub(super) fn go_local_declaration_at(&mut self, name: String, file_path: String, line: i64, column: i64) -> Result> { let r = ResolveRefIn { row_id: None, from_node_id: String::new(), reference_name: name.clone(), reference_kind: "references".into(), - line, column, candidates: None, file_path: file_path.clone(), language: "go".into(), failure_reason: None }; + line, column, candidates: None, file_path: file_path.clone(), language: "go".into(), failure_reason: None, generated: None }; let Some(file) = self.read_file(&file_path) else { return Ok(None) }; let Some(tree) = self.parsed_tree(&file, &r) else { return Ok(None) }; Ok(local_at(&tree, file.text(), &name, line, column)) diff --git a/codegraph-kernel/src/resolve/js_objects_upstream.rs b/codegraph-kernel/src/resolve/js_objects_upstream.rs index 0f9f80945..60114eb03 100644 --- a/codegraph-kernel/src/resolve/js_objects_upstream.rs +++ b/codegraph-kernel/src/resolve/js_objects_upstream.rs @@ -165,7 +165,7 @@ impl KernelResolver { let site = ResolveRefIn { row_id: None, from_node_id: n.id.clone(), reference_name: root.to_string(), reference_kind: "references".to_string(), line: n.start_line, column: n.start_column, candidates: None, - file_path: n.file_path.clone(), language: n.language.clone(), failure_reason: None }; + file_path: n.file_path.clone(), language: n.language.clone(), failure_reason: None, generated: None }; if self.js_root_binding_scope(&site, root).is_some() { return Ok(false); } let global = if global_host(n) { let host = root; diff --git a/codegraph-kernel/src/resolve/kotlin_calls.rs b/codegraph-kernel/src/resolve/kotlin_calls.rs index b1199f7bc..7d5901f29 100644 --- a/codegraph-kernel/src/resolve/kotlin_calls.rs +++ b/codegraph-kernel/src/resolve/kotlin_calls.rs @@ -91,7 +91,7 @@ impl KernelResolver { let call = re!(r"(?:^|[^\w$])([a-z_]\w*)\s*(?:<[^<>{}]*>)?\s*(?:\([^(){}]*\))?\s*$").captures(&pending).map(|m| m[1].to_string()); if let Some(call) = call.filter(|name| !matches!(name.as_str(), "if" | "else" | "for" | "while" | "do" | "when" | "try" | "catch" | "finally" | "init" | "get" | "set" | "constructor" | "fun" | "class" | "object" | "interface" | "return" | "by" | "lazy" | "apply" | "run" | "also" | "let" | "with" | "use")) { if !type_lambdas { stack.push((line, vec![UNTYPED_LAMBDA.to_string()])); pending.clear(); continue; } - let site = ResolveRefIn { row_id: None, from_node_id: String::new(), reference_name: call.clone(), reference_kind: "calls".to_string(), line, column: 0, candidates: None, file_path: file.to_string(), language: "kotlin".to_string(), failure_reason: None }; + let site = ResolveRefIn { row_id: None, from_node_id: String::new(), reference_name: call.clone(), reference_kind: "calls".to_string(), line, column: 0, candidates: None, file_path: file.to_string(), language: "kotlin".to_string(), failure_reason: None, generated: None }; let mut owners: HashSet = stack.iter().flat_map(|(_, names): &(i64, Vec)| names.iter().filter(|name| name.as_str() != "*").cloned()).collect(); // An explicit typed receiver also proves dispatch: m.single { ... }, m: Module. // One shared pattern with the callee captured, not a pattern compiled per call name: diff --git a/codegraph-kernel/src/resolve/kotlin_type_scope.rs b/codegraph-kernel/src/resolve/kotlin_type_scope.rs index 7e5e6ce8a..41cbdc912 100644 --- a/codegraph-kernel/src/resolve/kotlin_type_scope.rs +++ b/codegraph-kernel/src/resolve/kotlin_type_scope.rs @@ -67,7 +67,7 @@ impl KernelResolver { candidates: None, file_path: declaration.file_path.clone(), language: "kotlin".to_string(), - failure_reason: None, + failure_reason: None, generated: None, }; let Some(tree) = self.parsed_tree(&source, &site) else { continue; diff --git a/codegraph-kernel/src/resolve/mod.rs b/codegraph-kernel/src/resolve/mod.rs index fd48aeb95..81fe5e357 100644 --- a/codegraph-kernel/src/resolve/mod.rs +++ b/codegraph-kernel/src/resolve/mod.rs @@ -29,7 +29,7 @@ use crate::native::{Error, Result}; #[cfg(feature = "addon")] use napi_derive::napi; use regex::Regex; -use rusqlite::{Connection, OpenFlags}; +use rusqlite::{Connection, OpenFlags, OptionalExtension}; use std::cell::{OnceCell, RefCell}; use std::collections::{HashMap, HashSet, VecDeque}; use std::rc::Rc; @@ -175,6 +175,11 @@ pub struct ResolveRefIn { pub file_path: String, pub language: String, pub failure_reason: Option, + /// Files marked generated at index time (minified bundles, codegen + /// output) get a bounded name-match: their same-name candidate pools + /// are minification artifacts, and scanning all of them costs ~10× a + /// normal ref. Absent/NULL means false. + pub generated: Option, } impl ResolveRefIn { @@ -1086,10 +1091,11 @@ impl KernelResolver { ) -> Result> { const PREREQ_KINDS: &str = "('imports','extends','implements')"; let sql = format!( - "SELECT id, from_node_id, reference_name, reference_kind, line, col, \ - candidates, file_path, language, failure_reason FROM unresolved_refs \ - WHERE status = 'pending' AND id > ?1 \ - AND reference_kind {} {} ORDER BY id LIMIT ?2", + "SELECT r.id, r.from_node_id, r.reference_name, r.reference_kind, r.line, r.col, \ + r.candidates, r.file_path, r.language, r.failure_reason, f.generated \ + FROM unresolved_refs r LEFT JOIN files f ON f.path = r.file_path \ + WHERE r.status = 'pending' AND r.id > ?1 \ + AND r.reference_kind {} {} ORDER BY r.id LIMIT ?2", if prerequisites { "IN" } else { "NOT IN" }, PREREQ_KINDS ); @@ -1110,6 +1116,7 @@ impl KernelResolver { row.get::<_, Option>(7)?.unwrap_or_default(), row.get::<_, Option>(8)?.unwrap_or_default(), row.get::<_, Option>(9)?, + row.get::<_, Option>(10)?.map(|g| g != 0), )) }) .map_err(|e| Error::from_reason(e.to_string()))?; @@ -1119,13 +1126,14 @@ impl KernelResolver { } drop(stmt); let mut out = Vec::new(); - for (row_id, from_node_id, reference_name, reference_kind, line, column, candidates, mut file_path, mut language, failure_reason) in collected + for (row_id, from_node_id, reference_name, reference_kind, line, column, candidates, mut file_path, mut language, failure_reason, mut generated) in collected { // Denormalized fallback — mirrors UnresolvedRef construction in // resolveBatchYielding: `raw.filePath || nodePath`, // `raw.language || nodeLang` — falsy (empty) only; a stored // 'unknown' passes through unchanged. - if file_path.is_empty() || language.is_empty() { + let stored_path_empty = file_path.is_empty(); + if stored_path_empty || language.is_empty() { if let Some(node) = self.node_by_id(&from_node_id)? { if file_path.is_empty() { file_path = node.file_path.clone(); @@ -1135,6 +1143,16 @@ impl KernelResolver { } } } + // The files.generated join could not match an empty stored + // path: look the flag up under the path the fallback supplied, + // as generatedPredicateFor does on the TS side. + if stored_path_empty && generated != Some(true) { + generated = self.file_generated(&file_path)?; + } + // Union with the path convention — same rule as + // generatedPredicateFor on the TS side: the DB flag OR a + // generated-looking filename. + let generated = generated == Some(true) || crate::textutil::is_generated_file(&file_path); out.push(ResolveRefIn { row_id: Some(row_id), from_node_id, @@ -1146,11 +1164,26 @@ impl KernelResolver { file_path, language, failure_reason, + generated: Some(generated), }); } Ok(out) } + /// `SELECT generated FROM files WHERE path = ?` — the join's fallback + /// when a ref's stored file_path was empty (see read_pending_batch). + fn file_generated(&mut self, path: &str) -> Result> { + if path.is_empty() { + return Ok(None); + } + let hit = self + .conn()? + .query_row("SELECT generated FROM files WHERE path = ?1", [path], |row| row.get::<_, i64>(0)) + .optional() + .map_err(|e| Error::from_reason(e.to_string()))?; + Ok(hit.map(|g| g != 0)) + } + /// The `settle` stage — one verdict per ref in input order. #[cfg_attr(feature = "addon", napi)] pub fn resolve_chunk(&mut self, refs: Vec) -> Result> { diff --git a/codegraph-kernel/src/resolve/names.rs b/codegraph-kernel/src/resolve/names.rs index 35000c2fd..d1ad0b09d 100644 --- a/codegraph-kernel/src/resolve/names.rs +++ b/codegraph-kernel/src/resolve/names.rs @@ -7,6 +7,14 @@ use super::name_scope::{can_name_in_type_position, is_bare_rust_name}; /// no repository class uses may claim: a guess, not a proven link. const PHP_UNSEEN_ANCESTOR_CONFIDENCE: f64 = 0.5; +/// Max same-name candidate pool a generated-file ref will evaluate: real +/// same-name pools sit in the tens; minified bundles push thousands of +/// artifacts into one pool and turn each ref's retain/scope chain +/// quadratic (pretix's fabric.min.js: ~950ms per 250-ref chunk vs a ~90ms +/// floor). Over the cap the name-match declines — unresolved, never a +/// truncated winner. +const GENERATED_NAME_POOL_CAP: usize = 256; + /// A module a bundler loads as data, not code: a stylesheet, an image, a GraphQL document, JSON, a font. const ASSET_MODULE: &str = r"(?i)\.(?:css|scss|sass|less|styl|stylus|pcss|graphql|gql|png|jpe?g|gif|svg|webp|avif|ico|bmp|tiff?|json5?|ya?ml|toml|md|mdx|txt|html?|xml|csv|woff2?|ttf|otf|eot|mp3|mp4|webm|ogg|wav|flac|pdf|wasm)$"; @@ -280,7 +288,7 @@ impl KernelResolver { row_id: None, from_node_id: candidate.id.clone(), reference_name: candidate.name.clone(), reference_kind: "calls".to_string(), line: candidate.start_line, column: candidate.start_column, candidates: None, file_path: candidate.file_path.clone(), - language: "rust".to_string(), failure_reason: None, + language: "rust".to_string(), failure_reason: None, generated: None, }; let is_trait = self.rust_self_context(&site).is_some_and(|cx| cx.trait_name.is_some()); self.rust_trait_memo.insert(candidate.id.clone(), is_trait); @@ -686,6 +694,26 @@ impl KernelResolver { } } let named = self.nodes_by_name(&r.reference_name)?; + // Big same-name pools make a bare match pathological: a name like + // `t` gets thousands of candidates whose retain/scope checks + // dominate a whole batch (pretix's fabric.min.js: ~950ms per + // 250-ref chunk vs a ~90ms floor). Two bounds, both declining the + // name-match entirely — `unresolved`, never a truncated winner — + // with the framework merge still applying downstream: + // * the ref's own file is generated-flagged (minified/codegen + // sources degrade by precedent); + // * the pool is dominated by generated nodes — an artifact pool + // cannot hold a real target anyway, so handwritten refs in + // vendored sources (d3.v6.js, fabric.js, pdf.worker.js — + // unminified, unflagged) get the same bound, while real big + // pools (Migration, __init__) proceed untouched. + if named.len() > GENERATED_NAME_POOL_CAP + && (r.generated == Some(true) + || named.iter().filter(|n| crate::textutil::is_generated_file(&n.file_path)).count() * 4 + >= named.len() * 3) + { + return Ok(None); + } // Macro constants are not callees (#1838). Where the name is a // function-like macro, the call can only reach the function another // build configuration compiles, never a same-named type @@ -973,6 +1001,15 @@ impl KernelResolver { } } let candidates = self.nodes_by_lower_name(&r.reference_name)?; + // Same bound as match_by_exact_name_uncapped: the lowercase pool is + // at least as wide, so decline before the retain chain. + if candidates.len() > GENERATED_NAME_POOL_CAP + && (r.generated == Some(true) + || candidates.iter().filter(|n| crate::textutil::is_generated_file(&n.file_path)).count() * 4 + >= candidates.len() * 3) + { + return Ok(None); + } let type_ref = self.is_dotnet_type_ref(r); let rust_bare = is_bare_rust_name(r); // Names are case-sensitive in every language but a handful: Rust's diff --git a/codegraph-kernel/src/resolve/php_scope.rs b/codegraph-kernel/src/resolve/php_scope.rs index cf29c1c6b..e784facaa 100644 --- a/codegraph-kernel/src/resolve/php_scope.rs +++ b/codegraph-kernel/src/resolve/php_scope.rs @@ -364,7 +364,7 @@ impl KernelResolver { return false; } let Some(lines) = self.read_file(&candidate.file_path) else { return false }; - let site = ResolveRefIn { row_id: None, from_node_id: candidate.id.clone(), reference_name: candidate.name.clone(), reference_kind: "references".to_string(), line: candidate.start_line, column: candidate.start_column, candidates: None, file_path: candidate.file_path.clone(), language: candidate.language.clone(), failure_reason: None }; + let site = ResolveRefIn { row_id: None, from_node_id: candidate.id.clone(), reference_name: candidate.name.clone(), reference_kind: "references".to_string(), line: candidate.start_line, column: candidate.start_column, candidates: None, file_path: candidate.file_path.clone(), language: candidate.language.clone(), failure_reason: None, generated: None }; if let Some(tree) = self.parsed_tree(&lines, &site) { let mut node = super::iteration::descendant_for_position(tree.root_node(), lines.text(), ((site.line - 1).max(0) as usize, site.column.max(0) as usize)); while let Some(parent) = node.parent() { diff --git a/codegraph-kernel/src/resolve/resolver_upstream.rs b/codegraph-kernel/src/resolve/resolver_upstream.rs index 3f12b26bc..ff754c5f5 100644 --- a/codegraph-kernel/src/resolve/resolver_upstream.rs +++ b/codegraph-kernel/src/resolve/resolver_upstream.rs @@ -455,7 +455,7 @@ impl KernelResolver { let Some(source) = self.read_file(file) else { return !name.starts_with('_') }; let site = ResolveRefIn { row_id: None, from_node_id: String::new(), reference_name: String::new(), reference_kind: "imports".to_string(), line: 1, column: 0, candidates: None, - file_path: file.to_string(), language: "python".to_string(), failure_reason: None }; + file_path: file.to_string(), language: "python".to_string(), failure_reason: None, generated: None }; let Some(tree) = self.parsed_tree(&source, &site) else { return !name.starts_with('_') }; let mut names = HashSet::new(); let mut literal = true; diff --git a/codegraph-kernel/src/resolve/rust_paths.rs b/codegraph-kernel/src/resolve/rust_paths.rs index a94be91e9..df222d757 100644 --- a/codegraph-kernel/src/resolve/rust_paths.rs +++ b/codegraph-kernel/src/resolve/rust_paths.rs @@ -142,7 +142,7 @@ impl KernelResolver { line: 1, column: 0, candidates: None, - failure_reason: None, + failure_reason: None, generated: None, }; let mut tree = self.parsed_tree(&source, &r)?; // pin-project exposes its struct declaration inside a macro token tree. diff --git a/src/extraction/kernel/loader.ts b/src/extraction/kernel/loader.ts index 301988bdc..607a324c2 100644 --- a/src/extraction/kernel/loader.ts +++ b/src/extraction/kernel/loader.ts @@ -231,6 +231,9 @@ export interface ResolveRefIn { filePath: string; language: string; failureReason?: string; + /** files.generated at index time — bounds the name-match pool for + * minified/codegen sources (see kernel GENERATED_NAME_POOL_CAP). */ + generated?: boolean; } export interface KernelCandidateOut { diff --git a/src/resolution/index.ts b/src/resolution/index.ts index 6586a5eae..646961d08 100644 --- a/src/resolution/index.ts +++ b/src/resolution/index.ts @@ -1896,18 +1896,27 @@ export class ReferenceResolver { /** One kernel `resolveChunk` over `refs`, in order. Throws on any native failure. */ private resolveChunkWithKernel(kernel: KernelResolverLike, refs: UnresolvedReference[]): ResolveOutcome[] { - const kernelRows: ResolveRefIn[] = refs.map((raw) => ({ - rowId: raw.rowId, - fromNodeId: raw.fromNodeId, - referenceName: raw.referenceName, - referenceKind: raw.referenceKind, - line: raw.line, - column: raw.column, - candidates: raw.candidates ? JSON.stringify(raw.candidates) : undefined, - filePath: raw.filePath || this.getFilePathFromNodeId(raw.fromNodeId) || '', - language: raw.language || this.getLanguageFromNodeId(raw.fromNodeId) || '', - failureReason: raw.failureReason, - })); + // generatedPredicateFor unions the indexed flag with the path convention — + // the kernel applies its bounded name-match only to generated sources + // (minified bundles' same-name pools cost ~10x a normal ref to scan). + const filePaths: string[] = refs.map((raw) => raw.filePath || this.getFilePathFromNodeId(raw.fromNodeId) || ''); + const isGenerated = this.queries.generatedPredicateFor(filePaths); + const kernelRows: ResolveRefIn[] = refs.map((raw, i) => { + const filePath = filePaths[i] ?? ''; + return { + rowId: raw.rowId, + fromNodeId: raw.fromNodeId, + referenceName: raw.referenceName, + referenceKind: raw.referenceKind, + line: raw.line, + column: raw.column, + candidates: raw.candidates ? JSON.stringify(raw.candidates) : undefined, + filePath, + language: raw.language || this.getLanguageFromNodeId(raw.fromNodeId) || '', + failureReason: raw.failureReason, + generated: isGenerated(filePath), + }; + }); const outcomes = kernel.resolveChunk(kernelRows); if (outcomes.length !== refs.length) { throw new Error(`kernel resolveChunk returned ${outcomes.length} outcomes for ${refs.length} refs`);