Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@

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

abstract class SpringBaseLocalInspectionTool : LocalInspectionTool() {

override fun isAvailableForFile(file: PsiFile): Boolean {
return LibraryClassCache.searchForLibraryClass(file.project, CORE_ENVIRONMENT) != null
return InspectionAvailability.hasLibraryClass(file, CORE_ENVIRONMENT)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<TestLibrary> = 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()
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<TestLibrary> = 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()
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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<TestLibrary> = 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()
)
)
}
Original file line number Diff line number Diff line change
@@ -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 <T> Project.computeInDumbModeWithColdLibraryCaches(compute: () -> T): T =
DumbModeTestUtils.computeInDumbModeSynchronously(this, ThrowableComputable<T, Throwable> {
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<Throwable> { PsiManager.getInstance(this).dropPsiCaches() }
}
Loading