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 @@ -168,6 +168,7 @@
- fix: Inject the regular expression of a `@RequestMapping` path built by concatenation into the literal that actually contains it
- fix: Keep an Actuator endpoint's write operation next to a web extension's write operation that differs only by whether it reads a request body, as Boot routes them by the consumed media types (#655)
- fix: Read the endpoint a project Actuator web extension extends the way Boot discovers it: through a composed annotation, with an `@AliasFor` value or default taking precedence over the value fixed on the meta-annotation, and from an annotation declared on a superclass of the extension; a same-named attribute without `@AliasFor` counts only up to Spring Framework 6. An extension whose bean condition does not match no longer replaces the endpoint's operations, and one whose condition cannot be decided is applied while the endpoint's bean condition reads undecided, naming the extension (#655)
- fix: Read a method's request mapping from the single annotation Spring registers: a composed mapping contributes its merged path, verbs and media types to the endpoint gutters, Run in Swagger, OpenAPI generation, the duplicate-endpoint inspection and the MCP contract, and of several mapping annotations on one method only the one Spring uses is kept (a direct `@RequestMapping`, else the first in source order), so the Endpoints window and the MCP tools no longer list the others (#672)

### Spring MCP

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ import org.jetbrains.uast.UMethod
import org.jetbrains.uast.UParameter
import org.jetbrains.uast.UResolvable
import org.jetbrains.uast.visitor.AbstractUastVisitor
import org.jetbrains.uast.evaluateString

import org.jetbrains.uast.getParentOfType
import org.jetbrains.uast.getUastParentOfType
import org.jetbrains.uast.toUElement
Expand Down Expand Up @@ -1344,12 +1344,10 @@ class SpringBootApplicationMcpToolset : McpToolset {
if (uHandler == null || module == null) return MediaTypes(emptyList(), emptyList())
val mah = SpringSearchService.getInstance(project)
.getMetaAnnotations(module, SpringWebClasses.REQUEST_MAPPING)
val mapping = HandlerMethods.mappingSourceOf(uHandler.javaPsi) { it.isMetaAnnotatedBy(SpringWebClasses.REQUEST_MAPPING) }
?.toUElement() as? UMethod ?: uHandler
return MediaTypes(
produces = mah.getAnnotationValues(mapping, setOf("produces")).mapNotNull { it.evaluateString() },
consumes = mah.getAnnotationValues(mapping, setOf("consumes")).mapNotNull { it.evaluateString() },
)
val mappingSource = HandlerMethods.mappingSourceOf(uHandler.javaPsi) { it.isMetaAnnotatedBy(SpringWebClasses.REQUEST_MAPPING) }
?: uHandler.javaPsi
val mapping = HandlerMethods.methodMapping(mappingSource, mah)
return MediaTypes(produces = mapping.produces, consumes = mapping.consumes)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,22 @@ package com.explyt.spring.web.inspections
import com.explyt.spring.core.service.MetaAnnotationsHolder
import com.explyt.spring.web.SpringWebBundle
import com.explyt.spring.web.SpringWebClasses
import com.explyt.spring.web.util.HandlerMethods
import com.explyt.util.ExplytPsiUtil.getHighlightRange
import com.explyt.util.ExplytPsiUtil.isMetaAnnotatedBy
import com.explyt.util.ExplytPsiUtil.isMetaAnnotatedByOrSelf

import com.explyt.util.ExplytPsiUtil.isPublic
import com.intellij.codeInsight.AnnotationUtil
import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.openapi.module.ModuleUtilCore
import com.intellij.psi.PsiAnnotationMemberValue
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiMethod
import io.ktor.http.*
import org.jetbrains.kotlin.build.joinToReadableString
import org.jetbrains.uast.*
import org.jetbrains.uast.visitor.AbstractUastNonRecursiveVisitor


class RequestMappingDuplicateInspection : SpringWebBaseUastLocalInspectionTool() {

Expand Down Expand Up @@ -57,7 +57,7 @@ class RequestMappingDuplicateInspection : SpringWebBaseUastLocalInspectionTool()
methodsByUrlMap.forEach { (_, duplicates) ->
if (duplicates.size < 2) return@forEach
for (duplicate in duplicates) {
val psiElement = duplicate.psiAnnotationMemberValue.toUElement()?.sourcePsi ?: continue
val psiElement = duplicate.anchor ?: continue
holder.registerProblem(
manager.createProblemDescriptor(
psiElement, psiElement.getHighlightRange(),
Expand All @@ -77,111 +77,24 @@ class RequestMappingDuplicateInspection : SpringWebBaseUastLocalInspectionTool()
private fun toUrlMethodPair(
requestMappingMah: MetaAnnotationsHolder, psiMethod: PsiMethod
): List<DuplicateUrlInfo> {
return requestMappingMah.getAnnotationMemberValues(psiMethod, TARGET_VALUE)
.flatMap { toDuplicateUrlInfo(it, psiMethod, requestMappingMah) }
}

private fun toDuplicateUrlInfo(
psiValue: PsiAnnotationMemberValue, psiMethod: PsiMethod, requestMappingMah: MetaAnnotationsHolder
): List<DuplicateUrlInfo> {
val urlString = AnnotationUtil.getStringAttributeValue(psiValue) ?: return emptyList()
val httpMethods = requestMappingMah.getHttpMethods(psiMethod)
return httpMethods.mapNotNull { getDuplicateUrlInfo(urlString, it, psiMethod, psiValue) }
}

private fun getDuplicateUrlInfo(
urlString: String,
string: String,
psiMethod: PsiMethod,
psiValue: PsiAnnotationMemberValue
): DuplicateUrlInfo? {
val url = try {
Url(urlString)
} catch (_: Exception) {
return null
val mapping = HandlerMethods.methodMapping(psiMethod, requestMappingMah)
val httpMethods = mapping.methods.toSet()
return mapping.paths.flatMap { (path, anchor) ->
val url = urlOf(path) ?: return@flatMap emptyList()
httpMethods.map { DuplicateUrlInfo(url, it, psiMethod, anchor) }
}
return DuplicateUrlInfo(url, string, psiMethod, psiValue)
}

private fun MetaAnnotationsHolder.getHttpMethods(
psiMethod: PsiMethod
) = getAnnotationMemberValues(psiMethod, TARGET_METHOD)
.mapTo(mutableSetOf()) { it.text.split('.').last() }

companion object {
private val TARGET_VALUE = setOf("value")
private val TARGET_METHOD = setOf("method")
private fun urlOf(urlString: String): Url? = try {
Url(urlString)
} catch (_: Exception) {
null
}

private data class DuplicateUrlInfo(
val url: Url,
val methodType: String,
val psiMethod: PsiMethod,
val psiAnnotationMemberValue: PsiAnnotationMemberValue,
val anchor: PsiElement?,
)
}

private class RequestMappingVisitor(
private val problemsHolder: ProblemsHolder, private val isOnTheFly: Boolean
) : AbstractUastNonRecursiveVisitor() {

override fun visitLiteralExpression(node: ULiteralExpression): Boolean {
val psiExpression = node.sourcePsi ?: return true
val module = ModuleUtilCore.findModuleForPsiElement(psiExpression) ?: return true
val requestMappingMah = MetaAnnotationsHolder.of(module, SpringWebClasses.REQUEST_MAPPING)
val endpoint = node.evaluateString() ?: return true
val uAnnotation = node.getParentOfType<UAnnotation>() ?: return true
val psiAnnotation = uAnnotation.javaPsi ?: return true
if (!psiAnnotation.isMetaAnnotatedByOrSelf(SpringWebClasses.REQUEST_MAPPING)) return true
if (psiAnnotation.isMetaAnnotatedByOrSelf(SpringWebClasses.CONTROLLER)) return true

val psiMethod = uAnnotation.getContainingUMethod()?.javaPsi ?: return true
val psiClass = psiMethod.containingClass ?: return true
if (!psiClass.isMetaAnnotatedBy(SpringWebClasses.CONTROLLER)) return true
val httpMethods = requestMappingMah.getHttpMethods(psiMethod)

val conflictingEndpoints = psiClass.methods.asSequence()
.filter { it != psiMethod }
.filter { it.isMetaAnnotatedBy(SpringWebClasses.REQUEST_MAPPING) }
.filter {
requestMappingMah.getAnnotationMemberValues(it, TARGET_VALUE).any { anotherEndpoint ->
AnnotationUtil.getStringAttributeValue(anotherEndpoint) == endpoint
}
}
.filter {
requestMappingMah.getHttpMethods(it)
.intersect(httpMethods).isNotEmpty()
}.mapTo(mutableListOf()) {
it.name
}

if (conflictingEndpoints.isEmpty()) return true

problemsHolder.registerProblem(
problemsHolder.manager.createProblemDescriptor(
psiExpression,
psiExpression.getHighlightRange(),
SpringWebBundle.message(
"explyt.spring.web.inspection.requestMapping.duplicate.methods",
conflictingEndpoints.joinToReadableString()
),
ProblemHighlightType.GENERIC_ERROR,
isOnTheFly

)
)

return true
}

private fun MetaAnnotationsHolder.getHttpMethods(
psiMethod: PsiMethod
) = getAnnotationMemberValues(psiMethod, TARGET_METHOD)
.mapTo(mutableSetOf()) { it.text.split('.').last() }

companion object {
private val TARGET_VALUE = setOf("value")
private val TARGET_METHOD = setOf("method")
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import com.explyt.spring.web.util.MappingPathPlaceholders
import com.explyt.spring.web.util.SpringWebUtil
import com.explyt.util.ExplytPsiUtil.isMetaAnnotatedBy
import com.explyt.util.ExplytUastUtil.getCommentText
import com.intellij.codeInsight.AnnotationUtil

import com.intellij.codeInsight.daemon.LineMarkerInfo
import com.intellij.codeInsight.daemon.LineMarkerProviderDescriptor
import com.intellij.openapi.editor.markup.GutterIconRenderer
Expand Down Expand Up @@ -53,31 +53,23 @@ class ControllerEndpointActionsLineMarkerProvider : LineMarkerProviderDescriptor
val controllerName = psiClass.name ?: return null

val requestMappingMah = MetaAnnotationsHolder.of(module, SpringWebClasses.REQUEST_MAPPING)
val path = requestMappingMah.getAnnotationMemberValues(mappingSource, setOf("path", "value")).asSequence()
.mapNotNull { AnnotationUtil.getStringAttributeValue(it) }
.firstOrNull() ?: ""
val mapping = HandlerMethods.methodMapping(mappingSource, requestMappingMah)
val path = mapping.paths.firstOrNull()?.path ?: ""
if (OpenApiUtils.isAbsolutePath(path)) return null

val prefix = HandlerMethods.requestMappingPrefixes(psiClass, requestMappingMah).firstOrNull() ?: ""
if (OpenApiUtils.isAbsolutePath(prefix)) return null

val fullPath = SpringWebUtil.simplifyUrl(MappingPathPlaceholders.resolve(module, "$prefix/$path"))

val requestMethods =
requestMappingMah.getAnnotationMemberValues(mappingSource, setOf("method"))
.map { it.text.split('.').last() }
val produces = requestMappingMah.getAnnotationMemberValues(mappingSource, setOf("produces"))
.mapNotNull { AnnotationUtil.getStringAttributeValue(it) }
val consumes = requestMappingMah.getAnnotationMemberValues(mappingSource, setOf("consumes"))
.mapNotNull { AnnotationUtil.getStringAttributeValue(it) }

val description = uMethod.comments.firstOrNull()?.getCommentText() ?: ""
val returnType = uMethod.returnType
val returnTypeFqn = SpringWebUtil.getTypeFqn(returnType, psiMethod.language)

val endpointElement = EndpointInfo(
fullPath,
requestMethods,
mapping.methods,
psiElement,
uMethod.name,
controllerName.replace("controller", "", true)
Expand All @@ -88,8 +80,8 @@ class ControllerEndpointActionsLineMarkerProvider : LineMarkerProviderDescriptor
SpringWebUtil.collectRequestParameters(psiMethod),
SpringWebUtil.getRequestBodyInfo(psiMethod),
SpringWebUtil.collectRequestHeaders(psiMethod),
produces,
consumes
mapping.produces,
mapping.consumes
)

return LineMarkerInfo(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import com.explyt.spring.web.util.SpringWebUtil
import com.explyt.spring.web.util.SpringWebUtil.removeParams
import com.explyt.util.ExplytPsiUtil.isMetaAnnotatedBy
import com.explyt.util.ExplytUastUtil.getCommentText
import com.intellij.codeInsight.AnnotationUtil

import com.intellij.execution.lineMarker.RunLineMarkerContributor
import com.intellij.icons.AllIcons
import com.intellij.openapi.module.ModuleUtilCore
Expand Down Expand Up @@ -71,9 +71,7 @@ class EndpointRunLineMarkerProvider : RunLineMarkerContributor() {
psiMethod: PsiMethod,
mappingSource: PsiMethod,
): String {
var path = requestMappingMah.getAnnotationMemberValues(mappingSource, setOf("path", "value")).asSequence()
.mapNotNull { AnnotationUtil.getStringAttributeValue(it) }
.firstOrNull() ?: ""
var path = HandlerMethods.methodMapping(mappingSource, requestMappingMah).paths.firstOrNull()?.path ?: ""
if (isAbsolutePath(path)) return path

val containingClass = psiMethod.containingClass ?: return path
Expand All @@ -93,24 +91,18 @@ class EndpointRunLineMarkerProvider : RunLineMarkerContributor() {
val module = ModuleUtilCore.findModuleForPsiElement(psiMethod) ?: return null


val requestMappingMah = MetaAnnotationsHolder.of(module, SpringWebClasses.REQUEST_MAPPING)
val produces = requestMappingMah.getAnnotationMemberValues(mappingSource, setOf("produces"))
.mapNotNull { AnnotationUtil.getStringAttributeValue(it) }
val consumes = requestMappingMah.getAnnotationMemberValues(mappingSource, setOf("consumes"))
.mapNotNull { AnnotationUtil.getStringAttributeValue(it) }
val mapping = HandlerMethods.methodMapping(
mappingSource, MetaAnnotationsHolder.of(module, SpringWebClasses.REQUEST_MAPPING)
)

val fullPath = SpringWebUtil.simplifyUrl(removeParams(apiPath))

val requestMethods =
requestMappingMah.getAnnotationMemberValues(mappingSource, setOf("method"))
.map { it.text.split('.').last() }

val description = uMethod.comments.firstOrNull()?.getCommentText() ?: ""
val returnTypeFqn = SpringWebUtil.getTypeFqn(HandlerSignature.declaredReturnType(psiMethod), psiMethod.language)

return EndpointInfo(
fullPath,
requestMethods,
mapping.methods,
psiMethod,
uMethod.name,
"default",
Expand All @@ -120,8 +112,8 @@ class EndpointRunLineMarkerProvider : RunLineMarkerContributor() {
SpringWebUtil.collectRequestParameters(psiMethod),
SpringWebUtil.getRequestBodyInfo(psiMethod),
SpringWebUtil.collectRequestHeaders(psiMethod),
produces,
consumes
mapping.produces,
mapping.consumes
)
}

Expand Down
Loading
Loading