diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/ContractInspection.java b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/ContractInspection.java index bf4a13c6b5b5..12c6c49fb1ce 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/ContractInspection.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/ContractInspection.java @@ -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; diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/JavaMethodContractUtil.java b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/JavaMethodContractUtil.java index 23bfc4ae9225..7bcb7e214aa1 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/JavaMethodContractUtil.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/JavaMethodContractUtil.java @@ -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); } /** diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/JbContractAnnotationProvider.kt b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/JbContractAnnotationProvider.kt deleted file mode 100644 index 3651caa4ba68..000000000000 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/JbContractAnnotationProvider.kt +++ /dev/null @@ -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" -} \ No newline at end of file diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/JvmContractAnnotationProvider.kt b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/JvmContractAnnotationProvider.kt deleted file mode 100644 index d5e98262c077..000000000000 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/JvmContractAnnotationProvider.kt +++ /dev/null @@ -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 = - ExtensionPointName.Companion.create("com.intellij.codeInsight.contractProvider") - - @JvmStatic - fun qualifiedNames(): List { - return EP_NAME.extensionList.map { it.fqn } - } - - @JvmStatic - fun isMethodContract(fqn: String): Boolean { - return qualifiedNames().any { it == fqn } - } - } -} \ No newline at end of file diff --git a/java/java-impl/resources/META-INF/JavaPlugin.xml b/java/java-impl/resources/META-INF/JavaPlugin.xml index 89e7584b496a..ab448d5bc864 100644 --- a/java/java-impl/resources/META-INF/JavaPlugin.xml +++ b/java/java-impl/resources/META-INF/JavaPlugin.xml @@ -40,7 +40,6 @@ - @@ -2765,7 +2764,6 @@ - diff --git a/java/java-psi-api/src/com/intellij/codeInsight/StaticAnalysisAnnotationManager.java b/java/java-psi-api/src/com/intellij/codeInsight/StaticAnalysisAnnotationManager.java index ac5d5f694bcd..9c543423e23b 100644 --- a/java/java-psi-api/src/com/intellij/codeInsight/StaticAnalysisAnnotationManager.java +++ b/java/java-psi-api/src/com/intellij/codeInsight/StaticAnalysisAnnotationManager.java @@ -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; + } } diff --git a/java/java-tests/testSrc/com/intellij/spring/SpringChangeSignatureContractTest.kt b/java/java-tests/testSrc/com/intellij/spring/SpringChangeSignatureContractTest.kt new file mode 100644 index 000000000000..5ad594f45468 --- /dev/null +++ b/java/java-tests/testSrc/com/intellij/spring/SpringChangeSignatureContractTest.kt @@ -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 foo(@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) + } +} \ No newline at end of file diff --git a/java/java-tests/testSrc/com/intellij/spring/SpringConstantValueInspectionTest.kt b/java/java-tests/testSrc/com/intellij/spring/SpringConstantValueInspectionTest.kt new file mode 100644 index 000000000000..1459c5a4cefe --- /dev/null +++ b/java/java-tests/testSrc/com/intellij/spring/SpringConstantValueInspectionTest.kt @@ -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 (foo(null)) { } + } + } + """.trimIndent()) + myFixture.testHighlighting() + } + + override fun tearDown() { + try { + myFixture.disableInspections(inspection) + } + catch (e: Throwable) { + addSuppressedException(e) + } + finally { + super.tearDown() + } + } +} \ No newline at end of file diff --git a/java/java-tests/testSrc/com/intellij/spring/SpringContractIssuesInspectionTest.kt b/java/java-tests/testSrc/com/intellij/spring/SpringContractIssuesInspectionTest.kt new file mode 100644 index 000000000000..013100cf9102 --- /dev/null +++ b/java/java-tests/testSrc/com/intellij/spring/SpringContractIssuesInspectionTest.kt @@ -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("_ -> fail") + void foo() { + throw new AssertionError(); + } + } + """.trimIndent()) + myFixture.testHighlighting() + } + + override fun tearDown() { + try { + myFixture.disableInspections(inspection) + } + catch (e: Throwable) { + addSuppressedException(e) + } + finally { + super.tearDown() + } + } +} \ No newline at end of file diff --git a/java/java-tests/testSrc/com/intellij/spring/SpringJSpecifyLightHighlightingTestCase.kt b/java/java-tests/testSrc/com/intellij/spring/SpringJSpecifyLightHighlightingTestCase.kt new file mode 100644 index 000000000000..50dd7c6b15f9 --- /dev/null +++ b/java/java-tests/testSrc/com/intellij/spring/SpringJSpecifyLightHighlightingTestCase.kt @@ -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") + } + } +} \ No newline at end of file diff --git a/java/java-tests/testSrc/com/intellij/spring/SpringSafeDeleteContractTest.kt b/java/java-tests/testSrc/com/intellij/spring/SpringSafeDeleteContractTest.kt new file mode 100644 index 000000000000..606d79f4c5d5 --- /dev/null +++ b/java/java-tests/testSrc/com/intellij/spring/SpringSafeDeleteContractTest.kt @@ -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 o2) { + 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()) + } +} \ No newline at end of file