[java] Migrate contract provider extension point to hard coded values

#IDEA-366120 #IJ-CR-155341

(cherry picked from commit f0a38cf77f2a636e4d2b58f93f6d225d57d5738f)

GitOrigin-RevId: 34263eb95a904729580bd9180dbbad2c1124ea33
This commit is contained in:
Bart van Helvert
2025-02-23 18:13:56 +01:00
committed by intellij-monorepo-bot
parent edfee4abbf
commit 09b55a16cc
11 changed files with 182 additions and 44 deletions

View File

@@ -1,10 +1,7 @@
// 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.dataFlow;
import com.intellij.codeInsight.AnnotationUtil;
import com.intellij.codeInsight.Nullability;
import com.intellij.codeInsight.NullabilityAnnotationInfo;
import com.intellij.codeInsight.NullableNotNullManager;
import com.intellij.codeInsight.*;
import com.intellij.codeInspection.AbstractBaseJavaLocalInspectionTool;
import com.intellij.codeInspection.ProblemsHolder;
import com.intellij.codeInspection.dataFlow.StandardMethodContract.ValueConstraint;
@@ -52,7 +49,10 @@ public final class ContractInspection extends AbstractBaseJavaLocalInspectionToo
public void visitAnnotation(@NotNull PsiAnnotation annotation) {
String qualifiedName = annotation.getQualifiedName();
if (qualifiedName == null) return;
if (!JvmContractAnnotationProvider.isMethodContract(qualifiedName)) return;
if (!ContainerUtil.exists(
StaticAnalysisAnnotationManager.getInstance().getKnownContractAnnotations(),
fqn -> fqn.equals(qualifiedName))
) return;
PsiMethod method = PsiTreeUtil.getParentOfType(annotation, PsiMethod.class);
if (method == null) return;

View File

@@ -1,10 +1,7 @@
// 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.dataFlow;
import com.intellij.codeInsight.AnnotationUtil;
import com.intellij.codeInsight.Nullability;
import com.intellij.codeInsight.NullabilityAnnotationInfo;
import com.intellij.codeInsight.NullableNotNullManager;
import com.intellij.codeInsight.*;
import com.intellij.java.library.JavaLibraryModificationTracker;
import com.intellij.openapi.roots.ProjectFileIndex;
import com.intellij.openapi.util.text.StringUtil;
@@ -29,7 +26,7 @@ public final class JavaMethodContractUtil {
private JavaMethodContractUtil() {}
/**
* @deprecated To support contracts from different libraries please use {@link JvmContractAnnotationProvider}
* @deprecated To support contracts from different libraries please use {@link StaticAnalysisAnnotationManager#getKnownContractAnnotations}
*/
@Deprecated
public static final String ORG_JETBRAINS_ANNOTATIONS_CONTRACT = Contract.class.getName();
@@ -235,7 +232,9 @@ public final class JavaMethodContractUtil {
* @return a found annotation (null if not found)
*/
public static @Nullable PsiAnnotation findContractAnnotation(@NotNull PsiMethod method, boolean skipExternal) {
return AnnotationUtil.findAnnotationInHierarchy(method, new HashSet<>(JvmContractAnnotationProvider.qualifiedNames()), skipExternal);
return AnnotationUtil.findAnnotationInHierarchy(method,
Set.of(StaticAnalysisAnnotationManager.getInstance().getKnownContractAnnotations()),
skipExternal);
}
/**
@@ -245,7 +244,9 @@ public final class JavaMethodContractUtil {
* @return a found annotation (null if not found)
*/
public static @Nullable PsiAnnotation findContractAnnotation(@NotNull PsiMethod method) {
return AnnotationUtil.findAnnotationInHierarchy(method, new HashSet<>(JvmContractAnnotationProvider.qualifiedNames()), false);
return AnnotationUtil.findAnnotationInHierarchy(method,
Set.of(StaticAnalysisAnnotationManager.getInstance().getKnownContractAnnotations()),
false);
}
/**

View File

@@ -1,6 +0,0 @@
// 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.dataFlow
class JbContractAnnotationProvider : JvmContractAnnotationProvider {
override val fqn: String = "org.jetbrains.annotations.Contract"
}

View File

@@ -1,23 +0,0 @@
// 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.dataFlow
import com.intellij.openapi.extensions.ExtensionPointName
interface JvmContractAnnotationProvider {
val fqn: String
companion object {
private val EP_NAME: ExtensionPointName<JvmContractAnnotationProvider> =
ExtensionPointName.Companion.create<JvmContractAnnotationProvider>("com.intellij.codeInsight.contractProvider")
@JvmStatic
fun qualifiedNames(): List<String> {
return EP_NAME.extensionList.map { it.fqn }
}
@JvmStatic
fun isMethodContract(fqn: String): Boolean {
return qualifiedNames().any { it == fqn }
}
}
}

View File

@@ -39,7 +39,6 @@
</projectListeners>
<extensionPoints>
<extensionPoint qualifiedName="com.intellij.codeInsight.contractProvider" interface="com.intellij.codeInspection.dataFlow.JvmContractAnnotationProvider" dynamic="true"/>
<extensionPoint qualifiedName="com.intellij.methodImplementor" interface="com.intellij.codeInsight.MethodImplementor" dynamic="true"/>
<extensionPoint qualifiedName="com.intellij.javaExpressionSurrounder"
interface="com.intellij.codeInsight.generation.surroundWith.JavaExpressionSurrounder" dynamic="true"/>
@@ -2762,7 +2761,6 @@
<registryKey defaultValue="false" description="Suggested refactoring from call-site in Java"
key="ide.java.refactoring.suggested.call.site"/>
<dataflowIRProvider language="JAVA" implementationClass="com.intellij.codeInspection.dataFlow.java.JavaDataFlowIRProvider"/>
<codeInsight.contractProvider implementation="com.intellij.codeInspection.dataFlow.JbContractAnnotationProvider"/>
<java.effectively.final.fixer implementation="com.intellij.codeInsight.daemon.impl.quickfix.makefinal.MoveInitializerToIfBranchFixer"/>
<java.effectively.final.fixer implementation="com.intellij.codeInspection.streamMigration.ConvertToStreamFixer"/>
<postStartupActivity implementation="com.intellij.ide.FileNotInSourceRootChecker"/>

View File

@@ -1,4 +1,4 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// 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.codeInsight;
import com.intellij.openapi.application.ApplicationManager;
@@ -24,6 +24,11 @@ public final class StaticAnalysisAnnotationManager {
"org.gradle.api.Incubating"
};
private static final String[] KNOWN_CONTRACT_ANNOTATIONS = {
"org.jetbrains.annotations.Contract",
"org.springframework.lang.Contract"
};
public static StaticAnalysisAnnotationManager getInstance() {
return ApplicationManager.getApplication().getService(StaticAnalysisAnnotationManager.class);
}
@@ -35,4 +40,11 @@ public final class StaticAnalysisAnnotationManager {
public @NotNull String @NotNull [] getKnownUnstableApiAnnotations() {
return KNOWN_UNSTABLE_API_ANNOTATIONS;
}
/**
* @return array of contract annotations that uses the contract syntax specified by {@link org.jetbrains.annotations.Contract}
*/
public @NotNull String @NotNull [] getKnownContractAnnotations() {
return KNOWN_CONTRACT_ANNOTATIONS;
}
}

View File

@@ -0,0 +1,28 @@
// 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.spring
import com.intellij.ide.highlighter.JavaFileType
import com.intellij.psi.PsiMethod
import com.intellij.refactoring.changeSignature.JavaChangeSignatureDialog
class SpringChangeSignatureContractTest : SpringJSpecifyLightHighlightingTestCase() {
fun `test signature string contains contract annotation`() {
myFixture.configureByText(JavaFileType.INSTANCE, """
import org.jspecify.annotations.Nullable;
import org.springframework.lang.Contract;
class Baz {
@Contract("null -> false")
boolean fo<caret>o(@Nullable String name) {
return true;
}
}
""".trimIndent())
val psiMethod = myFixture.elementAtCaret as PsiMethod
val actualSignature = JavaChangeSignatureDialog(myFixture.project, psiMethod, false, psiMethod).calculateSignature()
assertEquals("""
@Contract("null -> false")
boolean foo(@Nullable String name)
""".trimIndent(), actualSignature)
}
}

View File

@@ -0,0 +1,42 @@
// 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.spring
import com.intellij.codeInspection.dataFlow.ConstantValueInspection
import com.intellij.ide.highlighter.JavaFileType
class SpringConstantValueInspectionTest : SpringJSpecifyLightHighlightingTestCase() {
private val inspection = ConstantValueInspection()
override fun setUp() {
super.setUp()
myFixture.enableInspections(inspection)
}
fun `test condition is always false`() {
myFixture.configureByText(JavaFileType.INSTANCE, """
class Baz {
@org.springframework.lang.Contract("null -> false")
boolean foo(@org.jspecify.annotations.Nullable String name) {
return true;
}
void bar() {
if (<warning descr="Condition 'foo(null)' is always 'false'">foo(null)</warning>) { }
}
}
""".trimIndent())
myFixture.testHighlighting()
}
override fun tearDown() {
try {
myFixture.disableInspections(inspection)
}
catch (e: Throwable) {
addSuppressedException(e)
}
finally {
super.tearDown()
}
}
}

View File

@@ -0,0 +1,38 @@
// 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.spring
import com.intellij.codeInspection.dataFlow.ContractInspection
import com.intellij.ide.highlighter.JavaFileType
class SpringContractIssuesInspectionTest : SpringJSpecifyLightHighlightingTestCase() {
private val inspection = ContractInspection()
override fun setUp() {
super.setUp()
myFixture.enableInspections(inspection)
}
fun `test parameter mismatch`() {
myFixture.configureByText(JavaFileType.INSTANCE, """
class Baz {
@org.springframework.lang.Contract("<warning descr="Method takes 0 parameters, while contract clause '_ -> fail' expects 1">_ -> fail</warning>")
void foo() {
throw new AssertionError();
}
}
""".trimIndent())
myFixture.testHighlighting()
}
override fun tearDown() {
try {
myFixture.disableInspections(inspection)
}
catch (e: Throwable) {
addSuppressedException(e)
}
finally {
super.tearDown()
}
}
}

View File

@@ -0,0 +1,19 @@
// 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.spring
import com.intellij.openapi.module.Module
import com.intellij.openapi.roots.ContentEntry
import com.intellij.openapi.roots.ModifiableRootModel
import com.intellij.pom.java.LanguageLevel
import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase
import com.intellij.testFramework.fixtures.MavenDependencyUtil
abstract class SpringJSpecifyLightHighlightingTestCase : LightJavaCodeInsightFixtureTestCase() {
override fun getProjectDescriptor() = object : ProjectDescriptor(LanguageLevel.HIGHEST) {
override fun configureModule(module: Module, model: ModifiableRootModel, contentEntry: ContentEntry) {
super.configureModule(module, model, contentEntry)
MavenDependencyUtil.addFromMaven(model, "org.springframework:spring-core:6.2.3")
MavenDependencyUtil.addFromMaven(model, "org.jspecify:jspecify:1.0.0")
}
}
}

View File

@@ -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.spring
import com.intellij.ide.highlighter.JavaFileType
import com.intellij.psi.PsiParameter
import com.intellij.refactoring.safeDelete.SafeDeleteHandler
class SpringSafeDeleteContractTest : SpringJSpecifyLightHighlightingTestCase() {
fun `test signature string contains contract annotation`() {
myFixture.configureByText(JavaFileType.INSTANCE, """
class Baz {
@org.springframework.lang.Contract("null, _ -> false")
public static boolean foo(Object o1, Object o<caret>2) {
return o1 != null;
}
}
""".trimIndent())
val psiParameter = myFixture.elementAtCaret as PsiParameter
SafeDeleteHandler.invoke(project, arrayOf(psiParameter), true)
myFixture.checkResult("""
class Baz {
@org.springframework.lang.Contract("null -> false")
public static boolean foo(Object o1) {
return o1 != null;
}
}
""".trimIndent())
}
}