From 363bd15762e3aa52c39807d4739dfa7bb71f5367 Mon Sep 17 00:00:00 2001 From: Max Medvedev Date: Mon, 10 Aug 2026 18:47:37 +0200 Subject: [PATCH] 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 --- .../codeInsight/daemon/PathReferenceTest.java | 13 ++++++ .../impl/providers/FileReference.java | 40 ++++++++++++++++++- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/PathReferenceTest.java b/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/PathReferenceTest.java index fcc7171e82bb..632b3c8ba990 100644 --- a/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/PathReferenceTest.java +++ b/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/PathReferenceTest.java @@ -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)); + } } diff --git a/platform/analysis-impl/src/com/intellij/psi/impl/source/resolve/reference/impl/providers/FileReference.java b/platform/analysis-impl/src/com/intellij/psi/impl/source/resolve/reference/impl/providers/FileReference.java index 4426ad9f8cb0..6d41cc93bf9a 100644 --- a/platform/analysis-impl/src/com/intellij/psi/impl/source/resolve/reference/impl/providers/FileReference.java +++ b/platform/analysis-impl/src/com/intellij/psi/impl/source/resolve/reference/impl/providers/FileReference.java @@ -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. + *

+ * 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 { - static final MyResolver INSTANCE = new MyResolver(); + private static final class MyResolver implements ResolveCache.PolyVariantContextResolver { + 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); } }