fix(resolution): stop per-call config-key scan that made large Java/Kotlin (Spring) indexes take ~1h (#1180) (#1210)
On a large Java/Kotlin Spring monorepo, reference resolution — not extraction —
dominated a full index (Spring Boot's ~9,650-file tree: extraction 62s,
resolution ~26min). The Spring framework resolver ran an uncached
getNodesByKind('constant') full scan + canonicalConfigKey() filter for EVERY
dotted `calls` ref (every list.add(), builder.build(), receiver.method()),
because the config-key branch gated only on "dotted java/kotlin", not on ref
kind. With ~1,100 constant nodes × ~200k dotted calls that is ~200M wasted
row-fetches/allocations.
Fixes, one theme — config-key constants bind config `references`, never `calls`:
- frameworks/java.ts: gate the Spring config-key branch on
referenceKind === 'references' (what @Value/@ConfigurationProperties emit) so
the `calls` flood skips the scan.
- name-matcher.ts: a `calls` ref no longer resolves to a yaml/properties config
node via matchByQualifiedName (service.process() vs the yaml key
service.process) — a wrong edge that also hid the real callee; it now falls
through to method resolution.
- resolution/index.ts: cache getNodesByKind in the resolver context (same
lifetime as nameCache). Fixes the same uncached-per-ref scan in the Drupal
hook_ resolver and is defense-in-depth for the Spring :prefix branch.
Measured (Spring Boot): resolution 269s→16.5s on a 4.3k-file module (16×) and
~26min→44.7s on the full 9.6k-file tree (~35×); graph byte-identical, full suite
passes. Adds a regression test (same key, two ref kinds, opposite outcomes).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
e65a39746c
commit
625b4fe921
@@ -412,7 +412,21 @@ export function matchByQualifiedName(
|
||||
return null;
|
||||
}
|
||||
|
||||
const candidates = context.getNodesByQualifiedName(ref.referenceName);
|
||||
// A method call `receiver.method()` can share an exact qualified name with a
|
||||
// config-file key: `service.process()` (a `calls` ref named `service.process`)
|
||||
// vs the yaml key `service.process`. Config keys are bound to their code refs
|
||||
// upstream by the framework resolvers (`@Value` → `references`); a `calls` ref
|
||||
// must never resolve to a yaml/properties config node — that's a wrong edge
|
||||
// AND it hides the real callee. Drop those from both the exact and the partial
|
||||
// candidate sets so resolution falls through to method resolution below (#1180).
|
||||
const keepForRef = (nodes: Node[]): Node[] =>
|
||||
ref.referenceKind === 'calls'
|
||||
? nodes.filter(
|
||||
(n) => !(n.kind === 'constant' && (n.language === 'yaml' || n.language === 'properties')),
|
||||
)
|
||||
: nodes;
|
||||
|
||||
const candidates = keepForRef(context.getNodesByQualifiedName(ref.referenceName));
|
||||
|
||||
if (candidates.length === 1) {
|
||||
return {
|
||||
@@ -444,8 +458,7 @@ export function matchByQualifiedName(
|
||||
const parts = ref.referenceName.split(/[:.]/);
|
||||
const lastName = parts[parts.length - 1];
|
||||
if (lastName) {
|
||||
const partialCandidates = context
|
||||
.getNodesByName(lastName)
|
||||
const partialCandidates = keepForRef(context.getNodesByName(lastName))
|
||||
.filter((candidate) => candidate.qualifiedName.endsWith(ref.referenceName));
|
||||
const chosen = preferCallSiteFile(partialCandidates, ref.filePath)[0];
|
||||
if (chosen) {
|
||||
|
||||
Reference in New Issue
Block a user