[java-analysis] ClassDataIndexer: provide an extension to register custom file suppressors

Should help BAZEL-852 IntelliJ incorrectly infers "Contract(pure = true)" annotations for methods from ijar-s produced by Bazel

GitOrigin-RevId: 2fdfc9166737d0aba84372a5a2d127aeeb747f9d
This commit is contained in:
Tagir Valeev
2025-02-18 09:36:18 +00:00
committed by intellij-monorepo-bot
parent f618ebaf5a
commit fff1c6af0c
5 changed files with 47 additions and 131 deletions
@@ -42,6 +42,8 @@
beanClass="com.intellij.codeInsight.daemon.impl.analysis.InjectedLanguageJavaReferenceSupplier" dynamic="true"/>
<extensionPoint qualifiedName="com.intellij.lang.jvm.annotations.marker.suppressor"
interface="com.intellij.codeInsight.NonCodeAnnotationsMarkerSuppressor" dynamic="true"/>
<extensionPoint qualifiedName="com.intellij.lang.jvm.bytecodeAnalysisSuppressor"
interface="com.intellij.codeInspection.bytecodeAnalysis.BytecodeAnalysisSuppressor" dynamic="true"/>
</extensionPoints>
<extensions defaultExtensionNs="com.intellij">
@@ -0,0 +1,29 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInspection.bytecodeAnalysis;
import com.intellij.openapi.extensions.ExtensionPointName;
import com.intellij.openapi.vfs.VirtualFile;
import org.jetbrains.annotations.NotNull;
/**
* An extension point interface to allow suppressing the bytecode analysis for specific class files.
* It's typically used when the class file is known to be a stub, which is replaced with an actual
* implementation at runtime. Analyzing such stubs may produce incorrect results
* (e.g., incorrect purity contract may be inferred for impure methods).
*/
public interface BytecodeAnalysisSuppressor {
ExtensionPointName<BytecodeAnalysisSuppressor> EP_NAME = ExtensionPointName.create("com.intellij.lang.jvm.bytecodeAnalysisSuppressor");
/**
* @return suppressor version. Override and increase the number every time the suppression algorithm changes.
*/
default int getVersion() {
return 1;
}
/**
* @param file file to check. It points to a class file, usually inside a jar
* @return true if the analysis for a given class file should be suppressed.
*/
boolean shouldSuppress(@NotNull VirtualFile file);
}
@@ -7,6 +7,7 @@ import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.text.StringHash;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.util.CachedValue;
@@ -37,6 +38,7 @@ import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BinaryOperator;
import java.util.function.Consumer;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static com.intellij.codeInspection.bytecodeAnalysis.Direction.*;
import static com.intellij.codeInspection.bytecodeAnalysis.Effects.VOLATILE_EFFECTS;
@@ -60,7 +62,9 @@ public final class ClassDataIndexer implements VirtualFileGist.GistCalculator<Ma
private static final int VERSION = 19; // change when inference algorithm changes
private static final int VERSION_MODIFIER = HardCodedPurity.AGGRESSIVE_HARDCODED_PURITY ? 1 : 0;
private static final int FINAL_VERSION = VERSION * 2 + VERSION_MODIFIER;
private static final int FINAL_VERSION = VERSION * 2 + VERSION_MODIFIER + StringHash.murmur(
BytecodeAnalysisSuppressor.EP_NAME.getExtensionList().stream().map(ep -> String.valueOf(ep.getVersion()))
.collect(Collectors.joining("-")));
private static final VirtualFileGist<Map<HMember, Equations>> ourGist = GistManager.getInstance().newVirtualFileGist(
"BytecodeAnalysisIndex", FINAL_VERSION, new BytecodeAnalysisIndex.EquationsExternalizer(), new ClassDataIndexer());
@@ -101,7 +105,8 @@ public final class ClassDataIndexer implements VirtualFileGist.GistCalculator<Ma
// Methods of GenericModel.class in Play framework throw UnsupportedOperationException
// However, it looks like they are replaced with something meaningful during compilation/runtime
// See IDEA-285334.
path.endsWith("!/play/db/jpa/GenericModel.class");
path.endsWith("!/play/db/jpa/GenericModel.class") ||
ContainerUtil.exists(BytecodeAnalysisSuppressor.EP_NAME.getExtensionList(), ep -> ep.shouldSuppress(file));
}
private static final Pattern ANDROID_JAR_PATH = Pattern.compile(
@@ -79,135 +79,6 @@
<item name='org.apache.velocity.runtime.parser.ParserConstants tokenImage'>
<annotation name='org.jetbrains.annotations.NotNull'/>
</item>
<item name='org.apache.velocity.runtime.parser.ParserTokenManager ParserTokenManager(org.apache.velocity.runtime.parser.CharStream)'>
<annotation name='org.jetbrains.annotations.Contract'>
<val name="pure" val="true"/>
</annotation>
</item>
<item name='org.apache.velocity.runtime.parser.ParserTokenManager boolean jjCanMove_0(int, int, int, long, long)'>
<annotation name='org.jetbrains.annotations.Contract'>
<val name="pure" val="true"/>
</annotation>
</item>
<item name='org.apache.velocity.runtime.parser.ParserTokenManager int jjStopAtPos(int, int)'>
<annotation name='org.jetbrains.annotations.Contract'>
<val name="mutates" val="&quot;this&quot;"/>
</annotation>
</item>
<item name='org.apache.velocity.runtime.parser.ParserTokenManager int jjStopStringLiteralDfa_0(int, long, long)'>
<annotation name='org.jetbrains.annotations.Contract'>
<val name="mutates" val="&quot;this&quot;"/>
</annotation>
</item>
<item name='org.apache.velocity.runtime.parser.ParserTokenManager int jjStopStringLiteralDfa_1(int, long, long)'>
<annotation name='org.jetbrains.annotations.Contract'>
<val name="mutates" val="&quot;this&quot;"/>
</annotation>
</item>
<item name='org.apache.velocity.runtime.parser.ParserTokenManager int jjStopStringLiteralDfa_10(int, long)'>
<annotation name='org.jetbrains.annotations.Contract'>
<val name="pure" val="true"/>
</annotation>
</item>
<item name='org.apache.velocity.runtime.parser.ParserTokenManager int jjStopStringLiteralDfa_11(int, long)'>
<annotation name='org.jetbrains.annotations.Contract'>
<val name="pure" val="true"/>
</annotation>
</item>
<item name='org.apache.velocity.runtime.parser.ParserTokenManager int jjStopStringLiteralDfa_2(int, long)'>
<annotation name='org.jetbrains.annotations.Contract'>
<val name="pure" val="true"/>
</annotation>
</item>
<item name='org.apache.velocity.runtime.parser.ParserTokenManager int jjStopStringLiteralDfa_3(int, long)'>
<annotation name='org.jetbrains.annotations.Contract'>
<val name="mutates" val="&quot;this&quot;"/>
</annotation>
</item>
<item name='org.apache.velocity.runtime.parser.ParserTokenManager int jjStopStringLiteralDfa_4(int, long)'>
<annotation name='org.jetbrains.annotations.Contract'>
<val name="mutates" val="&quot;this&quot;"/>
</annotation>
</item>
<item name='org.apache.velocity.runtime.parser.ParserTokenManager int jjStopStringLiteralDfa_5(int, long)'>
<annotation name='org.jetbrains.annotations.Contract'>
<val name="pure" val="true"/>
</annotation>
</item>
<item name='org.apache.velocity.runtime.parser.ParserTokenManager int jjStopStringLiteralDfa_6(int, long)'>
<annotation name='org.jetbrains.annotations.Contract'>
<val name="pure" val="true"/>
</annotation>
</item>
<item name='org.apache.velocity.runtime.parser.ParserTokenManager int jjStopStringLiteralDfa_7(int, long)'>
<annotation name='org.jetbrains.annotations.Contract'>
<val name="pure" val="true"/>
</annotation>
</item>
<item name='org.apache.velocity.runtime.parser.ParserTokenManager int jjStopStringLiteralDfa_8(int, long)'>
<annotation name='org.jetbrains.annotations.Contract'>
<val name="pure" val="true"/>
</annotation>
</item>
<item name='org.apache.velocity.runtime.parser.ParserTokenManager int jjStopStringLiteralDfa_9(int, long)'>
<annotation name='org.jetbrains.annotations.Contract'>
<val name="pure" val="true"/>
</annotation>
</item>
<item name='org.apache.velocity.runtime.parser.ParserTokenManager jjbitVec0'>
<annotation name='org.jetbrains.annotations.NotNull'/>
</item>
<item name='org.apache.velocity.runtime.parser.ParserTokenManager jjbitVec2'>
<annotation name='org.jetbrains.annotations.NotNull'/>
</item>
<item name='org.apache.velocity.runtime.parser.ParserTokenManager jjnewLexState'>
<annotation name='org.jetbrains.annotations.NotNull'/>
</item>
<item name='org.apache.velocity.runtime.parser.ParserTokenManager jjnextStates'>
<annotation name='org.jetbrains.annotations.NotNull'/>
</item>
<item name='org.apache.velocity.runtime.parser.ParserTokenManager jjstrLiteralImages'>
<annotation name='org.jetbrains.annotations.NotNull'/>
</item>
<item name='org.apache.velocity.runtime.parser.ParserTokenManager jjtoMore'>
<annotation name='org.jetbrains.annotations.NotNull'/>
</item>
<item name='org.apache.velocity.runtime.parser.ParserTokenManager jjtoSkip'>
<annotation name='org.jetbrains.annotations.NotNull'/>
</item>
<item name='org.apache.velocity.runtime.parser.ParserTokenManager jjtoSpecial'>
<annotation name='org.jetbrains.annotations.NotNull'/>
</item>
<item name='org.apache.velocity.runtime.parser.ParserTokenManager jjtoToken'>
<annotation name='org.jetbrains.annotations.NotNull'/>
</item>
<item name='org.apache.velocity.runtime.parser.ParserTokenManager lexStateNames'>
<annotation name='org.jetbrains.annotations.NotNull'/>
</item>
<item name='org.apache.velocity.runtime.parser.ParserTokenManager org.apache.velocity.runtime.parser.Token jjFillToken()'>
<annotation name='org.jetbrains.annotations.Contract'>
<val val="&quot;-&gt;new&quot;"/>
</annotation>
<annotation name='org.jetbrains.annotations.NotNull'/>
</item>
<item name='org.apache.velocity.runtime.parser.ParserTokenManager void SkipLexicalActions(org.apache.velocity.runtime.parser.Token) 0'>
<annotation name='org.jetbrains.annotations.Nullable'/>
</item>
<item name='org.apache.velocity.runtime.parser.ParserTokenManager void SwitchTo(int)'>
<annotation name='org.jetbrains.annotations.Contract'>
<val name="mutates" val="&quot;this&quot;"/>
</annotation>
</item>
<item name='org.apache.velocity.runtime.parser.ParserTokenManager void setDebugStream(java.io.PrintStream)'>
<annotation name='org.jetbrains.annotations.Contract'>
<val name="mutates" val="&quot;this&quot;"/>
</annotation>
</item>
<item name='org.apache.velocity.runtime.parser.ParserTokenManager.ParserState ParserState()'>
<annotation name='org.jetbrains.annotations.Contract'>
<val name="pure" val="true"/>
</annotation>
</item>
<item name='org.apache.velocity.runtime.parser.ParserTreeConstants jjtNodeName'>
<annotation name='org.jetbrains.annotations.NotNull'/>
</item>
@@ -5,9 +5,11 @@ import com.intellij.codeInsight.*;
import com.intellij.codeInsight.daemon.GutterMark;
import com.intellij.codeInsight.daemon.LineMarkerSettings;
import com.intellij.codeInsight.daemon.impl.LineMarkerSettingsImpl;
import com.intellij.codeInspection.bytecodeAnalysis.BytecodeAnalysisSuppressor;
import com.intellij.codeInspection.bytecodeAnalysis.ClassDataIndexer;
import com.intellij.codeInspection.bytecodeAnalysis.ProjectBytecodeAnalysis;
import com.intellij.codeInspection.dataFlow.JavaMethodContractUtil;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ex.PathManagerEx;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.editor.Document;
@@ -28,6 +30,7 @@ import com.intellij.psi.util.PsiFormatUtil;
import com.intellij.psi.xml.XmlFile;
import com.intellij.testFramework.LightProjectDescriptor;
import com.intellij.testFramework.PsiTestUtil;
import com.intellij.testFramework.ServiceContainerUtil;
import com.intellij.testFramework.fixtures.DefaultLightProjectDescriptor;
import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase;
import com.intellij.testFramework.fixtures.MavenDependencyUtil;
@@ -46,6 +49,8 @@ public class BytecodeAnalysisIntegrationTest extends LightJavaCodeInsightFixture
private static final String INFERRED_TEST_METHOD =
"org.apache.velocity.util.ExceptionUtils java.lang.Throwable createWithCause(java.lang.Class, java.lang.String, java.lang.Throwable)";
private static final String EXTERNAL_TEST_METHOD = "java.lang.String String(java.lang.String)";
private static final BytecodeAnalysisSuppressor TEST_SUPPRESSOR = file -> file.getPath().endsWith("/ParserTokenManager.class");
private static final DefaultLightProjectDescriptor PROJECT_DESCRIPTOR = new DefaultLightProjectDescriptor() {
@Override
@@ -133,6 +138,8 @@ public class BytecodeAnalysisIntegrationTest extends LightJavaCodeInsightFixture
public void testSdkAndLibAnnotations() {
PsiPackage rootPackage = JavaPsiFacade.getInstance(getProject()).findPackage("");
ServiceContainerUtil.registerExtension(ApplicationManager.getApplication(), BytecodeAnalysisSuppressor.EP_NAME, TEST_SUPPRESSOR,
getTestRootDisposable());
assertNotNull(rootPackage);
List<String> diffs = new ArrayList<>();
@@ -212,6 +219,8 @@ public class BytecodeAnalysisIntegrationTest extends LightJavaCodeInsightFixture
public void _testExportInferredAnnotations() {
PsiPackage rootPackage = JavaPsiFacade.getInstance(getProject()).findPackage("");
assertNotNull(rootPackage);
ServiceContainerUtil.registerExtension(ApplicationManager.getApplication(), BytecodeAnalysisSuppressor.EP_NAME, TEST_SUPPRESSOR,
getTestRootDisposable());
VirtualFile annotationsRoot = getAnnotationsRoot();
JavaRecursiveElementVisitor visitor = new PackageVisitor(GlobalSearchScope.moduleWithLibrariesScope(getModule())) {