diff --git a/CHANGELOG.md b/CHANGELOG.md index fdcbf3346..9b84db53d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -108,6 +108,7 @@ - fix: Require a configuration key's owning declaration to end at a segment boundary, so a declared `foo.bar` no longer owns the unrelated `foo.barbaz` — it decided the value type, the map-entry completion, the kebab-case exemption and whether an unknown key was reported at all - fix: support spring boot4 test annotations MockitoSpyBean/MockitoBean (#418) - fix: Event publisher navigation not working for child class if EvenListener has parent class (#419) +- fix: Stop Spring inspections from logging a `PluginException` caused by `IndexNotReadyException` while the IDE indexes: they stay inactive during indexing and run again once it finishes (#688) ### Spring Initializr - fix: Make `gradlew` and `mvnw` executable in a project generated through Spring Initializr, so the first `./gradlew` in a terminal no longer fails with "permission denied" (#60) diff --git a/modules/base/src/main/kotlin/com/explyt/inspection/InspectionAvailability.kt b/modules/base/src/main/kotlin/com/explyt/inspection/InspectionAvailability.kt new file mode 100644 index 000000000..201c8e84f --- /dev/null +++ b/modules/base/src/main/kotlin/com/explyt/inspection/InspectionAvailability.kt @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2026 Explyt Ltd + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.explyt.inspection + +import com.explyt.base.LibraryClassCache +import com.intellij.openapi.project.DumbService +import com.intellij.psi.PsiFile + +object InspectionAvailability { + + fun isIndexReady(file: PsiFile): Boolean = !DumbService.isDumb(file.project) + + fun hasLibraryClass(file: PsiFile, fqn: String): Boolean = + isIndexReady(file) && LibraryClassCache.searchForLibraryClass(file.project, fqn) != null +} diff --git a/modules/base/src/main/kotlin/com/explyt/inspection/SpringBaseLocalInspectionTool.kt b/modules/base/src/main/kotlin/com/explyt/inspection/SpringBaseLocalInspectionTool.kt index efa2aaa8f..c14287df6 100644 --- a/modules/base/src/main/kotlin/com/explyt/inspection/SpringBaseLocalInspectionTool.kt +++ b/modules/base/src/main/kotlin/com/explyt/inspection/SpringBaseLocalInspectionTool.kt @@ -5,7 +5,6 @@ package com.explyt.inspection -import com.explyt.base.LibraryClassCache import com.explyt.util.SpringBaseClasses.CORE_ENVIRONMENT import com.intellij.codeInspection.LocalInspectionTool import com.intellij.psi.PsiFile @@ -13,6 +12,6 @@ import com.intellij.psi.PsiFile abstract class SpringBaseLocalInspectionTool : LocalInspectionTool() { override fun isAvailableForFile(file: PsiFile): Boolean { - return LibraryClassCache.searchForLibraryClass(file.project, CORE_ENVIRONMENT) != null + return InspectionAvailability.hasLibraryClass(file, CORE_ENVIRONMENT) } } \ No newline at end of file diff --git a/modules/base/src/main/kotlin/com/explyt/inspection/SpringBaseUastLocalInspectionTool.kt b/modules/base/src/main/kotlin/com/explyt/inspection/SpringBaseUastLocalInspectionTool.kt index e226f3110..2f8d1362f 100644 --- a/modules/base/src/main/kotlin/com/explyt/inspection/SpringBaseUastLocalInspectionTool.kt +++ b/modules/base/src/main/kotlin/com/explyt/inspection/SpringBaseUastLocalInspectionTool.kt @@ -5,7 +5,6 @@ package com.explyt.inspection -import com.explyt.base.LibraryClassCache import com.explyt.plugin.PluginIds import com.explyt.util.SpringBaseClasses.CORE_ENVIRONMENT import com.intellij.codeInspection.AbstractBaseUastLocalInspectionTool @@ -16,6 +15,6 @@ abstract class SpringBaseUastLocalInspectionTool : AbstractBaseUastLocalInspecti override fun isAvailableForFile(file: PsiFile): Boolean { //spring boot is ultimate plugin. it is mean that inspection not available in Paid version, // because it already has spring bundle plugin - return LibraryClassCache.searchForLibraryClass(file.project, CORE_ENVIRONMENT) != null + return InspectionAvailability.hasLibraryClass(file, CORE_ENVIRONMENT) } } \ No newline at end of file diff --git a/modules/spring-core/src/main/kotlin/com/explyt/spring/core/inspections/SpringBoot3JakartaMigrationInspection.kt b/modules/spring-core/src/main/kotlin/com/explyt/spring/core/inspections/SpringBoot3JakartaMigrationInspection.kt index 60eff6212..6ed38318d 100644 --- a/modules/spring-core/src/main/kotlin/com/explyt/spring/core/inspections/SpringBoot3JakartaMigrationInspection.kt +++ b/modules/spring-core/src/main/kotlin/com/explyt/spring/core/inspections/SpringBoot3JakartaMigrationInspection.kt @@ -5,7 +5,7 @@ package com.explyt.spring.core.inspections -import com.explyt.base.LibraryClassCache +import com.explyt.inspection.InspectionAvailability import com.explyt.spring.core.SpringCoreBundle.message import com.explyt.spring.core.util.SpringBootUtil import com.explyt.util.SpringBaseClasses.CORE_ENVIRONMENT @@ -40,7 +40,7 @@ import org.jetbrains.kotlin.psi.KtVisitorVoid class SpringBoot3JakartaMigrationInspection : LocalInspectionTool() { override fun isAvailableForFile(file: PsiFile): Boolean { - return LibraryClassCache.searchForLibraryClass(file.project, CORE_ENVIRONMENT) != null + return InspectionAvailability.hasLibraryClass(file, CORE_ENVIRONMENT) && SpringBootUtil.isAtLeastSpringBoot3(file) && isAnyLegacyJavaxAvailable(file) } diff --git a/modules/spring-core/src/test/kotlin/com/explyt/spring/core/inspections/InspectionAvailabilityInDumbModeTest.kt b/modules/spring-core/src/test/kotlin/com/explyt/spring/core/inspections/InspectionAvailabilityInDumbModeTest.kt new file mode 100644 index 000000000..dc12327b0 --- /dev/null +++ b/modules/spring-core/src/test/kotlin/com/explyt/spring/core/inspections/InspectionAvailabilityInDumbModeTest.kt @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2026 Explyt Ltd + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.explyt.spring.core.inspections + +import com.explyt.base.LibraryClassCache +import com.explyt.inspection.SpringBaseLocalInspectionTool +import com.explyt.inspection.SpringBaseUastLocalInspectionTool +import com.explyt.spring.test.ExplytJavaLightTestCase +import com.explyt.spring.test.TestLibrary +import com.explyt.spring.test.util.assertAvailableOnlyInSmartMode +import com.explyt.spring.test.util.dropLibraryCaches +import com.explyt.util.SpringBaseClasses.CORE_ENVIRONMENT +import com.intellij.openapi.project.DumbService +import com.intellij.openapi.project.IndexNotReadyException +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiFile +import com.intellij.testFramework.DumbModeTestUtils + +class InspectionAvailabilityInDumbModeTest : ExplytJavaLightTestCase() { + + override val libraries: Array = arrayOf( + TestLibrary.springContext_6_0_7, + TestLibrary.springBoot_4_0_0, + TestLibrary.javax_annotation_1_3_2, + ) + + fun testSpringBaseUastLocalInspectionTool() = + object : SpringBaseUastLocalInspectionTool() {}.assertAvailableOnlyInSmartMode(sampleFile()) + + fun testSpringBaseLocalInspectionTool() = + object : SpringBaseLocalInspectionTool() {}.assertAvailableOnlyInSmartMode(sampleFile()) + + fun testSpring4UastLocalInspectionTool() { + myFixture.addClass("package com.fasterxml.jackson.databind; public class ObjectMapper { }") + myFixture.addClass("package tools.jackson.databind.json; public class JsonMapper { }") + SpringBoot4Jackson2ObjectMapperInspection().assertAvailableOnlyInSmartMode(sampleFile()) + } + + fun testSpring4LocalInspectionTool() = + SpringBoot4PropertyMigrationInspection().assertAvailableOnlyInSmartMode(sampleFile()) + + fun testSpringBoot3JakartaMigrationInspection() = + SpringBoot3JakartaMigrationInspection().assertAvailableOnlyInSmartMode(sampleFile()) + + fun testColdLibraryClassCacheThrowsIndexNotReadyInDumbMode() { + environmentFoundInSmartMode() + DumbModeTestUtils.runInDumbModeSynchronously(project) { + project.dropLibraryCaches() + assertThrows(IndexNotReadyException::class.java) { findEnvironment() } + } + } + + fun testColdDumbModeLookupLeavesNoStaleAnswerAfterIndexing() { + environmentFoundInSmartMode() + DumbModeTestUtils.runInDumbModeSynchronously(project) { + project.dropLibraryCaches() + val coldAnswer = try { + findEnvironment() + } catch (_: IndexNotReadyException) { + null + } + assertNull("a cold dumb-mode lookup must not find the class", coldAnswer) + } + assertNotNull("the dumb-mode lookup must not outlive indexing", findEnvironment()) + } + + private fun environmentFoundInSmartMode(): PsiClass { + assertFalse("the precondition lookup must run in smart mode", DumbService.isDumb(project)) + return findEnvironment() ?: throw AssertionError("the fixture must provide $CORE_ENVIRONMENT in smart mode") + } + + private fun findEnvironment(): PsiClass? = LibraryClassCache.searchForLibraryClass(project, CORE_ENVIRONMENT) + + private fun sampleFile(): PsiFile = myFixture.configureByText( + "Sample.java", + """ + import javax.annotation.PostConstruct; + + public class Sample { + @PostConstruct + void init() {} + } + """.trimIndent() + ) +} diff --git a/modules/spring-data/src/main/kotlin/com/explyt/spring/data/inspection/SpringDataBaseUastLocalInspectionTool.kt b/modules/spring-data/src/main/kotlin/com/explyt/spring/data/inspection/SpringDataBaseUastLocalInspectionTool.kt index 9a1f823ac..26c8491b6 100644 --- a/modules/spring-data/src/main/kotlin/com/explyt/spring/data/inspection/SpringDataBaseUastLocalInspectionTool.kt +++ b/modules/spring-data/src/main/kotlin/com/explyt/spring/data/inspection/SpringDataBaseUastLocalInspectionTool.kt @@ -5,7 +5,7 @@ package com.explyt.spring.data.inspection -import com.explyt.base.LibraryClassCache +import com.explyt.inspection.InspectionAvailability import com.explyt.inspection.SpringBaseUastLocalInspectionTool import com.explyt.plugin.PluginIds import com.explyt.spring.data.SpringDataClasses @@ -14,9 +14,7 @@ import com.intellij.psi.PsiFile abstract class SpringDataBaseUastLocalInspectionTool : SpringBaseUastLocalInspectionTool() { override fun isAvailableForFile(file: PsiFile): Boolean { - return PluginIds.SPRING_DATA_JB.isNotEnabled() && LibraryClassCache.searchForLibraryClass( - file.project, - SpringDataClasses.REPOSITORY - ) != null + return PluginIds.SPRING_DATA_JB.isNotEnabled() + && InspectionAvailability.hasLibraryClass(file, SpringDataClasses.REPOSITORY) } } \ No newline at end of file diff --git a/modules/spring-data/src/main/kotlin/com/explyt/spring/data/inspection/SpringJdbcClientParamInspection.kt b/modules/spring-data/src/main/kotlin/com/explyt/spring/data/inspection/SpringJdbcClientParamInspection.kt index 7e6cf3e79..125196365 100644 --- a/modules/spring-data/src/main/kotlin/com/explyt/spring/data/inspection/SpringJdbcClientParamInspection.kt +++ b/modules/spring-data/src/main/kotlin/com/explyt/spring/data/inspection/SpringJdbcClientParamInspection.kt @@ -5,6 +5,7 @@ package com.explyt.spring.data.inspection +import com.explyt.inspection.InspectionAvailability import com.explyt.inspection.SpringBaseUastLocalInspectionTool import com.explyt.spring.data.SpringDataBundle import com.explyt.spring.data.SpringDataClasses @@ -24,6 +25,7 @@ import org.jetbrains.uast.visitor.AbstractUastNonRecursiveVisitor class SpringJdbcClientParamInspection : SpringBaseUastLocalInspectionTool() { override fun isAvailableForFile(file: PsiFile): Boolean { + if (!InspectionAvailability.isIndexReady(file)) return false val module = ModuleUtilCore.findModuleForPsiElement(file) ?: return false val uFile = file.toUElement() as? UFile if (uFile != null) { diff --git a/modules/spring-data/src/test/kotlin/com/explyt/spring/data/inspection/InspectionAvailabilityInDumbModeTest.kt b/modules/spring-data/src/test/kotlin/com/explyt/spring/data/inspection/InspectionAvailabilityInDumbModeTest.kt new file mode 100644 index 000000000..1f14cc280 --- /dev/null +++ b/modules/spring-data/src/test/kotlin/com/explyt/spring/data/inspection/InspectionAvailabilityInDumbModeTest.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2026 Explyt Ltd + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.explyt.spring.data.inspection + +import com.explyt.spring.test.ExplytJavaLightTestCase +import com.explyt.spring.test.TestLibrary +import com.explyt.spring.test.util.assertAvailableOnlyInSmartMode +import com.intellij.psi.PsiFile + +class InspectionAvailabilityInDumbModeTest : ExplytJavaLightTestCase() { + + override val libraries: Array = arrayOf( + TestLibrary.springContext_6_0_7, + TestLibrary.springDataJpa_3_1_0, + TestLibrary.springJdbc_6_2_5, + ) + + fun testSpringDataBaseUastLocalInspectionTool() = + object : SpringDataBaseUastLocalInspectionTool() {}.assertAvailableOnlyInSmartMode(sampleFile()) + + fun testSpringJdbcClientParamInspection() = + SpringJdbcClientParamInspection().assertAvailableOnlyInSmartMode(sampleFile()) + + private fun sampleFile(): PsiFile = myFixture.configureByText( + "OrderDao.java", + """ + import org.springframework.jdbc.core.simple.JdbcClient; + + public class OrderDao { + JdbcClient jdbcClient; + } + """.trimIndent() + ) +} diff --git a/modules/spring-web/src/main/kotlin/com/explyt/spring/web/inspections/SpringWebBaseUastLocalInspectionTool.kt b/modules/spring-web/src/main/kotlin/com/explyt/spring/web/inspections/SpringWebBaseUastLocalInspectionTool.kt index e103a59c7..9b1217729 100644 --- a/modules/spring-web/src/main/kotlin/com/explyt/spring/web/inspections/SpringWebBaseUastLocalInspectionTool.kt +++ b/modules/spring-web/src/main/kotlin/com/explyt/spring/web/inspections/SpringWebBaseUastLocalInspectionTool.kt @@ -5,7 +5,7 @@ package com.explyt.spring.web.inspections -import com.explyt.base.LibraryClassCache +import com.explyt.inspection.InspectionAvailability import com.explyt.inspection.SpringBaseUastLocalInspectionTool import com.explyt.plugin.PluginIds import com.explyt.spring.web.SpringWebClasses @@ -14,9 +14,7 @@ import com.intellij.psi.PsiFile abstract class SpringWebBaseUastLocalInspectionTool : SpringBaseUastLocalInspectionTool() { override fun isAvailableForFile(file: PsiFile): Boolean { - return PluginIds.SPRING_WEB_JB.isNotEnabled() && LibraryClassCache.searchForLibraryClass( - file.project, - SpringWebClasses.WEB_INITIALIZER - ) != null + return PluginIds.SPRING_WEB_JB.isNotEnabled() + && InspectionAvailability.hasLibraryClass(file, SpringWebClasses.WEB_INITIALIZER) } } \ No newline at end of file diff --git a/modules/spring-web/src/test/kotlin/com/explyt/spring/web/inspections/InspectionAvailabilityInDumbModeTest.kt b/modules/spring-web/src/test/kotlin/com/explyt/spring/web/inspections/InspectionAvailabilityInDumbModeTest.kt new file mode 100644 index 000000000..347ed66e3 --- /dev/null +++ b/modules/spring-web/src/test/kotlin/com/explyt/spring/web/inspections/InspectionAvailabilityInDumbModeTest.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2026 Explyt Ltd + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.explyt.spring.web.inspections + +import com.explyt.spring.test.ExplytJavaLightTestCase +import com.explyt.spring.test.TestLibrary +import com.explyt.spring.test.util.assertAvailableOnlyInSmartMode + +class InspectionAvailabilityInDumbModeTest : ExplytJavaLightTestCase() { + + override val libraries: Array = arrayOf(TestLibrary.springWeb_6_0_7) + + fun testSpringWebBaseUastLocalInspectionTool() = + object : SpringWebBaseUastLocalInspectionTool() {}.assertAvailableOnlyInSmartMode( + myFixture.configureByText( + "OrderController.java", + """ + import org.springframework.web.bind.annotation.RestController; + + @RestController + public class OrderController { + } + """.trimIndent() + ) + ) +} diff --git a/modules/test-framework/src/main/kotlin/com/explyt/spring/test/util/DumbModeAvailability.kt b/modules/test-framework/src/main/kotlin/com/explyt/spring/test/util/DumbModeAvailability.kt new file mode 100644 index 000000000..0d46fd3f1 --- /dev/null +++ b/modules/test-framework/src/main/kotlin/com/explyt/spring/test/util/DumbModeAvailability.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2026 Explyt Ltd + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.explyt.spring.test.util + +import com.intellij.codeInspection.LocalInspectionTool +import com.intellij.java.library.JavaLibraryModificationTracker +import com.intellij.openapi.application.WriteAction +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.ThrowableComputable +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiManager +import com.intellij.testFramework.DumbModeTestUtils +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue + +fun Project.computeInDumbModeWithColdLibraryCaches(compute: () -> T): T = + DumbModeTestUtils.computeInDumbModeSynchronously(this, ThrowableComputable { + dropLibraryCaches() + runCatching(compute).getOrElse { throw AssertionError("Threw in dumb mode: $it", it) } + }) + +fun LocalInspectionTool.isAvailableInDumbMode(file: PsiFile): Boolean = + file.project.computeInDumbModeWithColdLibraryCaches { isAvailableForFile(file) } + +fun LocalInspectionTool.assertAvailableOnlyInSmartMode(file: PsiFile) { + assertTrue("${javaClass.name} must be available for ${file.name} in smart mode", isAvailableForFile(file)) + assertFalse("${javaClass.name} must not be available for ${file.name} in dumb mode", isAvailableInDumbMode(file)) +} + +@Suppress("UnstableApiUsage") +fun Project.dropLibraryCaches() { + JavaLibraryModificationTracker.incModificationCount(this) + WriteAction.run { PsiManager.getInstance(this).dropPsiCaches() } +}