IJPL-251723 resolve long FileReference context chains iteratively

Resolving a FileReference recursed into its context reference (the preceding
path segment), which recursed into its own, down to index 0 at ~15 stack frames
per segment. A path with thousands of segments -- a generated path, a long
command line in a string literal, a base64 data URI -- therefore ended in a
StackOverflowError. Neither existing guard applies: multiResolve asks
ResolveCache not to prevent recursion, and the RecursionManager key in
innerResolve is the reference instance, so it only catches a reference
re-entering itself, never a chain of distinct references.

Beyond 64 segments the chain is now resolved bottom-up before the reference
itself, so every level finds its context reference already cached and returns
without recursing. The loop resolves through a second resolver instance that
skips the walk; otherwise every level would rescan the whole prefix and the
pre-warm would cost O(n^2) cache lookups.

GitOrigin-RevId: 62b0966db7ff35395197d5149e53bf8354e92fd7
This commit is contained in:
Max Medvedev
2026-08-14 14:38:02 +00:00
committed by intellij-monorepo-bot
parent 9d256f09c4
commit 363bd15762
2 changed files with 51 additions and 2 deletions
@@ -2,7 +2,10 @@
package com.intellij.java.codeInsight.daemon;
import com.intellij.ide.highlighter.JavaFileType;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiReference;
import com.intellij.psi.impl.source.resolve.reference.impl.providers.FileReference;
import com.intellij.psi.impl.source.resolve.reference.impl.providers.FileReferenceSet;
import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase;
public class PathReferenceTest extends LightJavaCodeInsightFixtureTestCase {
@@ -17,4 +20,14 @@ public class PathReferenceTest extends LightJavaCodeInsightFixtureTestCase {
PsiReference[] references = reference.getElement().getReferences();
assertEquals(1, references.length);
}
public void testDeepPathDoesNotOverflowStack() {
// resolving reference #i used to recurse into #i-1, so a path with many segments blew the stack
String path = "dir/".repeat(20_000) + "file.txt";
PsiFile file = myFixture.configureByText("a.txt", path);
FileReferenceSet set = new FileReferenceSet(path, file, 0, null, true);
FileReference last = set.getLastReference();
assertNotNull(last);
assertEmpty(last.multiResolve(false));
}
}
@@ -66,6 +66,12 @@ public class FileReference implements PsiFileReference, FileReferenceOwner, PsiP
public static final FileReference[] EMPTY = new FileReference[0];
/**
* The number of path segments beyond which the context chain is resolved iteratively
* instead of relying on the recursion in {@link #getContexts()}.
*/
private static final int ITERATIVE_CONTEXT_RESOLVE_THRESHOLD = 64;
private final int myIndex;
private TextRange myRange;
private final String myText;
@@ -134,6 +140,24 @@ public class FileReference implements PsiFileReference, FileReferenceOwner, PsiP
return ResolveCache.getInstance(file.getProject()).resolveWithCaching(this, MyResolver.INSTANCE, false, false, file);
}
/**
* {@link #getContexts()} resolves the context reference, which resolves its own context reference and so on down to
* index 0, so a path with many segments would exhaust the stack. Resolve a long chain bottom-up instead: every level
* then finds its context reference already cached and returns without recursing.
* <p>
* Called only on a cache miss, from {@link MyResolver#INSTANCE}. The references resolved here go through
* {@link MyResolver#WITHOUT_CONTEXT_CHAIN} so that they don't walk the chain again.
*/
private void resolveContextChain(@NotNull PsiFile containingFile) {
if (myIndex < ITERATIVE_CONTEXT_RESOLVE_THRESHOLD) return;
ResolveCache cache = ResolveCache.getInstance(containingFile.getProject());
FileReference[] references = myFileReferenceSet.getAllReferences();
int limit = Math.min(myIndex, references.length);
for (int i = 0; i < limit; i++) {
cache.resolveWithCaching(references[i], MyResolver.WITHOUT_CONTEXT_CHAIN, false, false, containingFile);
}
}
protected ResolveResult @NotNull [] innerResolve(boolean caseSensitive, @NotNull PsiFile containingFile) {
final String referenceText = getText();
if (referenceText.isEmpty() && myIndex == 0) {
@@ -599,11 +623,23 @@ public class FileReference implements PsiFileReference, FileReferenceOwner, PsiP
return myFileReferenceSet.getLastReference();
}
private static class MyResolver implements ResolveCache.PolyVariantContextResolver<FileReference> {
static final MyResolver INSTANCE = new MyResolver();
private static final class MyResolver implements ResolveCache.PolyVariantContextResolver<FileReference> {
static final MyResolver INSTANCE = new MyResolver(true);
/** Used while a context chain is being resolved bottom-up, see {@link FileReference#resolveContextChain}. */
static final MyResolver WITHOUT_CONTEXT_CHAIN = new MyResolver(false);
private final boolean myResolveContextChain;
private MyResolver(boolean resolveContextChain) {
myResolveContextChain = resolveContextChain;
}
@Override
public ResolveResult @NotNull [] resolve(@NotNull FileReference ref, @NotNull PsiFile containingFile, boolean incompleteCode) {
if (myResolveContextChain) {
ref.resolveContextChain(containingFile);
}
return ref.innerResolve(ref.getFileReferenceSet().isCaseSensitive(), containingFile);
}
}