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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
## [Unreleased]

### Spring Core
- feat: Report two `@Bean` methods with different names that declare the same bean name in one configuration class, which fails Spring Boot startup unless bean definition overriding is allowed; methods annotated with `@Profile` or another `@Conditional` are not reported, since Spring skips them before it registers the name (#597)
- feat: Report two `@Bean` methods with different names that declare the same bean name in one configuration class, which fails Spring Boot startup unless bean definition overriding is allowed; Spring Framework 7 fails for a `@Configuration` class with `enforceUniqueMethods` even when overriding is allowed, so the problem stays an error there whatever `spring.main.allow-bean-definition-overriding` says, and is a warning for an allowed overriding when the Spring Framework version cannot be determined; methods annotated with `@Profile` or another `@Conditional` are not reported, since Spring skips them before it registers the name (#597, #686)
- feat: Report a stereotype annotation such as `@Component` on an abstract class without a `@Lookup` method, which Spring does not register, with a quick fix that removes it (#601)
- fix: Bean usages and the abstract-component gutter now list consumers declared in modules that depend on the bean's module, and count as injection points only the constructors Spring injects through: the `@Autowired` ones, otherwise the only constructor when it takes parameters, or a Kotlin primary constructor next to a no-arg secondary one, so the other constructors of a class with several and no `@Autowired` are no longer listed (#600)
- fix: Type a bean declared by a superclass `@Bean` method and overridden with a narrower return type by the override's type, as Spring predicts it: the bean keeps the superclass declaration's name and annotations, and now autowires by the override's return type, in the bean model, the autowiring inspection and the Spring MCP bean tools (#594)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@ package com.explyt.spring.core.inspections
import com.explyt.inspection.SpringBaseUastLocalInspectionTool
import com.explyt.spring.core.SpringCoreBundle
import com.explyt.spring.core.SpringCoreClasses
import com.explyt.spring.core.service.SpringSearchService
import com.explyt.spring.core.service.conditional.ConditionPropertyValue
import com.explyt.spring.core.service.conditional.PropertyConditionValues
import com.explyt.spring.core.util.BeanFactoryMethods
import com.explyt.spring.core.util.SpringBootUtil
import com.explyt.spring.core.util.SpringCoreUtil.resolveBeanName
import com.explyt.util.ExplytPsiUtil.getMetaAnnotation
import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleUtilCore
import com.intellij.openapi.progress.ProgressManager
import com.intellij.psi.PsiClass
Expand All @@ -33,7 +36,7 @@ class SpringDuplicateBeanNameInspection : SpringBaseUastLocalInspectionTool() {
): Array<ProblemDescriptor>? {
val psiClass = aClass.javaPsi
val collisions = collisionsIn(psiClass).ifEmpty { return null }
val overriding = overridingOf(psiClass).takeUnless { it == Overriding.ALLOWED } ?: return null
val overriding = overridingOf(aClass).takeUnless { it == Overriding.ALLOWED } ?: return null
return collisions.mapNotNull { it.toProblem(manager, isOnTheFly, overriding) }.toTypedArray()
}

Expand All @@ -52,25 +55,45 @@ class SpringDuplicateBeanNameInspection : SpringBaseUastLocalInspectionTool() {

private fun PsiMethod.isConditional(): Boolean = getMetaAnnotation(SpringCoreClasses.CONDITIONAL) != null

private fun overridingOf(psiClass: PsiClass): Overriding {
val module = ModuleUtilCore.findModuleForPsiElement(psiClass) ?: return Overriding.DISALLOWED
private fun overridingOf(aClass: UClass): Overriding {
val module = ModuleUtilCore.findModuleForPsiElement(aClass.javaPsi) ?: return Overriding.DISALLOWED
val ignoredOverriding = if (aClass.enforcesUniqueMethods(module)) ignoredOverridingOf(module) else null
if (ignoredOverriding == Overriding.IGNORED_ON_SPRING_7) return ignoredOverriding
return when (val value = PropertyConditionValues(module).valueOf(ALLOW_OVERRIDING_KEY)) {
is ConditionPropertyValue.Known ->
if (value.text.trim().equals("true", ignoreCase = true)) Overriding.ALLOWED else Overriding.DISALLOWED
if (value.text.trim().equals("true", ignoreCase = true)) ignoredOverriding ?: Overriding.ALLOWED
else Overriding.DISALLOWED

ConditionPropertyValue.Missing -> Overriding.DISALLOWED
ConditionPropertyValue.Unresolvable -> Overriding.UNKNOWN
ConditionPropertyValue.Unresolvable -> Overriding.UNRESOLVED
}
}

private fun ignoredOverridingOf(module: Module): Overriding? {
val springMajor = SpringBootUtil.getSpringCoreMajorVersion(module) ?: return Overriding.IGNORED_ON_UNKNOWN_SPRING
return Overriding.IGNORED_ON_SPRING_7.takeIf { springMajor >= SPRING_7 }
}

private fun UClass.enforcesUniqueMethods(module: Module): Boolean {
val configurations = SpringSearchService.getInstance(module.project)
.getMetaAnnotations(module, SpringCoreClasses.CONFIGURATION)
val configuration = uAnnotations.firstOrNull { it.qualifiedName == SpringCoreClasses.CONFIGURATION }
?: uAnnotations.firstOrNull { configurations.contains(it) }
?: return false
val values = configurations.getAnnotationMemberValues(configuration, setOf(ENFORCE_UNIQUE_METHODS))
return values.firstOrNull()?.evaluate() != false
}

private fun Collision.toProblem(
manager: InspectionManager,
isOnTheFly: Boolean,
overriding: Overriding
): ProblemDescriptor? {
val anchor = second.toUElementOfType<UMethod>()?.uastAnchor?.sourcePsi ?: return null
val (messageKey, highlightType) = when (overriding) {
Overriding.UNKNOWN -> UNRESOLVED_OVERRIDING_MESSAGE to ProblemHighlightType.WARNING
Overriding.UNRESOLVED -> UNRESOLVED_OVERRIDING_MESSAGE to ProblemHighlightType.WARNING
Overriding.IGNORED_ON_UNKNOWN_SPRING -> UNKNOWN_SPRING_OVERRIDING_MESSAGE to ProblemHighlightType.WARNING
Overriding.IGNORED_ON_SPRING_7 -> SPRING_7_OVERRIDING_MESSAGE to ProblemHighlightType.GENERIC_ERROR_OR_WARNING
else -> MESSAGE to ProblemHighlightType.GENERIC_ERROR_OR_WARNING
}
return manager.createProblemDescriptor(
Expand All @@ -84,11 +107,16 @@ class SpringDuplicateBeanNameInspection : SpringBaseUastLocalInspectionTool() {

private data class Collision(val beanName: String, val first: PsiMethod, val second: PsiMethod)

private enum class Overriding { ALLOWED, DISALLOWED, UNKNOWN }
private enum class Overriding { ALLOWED, DISALLOWED, UNRESOLVED, IGNORED_ON_SPRING_7, IGNORED_ON_UNKNOWN_SPRING }

private companion object {
const val ALLOW_OVERRIDING_KEY = "spring.main.allow-bean-definition-overriding"
const val ENFORCE_UNIQUE_METHODS = "enforceUniqueMethods"
const val SPRING_7 = 7
const val MESSAGE = "explyt.spring.inspection.bean.name.duplicate"
const val UNRESOLVED_OVERRIDING_MESSAGE = "explyt.spring.inspection.bean.name.duplicate.overriding.unresolved"
const val SPRING_7_OVERRIDING_MESSAGE = "explyt.spring.inspection.bean.name.duplicate.overriding.spring7"
const val UNKNOWN_SPRING_OVERRIDING_MESSAGE =
"explyt.spring.inspection.bean.name.duplicate.overriding.unknown.spring"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,18 @@
of the same configuration class.
<p>Spring Boot disallows bean definition overriding by default, so such a configuration fails at startup with
<code>BeanDefinitionOverrideException</code>. Overloaded <code>@Bean</code> methods with the same method name are merged by
Spring and are not reported. Nothing is reported when <code>spring.main.allow-bean-definition-overriding=true</code>
is set in the module configuration.</p>
Spring and are not reported.</p>
<p>When <code>spring.main.allow-bean-definition-overriding=true</code> is set in the module configuration, nothing is
reported before Spring Framework 7, for a lite class such as a <code>@Component</code> with <code>@Bean</code> methods, or for
a <code>@Configuration(enforceUniqueMethods = false)</code> class, also when <code>enforceUniqueMethods</code> is set through
a composed annotation. Spring Framework 7 fails at startup for any other <code>@Configuration</code> class whatever the
property says, so the problem is always an error there, and it is reported as a warning when the property allows
overriding but the Spring Framework version cannot be determined.</p>
<p>Methods annotated with <code>@Profile</code> or another <code>@Conditional</code> annotation, such as
<code>@ConditionalOnProperty</code>, are not reported: Spring evaluates their conditions before it registers the bean name,
so only one of them may be active.</p>
<p>When the value of <code>spring.main.allow-bean-definition-overriding</code> cannot be resolved, for example because it is
a placeholder or depends on a profile, the problem is reported as a warning instead of an error.</p>
a placeholder or depends on a profile, the problem is reported as a warning instead of an error, except for a
<code>@Configuration</code> class on Spring Framework 7, which fails whatever the value is.</p>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ explyt.spring.inspection.configuration.proxy.quickfix=Use dependency injection
explyt.spring.inspection.bean.name.duplicate.display=Duplicate @Bean name in one configuration class
explyt.spring.inspection.bean.name.duplicate=Bean ''{0}'' is also declared by ''{1}'' in this class; Spring Boot fails at startup unless bean definition overriding is allowed
explyt.spring.inspection.bean.name.duplicate.overriding.unresolved=Bean ''{0}'' is also declared by ''{1}'' in this class; Spring Boot fails at startup unless spring.main.allow-bean-definition-overriding is true, and its value cannot be resolved here
explyt.spring.inspection.bean.name.duplicate.overriding.spring7=Bean ''{0}'' is also declared by ''{1}'' in this class; Spring 7 fails at startup for a @Configuration class even when bean definition overriding is allowed, unless enforceUniqueMethods is false
explyt.spring.inspection.bean.name.duplicate.overriding.unknown.spring=Bean ''{0}'' is also declared by ''{1}'' in this class; spring.main.allow-bean-definition-overriding=true does not help a @Configuration class on Spring 7, and the Spring version cannot be determined here
explyt.spring.inspection.bean.lookup=@Lookup
explyt.spring.inspection.bean.lookup.unknown=Cannot resolve bean ''{0}''
explyt.spring.inspection.bean.lookup.wrongType=Expected bean of ''{0}'' type but found ''{1}''
Expand Down
Loading
Loading