From 117d738a791c994edac948da26474f6de0a5b937 Mon Sep 17 00:00:00 2001 From: Ilya Muromtsev Date: Tue, 13 Oct 2026 21:53:41 +0300 Subject: [PATCH 1/5] test: Pin Spring inspection availability in dumb mode (#688) --- .../InspectionAvailabilityInDumbModeTest.kt | 72 +++++++++++++++++++ .../InspectionAvailabilityInDumbModeTest.kt | 37 ++++++++++ .../InspectionAvailabilityInDumbModeTest.kt | 29 ++++++++ .../spring/test/util/DumbModeAvailability.kt | 37 ++++++++++ 4 files changed, 175 insertions(+) create mode 100644 modules/spring-core/src/test/kotlin/com/explyt/spring/core/inspections/InspectionAvailabilityInDumbModeTest.kt create mode 100644 modules/spring-data/src/test/kotlin/com/explyt/spring/data/inspection/InspectionAvailabilityInDumbModeTest.kt create mode 100644 modules/spring-web/src/test/kotlin/com/explyt/spring/web/inspections/InspectionAvailabilityInDumbModeTest.kt create mode 100644 modules/test-framework/src/main/kotlin/com/explyt/spring/test/util/DumbModeAvailability.kt 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..9af2c6611 --- /dev/null +++ b/modules/spring-core/src/test/kotlin/com/explyt/spring/core/inspections/InspectionAvailabilityInDumbModeTest.kt @@ -0,0 +1,72 @@ +/* + * 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.computeInDumbModeWithColdLibraryCaches +import com.explyt.util.SpringBaseClasses.CORE_ENVIRONMENT +import com.intellij.psi.PsiFile + +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 testLibraryClassCacheFindsNothingInDumbModeAndResolvesAfterIndexing() { + assertNotNull( + "the fixture must provide $CORE_ENVIRONMENT in smart mode", + LibraryClassCache.searchForLibraryClass(project, CORE_ENVIRONMENT) + ) + assertNull( + "a dumb-mode lookup must answer nothing instead of touching indexes", + project.computeInDumbModeWithColdLibraryCaches { + LibraryClassCache.searchForLibraryClass(project, CORE_ENVIRONMENT) + } + ) + assertNotNull( + "the dumb-mode answer must not outlive indexing", + 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/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..1ebe19fe4 --- /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/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..e770741d3 --- /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..480f15be9 --- /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") +private fun Project.dropLibraryCaches() { + JavaLibraryModificationTracker.incModificationCount(this) + WriteAction.run { PsiManager.getInstance(this).dropPsiCaches() } +} From bdd8769b35104a3aae2d276c1d78b617bb4be7f8 Mon Sep 17 00:00:00 2001 From: Ilya Muromtsev Date: Tue, 13 Oct 2026 22:35:36 +0300 Subject: [PATCH 2/5] fix: Keep Spring inspections unavailable while the IDE indexes instead of throwing IndexNotReadyException (#688) --- CHANGELOG.md | 1 + .../com/explyt/base/LibraryClassCache.kt | 38 ++++++++++++------- .../inspection/InspectionAvailability.kt | 18 +++++++++ .../SpringBaseLocalInspectionTool.kt | 3 +- .../SpringBaseUastLocalInspectionTool.kt | 3 +- .../SpringBoot3JakartaMigrationInspection.kt | 4 +- .../SpringDataBaseUastLocalInspectionTool.kt | 8 ++-- .../SpringJdbcClientParamInspection.kt | 2 + .../SpringWebBaseUastLocalInspectionTool.kt | 8 ++-- 9 files changed, 56 insertions(+), 29 deletions(-) create mode 100644 modules/base/src/main/kotlin/com/explyt/inspection/InspectionAvailability.kt 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/base/LibraryClassCache.kt b/modules/base/src/main/kotlin/com/explyt/base/LibraryClassCache.kt index 8de6bb072..df2fa040d 100644 --- a/modules/base/src/main/kotlin/com/explyt/base/LibraryClassCache.kt +++ b/modules/base/src/main/kotlin/com/explyt/base/LibraryClassCache.kt @@ -9,6 +9,8 @@ import com.explyt.util.CacheKeyStore import com.intellij.java.library.JavaLibraryModificationTracker import com.intellij.openapi.application.runReadAction import com.intellij.openapi.module.Module +import com.intellij.openapi.project.DumbService +import com.intellij.openapi.project.IndexNotReadyException import com.intellij.openapi.project.Project import com.intellij.openapi.roots.impl.LibraryScopeCache import com.intellij.openapi.util.Key @@ -26,17 +28,25 @@ object LibraryClassCache { project: Project, fqn: String ): PsiClass? = runReadAction { + if (DumbService.isDumb(project)) findWhileIndexing(project, fqn) else findCached(project, fqn) + } + + fun searchForLibraryClass(module: Module, fqn: String) = searchForLibraryClass(module.project, fqn) + + fun searchForLibraryClasses( + module: Module, + fqns: Collection + ): Collection = + fqns.mapNotNull { searchForLibraryClass(module, it) } + + private fun findCached(project: Project, fqn: String): PsiClass? = CachedValuesManager.getManager(project) .getCachedValue( project, keys.getValue(fqn), { - val result = JavaPsiFacade.getInstance(project).findClass( - fqn, LibraryScopeCache.getInstance(project).librariesOnlyScope - ) - CachedValueProvider.Result.create( - result, + findLibraryClass(project, fqn), @Suppress("UnstableApiUsage") JavaLibraryModificationTracker.getInstance(project), @@ -46,13 +56,15 @@ object LibraryClassCache { }, false ) - } - fun searchForLibraryClass(module: Module, fqn: String) = searchForLibraryClass(module.project, fqn) + private fun findWhileIndexing(project: Project, fqn: String): PsiClass? = + project.getUserData(keys.getValue(fqn))?.upToDateOrNull?.get() + ?: try { + findLibraryClass(project, fqn) + } catch (_: IndexNotReadyException) { + null + } - fun searchForLibraryClasses( - module: Module, - fqns: Collection - ): Collection = - fqns.mapNotNull { searchForLibraryClass(module, it) } -} \ No newline at end of file + private fun findLibraryClass(project: Project, fqn: String): PsiClass? = + JavaPsiFacade.getInstance(project).findClass(fqn, LibraryScopeCache.getInstance(project).librariesOnlyScope) +} 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-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-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 From 3ca9eac97ba198f6e99e6ea7816b3d21032350b9 Mon Sep 17 00:00:00 2001 From: Ilya Muromtsev Date: Tue, 13 Oct 2026 22:54:26 +0300 Subject: [PATCH 3/5] test: Pin the LibraryClassCache dumb-mode contract (#688) --- .../InspectionAvailabilityInDumbModeTest.kt | 46 +++++++++++++------ .../spring/test/util/DumbModeAvailability.kt | 2 +- 2 files changed, 32 insertions(+), 16 deletions(-) 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 index 9af2c6611..327f51eae 100644 --- 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 @@ -11,9 +11,13 @@ 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.computeInDumbModeWithColdLibraryCaches +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() { @@ -41,23 +45,35 @@ class InspectionAvailabilityInDumbModeTest : ExplytJavaLightTestCase() { fun testSpringBoot3JakartaMigrationInspection() = SpringBoot3JakartaMigrationInspection().assertAvailableOnlyInSmartMode(sampleFile()) - fun testLibraryClassCacheFindsNothingInDumbModeAndResolvesAfterIndexing() { - assertNotNull( - "the fixture must provide $CORE_ENVIRONMENT in smart mode", - LibraryClassCache.searchForLibraryClass(project, CORE_ENVIRONMENT) - ) - assertNull( - "a dumb-mode lookup must answer nothing instead of touching indexes", - project.computeInDumbModeWithColdLibraryCaches { - LibraryClassCache.searchForLibraryClass(project, CORE_ENVIRONMENT) + 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 } - ) - assertNotNull( - "the dumb-mode answer must not outlive indexing", - LibraryClassCache.searchForLibraryClass(project, CORE_ENVIRONMENT) - ) + 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", """ 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 index 480f15be9..0d46fd3f1 100644 --- 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 @@ -31,7 +31,7 @@ fun LocalInspectionTool.assertAvailableOnlyInSmartMode(file: PsiFile) { } @Suppress("UnstableApiUsage") -private fun Project.dropLibraryCaches() { +fun Project.dropLibraryCaches() { JavaLibraryModificationTracker.incModificationCount(this) WriteAction.run { PsiManager.getInstance(this).dropPsiCaches() } } From 7326787087a3fba8ad7a20b6ef0983b5836f4554 Mon Sep 17 00:00:00 2001 From: Ilya Muromtsev Date: Tue, 13 Oct 2026 22:57:07 +0300 Subject: [PATCH 4/5] fix: Keep LibraryClassCache throwing IndexNotReadyException for a cold lookup while indexing (#688) --- .../com/explyt/base/LibraryClassCache.kt | 38 +++++++------------ 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/modules/base/src/main/kotlin/com/explyt/base/LibraryClassCache.kt b/modules/base/src/main/kotlin/com/explyt/base/LibraryClassCache.kt index df2fa040d..8de6bb072 100644 --- a/modules/base/src/main/kotlin/com/explyt/base/LibraryClassCache.kt +++ b/modules/base/src/main/kotlin/com/explyt/base/LibraryClassCache.kt @@ -9,8 +9,6 @@ import com.explyt.util.CacheKeyStore import com.intellij.java.library.JavaLibraryModificationTracker import com.intellij.openapi.application.runReadAction import com.intellij.openapi.module.Module -import com.intellij.openapi.project.DumbService -import com.intellij.openapi.project.IndexNotReadyException import com.intellij.openapi.project.Project import com.intellij.openapi.roots.impl.LibraryScopeCache import com.intellij.openapi.util.Key @@ -28,25 +26,17 @@ object LibraryClassCache { project: Project, fqn: String ): PsiClass? = runReadAction { - if (DumbService.isDumb(project)) findWhileIndexing(project, fqn) else findCached(project, fqn) - } - - fun searchForLibraryClass(module: Module, fqn: String) = searchForLibraryClass(module.project, fqn) - - fun searchForLibraryClasses( - module: Module, - fqns: Collection - ): Collection = - fqns.mapNotNull { searchForLibraryClass(module, it) } - - private fun findCached(project: Project, fqn: String): PsiClass? = CachedValuesManager.getManager(project) .getCachedValue( project, keys.getValue(fqn), { + val result = JavaPsiFacade.getInstance(project).findClass( + fqn, LibraryScopeCache.getInstance(project).librariesOnlyScope + ) + CachedValueProvider.Result.create( - findLibraryClass(project, fqn), + result, @Suppress("UnstableApiUsage") JavaLibraryModificationTracker.getInstance(project), @@ -56,15 +46,13 @@ object LibraryClassCache { }, false ) + } - private fun findWhileIndexing(project: Project, fqn: String): PsiClass? = - project.getUserData(keys.getValue(fqn))?.upToDateOrNull?.get() - ?: try { - findLibraryClass(project, fqn) - } catch (_: IndexNotReadyException) { - null - } + fun searchForLibraryClass(module: Module, fqn: String) = searchForLibraryClass(module.project, fqn) - private fun findLibraryClass(project: Project, fqn: String): PsiClass? = - JavaPsiFacade.getInstance(project).findClass(fqn, LibraryScopeCache.getInstance(project).librariesOnlyScope) -} + fun searchForLibraryClasses( + module: Module, + fqns: Collection + ): Collection = + fqns.mapNotNull { searchForLibraryClass(module, it) } +} \ No newline at end of file From 5891791af3c0569e3e2dd2b2a64a9e627b79f4de Mon Sep 17 00:00:00 2001 From: Ilya Muromtsev Date: Tue, 13 Oct 2026 23:17:31 +0300 Subject: [PATCH 5/5] style: Strip whitespace-only lines in dumb-mode availability fixtures (#688) --- .../core/inspections/InspectionAvailabilityInDumbModeTest.kt | 2 +- .../data/inspection/InspectionAvailabilityInDumbModeTest.kt | 2 +- .../web/inspections/InspectionAvailabilityInDumbModeTest.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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 index 327f51eae..dc12327b0 100644 --- 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 @@ -78,7 +78,7 @@ class InspectionAvailabilityInDumbModeTest : ExplytJavaLightTestCase() { "Sample.java", """ import javax.annotation.PostConstruct; - + public class Sample { @PostConstruct void init() {} 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 index 1ebe19fe4..1f14cc280 100644 --- 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 @@ -28,7 +28,7 @@ class InspectionAvailabilityInDumbModeTest : ExplytJavaLightTestCase() { "OrderDao.java", """ import org.springframework.jdbc.core.simple.JdbcClient; - + public class OrderDao { JdbcClient jdbcClient; } 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 index e770741d3..347ed66e3 100644 --- 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 @@ -19,7 +19,7 @@ class InspectionAvailabilityInDumbModeTest : ExplytJavaLightTestCase() { "OrderController.java", """ import org.springframework.web.bind.annotation.RestController; - + @RestController public class OrderController { }