diff --git a/src/main/kotlin/insight/InsightUtil.kt b/src/main/kotlin/insight/InsightUtil.kt index 0b577d85f..1742b836f 100644 --- a/src/main/kotlin/insight/InsightUtil.kt +++ b/src/main/kotlin/insight/InsightUtil.kt @@ -18,12 +18,13 @@ import org.jetbrains.uast.UClass import org.jetbrains.uast.UElement import org.jetbrains.uast.UMethod import org.jetbrains.uast.UParameter +import org.jetbrains.uast.getParentOfType import org.jetbrains.uast.toUElementOfType val UElement.uastEventListener: Pair? get() { // The PsiIdentifier is going to be a method of course! - val method = this.uastParent as? UMethod ?: return null + val method = this.getParentOfType() ?: return null if (method.javaPsi.hasModifierProperty(PsiModifier.ABSTRACT)) { // I don't think any implementation allows for abstract method listeners. return null diff --git a/src/main/kotlin/insight/ListenerLineMarkerProvider.kt b/src/main/kotlin/insight/ListenerLineMarkerProvider.kt index 8fbb53a7c..332bc55ba 100644 --- a/src/main/kotlin/insight/ListenerLineMarkerProvider.kt +++ b/src/main/kotlin/insight/ListenerLineMarkerProvider.kt @@ -45,7 +45,13 @@ class ListenerLineMarkerProvider : LineMarkerProviderDescriptor() { return null } - val listener = element.toUElementOfType()?.uastEventListener ?: return null + val identifier = element.toUElementOfType() + if (identifier?.uastParent !is UMethod) { + // We only target the method name + return null + } + + val listener = identifier.uastEventListener ?: return null // By this point, we can guarantee that the action of "go to declaration" will work // since the PsiClass can be resolved, meaning the event listener is listening to // a valid event. diff --git a/src/main/kotlin/platform/sponge/completion/SpongeCompletionConfidence.kt b/src/main/kotlin/platform/sponge/completion/SpongeCompletionConfidence.kt deleted file mode 100644 index 51d344dc7..000000000 --- a/src/main/kotlin/platform/sponge/completion/SpongeCompletionConfidence.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Minecraft Dev for IntelliJ - * - * https://minecraftdev.org - * - * Copyright (c) 2021 minecraft-dev - * - * MIT License - */ - -package com.demonwav.mcdev.platform.sponge.completion - -import com.demonwav.mcdev.platform.sponge.util.SpongeConstants -import com.demonwav.mcdev.util.findContainingMethod -import com.intellij.codeInsight.completion.CompletionConfidence -import com.intellij.codeInsight.completion.SkipAutopopupInStrings -import com.intellij.psi.PsiAnnotation -import com.intellij.psi.PsiAnnotationMemberValue -import com.intellij.psi.PsiElement -import com.intellij.psi.PsiFile -import com.intellij.psi.util.parentOfTypes -import com.intellij.util.ThreeState - -class SpongeCompletionConfidence : CompletionConfidence() { - - override fun shouldSkipAutopopup(element: PsiElement, psiFile: PsiFile, offset: Int): ThreeState { - if (!SkipAutopopupInStrings.isInStringLiteral(element)) { - return ThreeState.UNSURE - } - - val memberValue = element.parentOfTypes(PsiAnnotationMemberValue::class) ?: return ThreeState.UNSURE - val annotation = memberValue.parentOfTypes(PsiAnnotation::class) ?: return ThreeState.UNSURE - - val method = element.findContainingMethod() ?: return ThreeState.UNSURE - return if ( - method.hasAnnotation(SpongeConstants.LISTENER_ANNOTATION) && - annotation.qualifiedName == SpongeConstants.GETTER_ANNOTATION - ) { - ThreeState.NO - } else { - ThreeState.UNSURE - } - } -} diff --git a/src/main/kotlin/platform/sponge/completion/SpongeGetterFilterCompletionContributor.kt b/src/main/kotlin/platform/sponge/completion/SpongeGetterFilterCompletionContributor.kt deleted file mode 100644 index f179e6014..000000000 --- a/src/main/kotlin/platform/sponge/completion/SpongeGetterFilterCompletionContributor.kt +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Minecraft Dev for IntelliJ - * - * https://minecraftdev.org - * - * Copyright (c) 2021 minecraft-dev - * - * MIT License - */ - -package com.demonwav.mcdev.platform.sponge.completion - -import com.demonwav.mcdev.platform.sponge.inspection.SpongeInvalidGetterTargetInspection -import com.demonwav.mcdev.platform.sponge.util.SpongeConstants -import com.demonwav.mcdev.platform.sponge.util.isValidSpongeListener -import com.demonwav.mcdev.platform.sponge.util.resolveSpongeGetterTarget -import com.demonwav.mcdev.util.findContainingMethod -import com.demonwav.mcdev.util.isJavaOptional -import com.demonwav.mcdev.util.withImportInsertion -import com.intellij.codeInsight.completion.CompletionContributor -import com.intellij.codeInsight.completion.CompletionParameters -import com.intellij.codeInsight.completion.CompletionResultSet -import com.intellij.codeInsight.completion.JavaCompletionContributor -import com.intellij.codeInsight.completion.JavaLookupElementBuilder -import com.intellij.codeInsight.completion.PrioritizedLookupElement -import com.intellij.codeInsight.lookup.LookupElementBuilder -import com.intellij.psi.JavaPsiFacade -import com.intellij.psi.JavaTokenType -import com.intellij.psi.PsiAnnotation -import com.intellij.psi.PsiClassType -import com.intellij.psi.PsiJavaToken -import com.intellij.psi.PsiModifierList -import com.intellij.psi.PsiPrimitiveType -import com.intellij.psi.PsiType -import com.intellij.psi.PsiTypeElement -import com.intellij.psi.impl.source.PsiClassReferenceType -import com.intellij.psi.search.ProjectScope -import com.intellij.psi.util.PropertyUtil -import com.intellij.psi.util.PsiTreeUtil -import com.intellij.psi.util.parentOfType -import com.intellij.psi.util.parentOfTypes -import org.jetbrains.plugins.groovy.lang.psi.util.childrenOfType - -class SpongeGetterFilterCompletionContributor : CompletionContributor() { - - override fun fillCompletionVariants(parameters: CompletionParameters, result: CompletionResultSet) { - val position = parameters.position - if (!JavaCompletionContributor.isInJavaContext(position)) { - return - } - - val eventHandler = position.findContainingMethod() ?: return - if (!eventHandler.isValidSpongeListener()) { - return - } - - val prevVisibleLeaf = PsiTreeUtil.prevVisibleLeaf(position) - if (prevVisibleLeaf is PsiJavaToken && prevVisibleLeaf.tokenType == JavaTokenType.COMMA) { - // We are right after a comma - val projectScope = ProjectScope.getAllScope(parameters.position.project) - val getterAnnoClass = JavaPsiFacade.getInstance(parameters.position.project) - .findClass(SpongeConstants.GETTER_ANNOTATION, projectScope) - - if (getterAnnoClass != null) { - result.addElement( - JavaLookupElementBuilder.forClass(getterAnnoClass, getterAnnoClass.name, true) - .withTypeText("Event filter") - .withInsertHandler { context, _ -> - val at = context.document.text[context.startOffset - 1] - if (at != '@') { - context.document.insertString(context.startOffset, "@") - context.commitDocument() - } - - val inserted = context.file.findElementAt(context.startOffset) - ?.parentOfTypes(PsiAnnotation::class) ?: return@withInsertHandler - SpongeInvalidGetterTargetInspection.QuickFix.doFix(getterAnnoClass.project, inserted) - } - ) - } - } else if (prevVisibleLeaf is PsiJavaToken && prevVisibleLeaf.tokenType != JavaTokenType.AT) { - // We are not trying to complete an annotation - val modifierList = prevVisibleLeaf.parentOfType() - val getterAnnotation = modifierList?.childrenOfType() - ?.firstOrNull { it.hasQualifiedName(SpongeConstants.GETTER_ANNOTATION) } - if (getterAnnotation != null) { - // We are right after a @Getter() - completeGetterParameterType(getterAnnotation, result) - } - } - - val paramType = PsiTreeUtil.getPrevSiblingOfType(position, PsiTypeElement::class.java) - if (paramType != null) { - // We are completing the parameter name - val mods = PsiTreeUtil.getPrevSiblingOfType(paramType, PsiModifierList::class.java) - val getter = mods?.childrenOfType() - ?.firstOrNull { it.hasQualifiedName(SpongeConstants.GETTER_ANNOTATION) } - val getterTargetName = getter?.resolveSpongeGetterTarget()?.name - if (getterTargetName != null) { - val propertyName = PropertyUtil.getPropertyName(getterTargetName) - if (propertyName != null) { - val element = LookupElementBuilder.create(propertyName) - result.addElement(PrioritizedLookupElement.withPriority(element, 100.0)) - } - } - } - } - - private fun completeGetterParameterType(annotation: PsiAnnotation, result: CompletionResultSet) { - val getterTarget = annotation.resolveSpongeGetterTarget() - val classType = getterTarget?.returnType ?: return - suggestGetterParameter(classType, result) - - val classReferenceType = classType as? PsiClassReferenceType ?: return - val getterTargetClass = classReferenceType.resolve() ?: return - if (getterTargetClass.isJavaOptional()) { - val psiType = classReferenceType.parameters.firstOrNull() - if (psiType != null) { - suggestGetterParameter(psiType, result) - } - } - } - - private fun suggestGetterParameter(psiType: PsiType, result: CompletionResultSet) { - if (psiType is PsiPrimitiveType) { - val element = LookupElementBuilder.create(psiType.name) - .bold() - .withTypeText("@Getter target type") - result.addElement(element) - } else if (psiType is PsiClassType) { - val resolveResult = psiType.resolveGenerics() - val resolvedClass = resolveResult.element ?: return - val genericTypes = resolveResult.substitutor.substitutionMap.values - - val genericClasses = mutableListOf(resolvedClass) - genericTypes.mapNotNullTo(genericClasses) { (it as? PsiClassType)?.resolve() } - - val element = JavaLookupElementBuilder.forClass(resolvedClass, psiType.presentableText, true) - .withTypeText("@Getter target type") - .withImportInsertion(genericClasses) - result.addElement(element) - } - } -} diff --git a/src/main/kotlin/platform/sponge/inspection/SpongeInvalidGetterTargetInspection.kt b/src/main/kotlin/platform/sponge/inspection/SpongeInvalidGetterTargetInspection.kt index 7e68a8073..4d277cd43 100644 --- a/src/main/kotlin/platform/sponge/inspection/SpongeInvalidGetterTargetInspection.kt +++ b/src/main/kotlin/platform/sponge/inspection/SpongeInvalidGetterTargetInspection.kt @@ -13,90 +13,43 @@ package com.demonwav.mcdev.platform.sponge.inspection import com.demonwav.mcdev.platform.sponge.util.SpongeConstants import com.demonwav.mcdev.platform.sponge.util.isValidSpongeListener import com.demonwav.mcdev.platform.sponge.util.resolveSpongeGetterTarget -import com.intellij.codeInsight.AutoPopupController +import com.intellij.codeInspection.AbstractBaseUastLocalInspectionTool +import com.intellij.codeInspection.InspectionManager import com.intellij.codeInspection.ProblemDescriptor -import com.intellij.openapi.project.Project -import com.intellij.psi.JavaPsiFacade -import com.intellij.psi.PsiAnnotation -import com.intellij.psi.PsiMethod -import com.intellij.psi.util.PsiEditorUtil -import com.intellij.structuralsearch.plugin.util.SmartPsiPointer -import com.siyeh.ig.BaseInspection -import com.siyeh.ig.BaseInspectionVisitor -import com.siyeh.ig.InspectionGadgetsFix +import com.intellij.codeInspection.ProblemHighlightType +import org.jetbrains.uast.UMethod -class SpongeInvalidGetterTargetInspection : BaseInspection() { +class SpongeInvalidGetterTargetInspection : AbstractBaseUastLocalInspectionTool() { override fun getDisplayName() = "@Getter targeted method does not exist" - override fun buildErrorString(vararg infos: Any?) = staticDescription - override fun getStaticDescription() = "@Getter must target a method accessible from the event class of this listener" - override fun buildVisitor(): BaseInspectionVisitor { - return object : BaseInspectionVisitor() { - override fun visitMethod(method: PsiMethod) { - if (method.parameters.size < 2 || !method.isValidSpongeListener()) { - return - } - - for (i in 1 until method.parameters.size) { - val parameter = method.parameters[i] - val getterAnnotation = - parameter.getAnnotation(SpongeConstants.GETTER_ANNOTATION) as? PsiAnnotation ?: continue - - val getterTarget = getterAnnotation.resolveSpongeGetterTarget() - if (getterTarget == null) { - val attribute = getterAnnotation.findAttributeValue("value") - if (attribute == null) { - registerError(getterAnnotation, getterAnnotation) - } else { - registerError(attribute) - } - continue - } - } - } - } - } - - override fun buildFix(vararg infos: Any?): InspectionGadgetsFix? { - if (infos.isEmpty()) { - return null - } - - val annotation = infos[0] as PsiAnnotation - if (!annotation.isWritable) { + override fun checkMethod( + method: UMethod, + manager: InspectionManager, + isOnTheFly: Boolean + ): Array? { + if (!method.isValidSpongeListener()) { return null } - return QuickFix(annotation, "Add target method") - } - - class QuickFix(annotation: PsiAnnotation, private val name: String) : InspectionGadgetsFix() { - - private val pointer = SmartPsiPointer(annotation) - - override fun doFix(project: Project, descriptor: ProblemDescriptor?) { - doFix(project, pointer.element as PsiAnnotation) - } - - override fun getFamilyName() = name - - override fun getName() = name - - companion object { - - fun doFix(project: Project, annotation: PsiAnnotation) { - val value = JavaPsiFacade.getElementFactory(project).createExpressionFromText("\"\"", annotation) - val newValue = annotation.setDeclaredAttributeValue(null, value) - val editor = PsiEditorUtil.Service.getInstance().findEditorByPsiElement(annotation) ?: return - editor.caretModel.removeSecondaryCarets() - editor.selectionModel.removeSelection() - editor.caretModel.moveToOffset(newValue.textOffset + 1) - AutoPopupController.getInstance(project).scheduleAutoPopup(editor) + val problems = mutableListOf() + for (parameter in method.uastParameters.drop(1)) { + val getter = parameter.findAnnotation(SpongeConstants.GETTER_ANNOTATION) ?: continue + if (getter.resolveSpongeGetterTarget() == null) { + getter.findAttributeValue("value")?.sourcePsi?.let { problemAnchor -> + problems += manager.createProblemDescriptor( + problemAnchor, + this.staticDescription, + isOnTheFly, + null, + ProblemHighlightType.GENERIC_ERROR_OR_WARNING + ) + } } } + return problems.toTypedArray() } } diff --git a/src/main/kotlin/platform/sponge/inspection/SpongeWrongGetterTypeInspection.kt b/src/main/kotlin/platform/sponge/inspection/SpongeWrongGetterTypeInspection.kt index dca1fbdd5..6633a53ce 100644 --- a/src/main/kotlin/platform/sponge/inspection/SpongeWrongGetterTypeInspection.kt +++ b/src/main/kotlin/platform/sponge/inspection/SpongeWrongGetterTypeInspection.kt @@ -14,126 +14,175 @@ import com.demonwav.mcdev.platform.sponge.util.SpongeConstants import com.demonwav.mcdev.platform.sponge.util.isValidSpongeListener import com.demonwav.mcdev.platform.sponge.util.resolveSpongeGetterTarget import com.demonwav.mcdev.util.isJavaOptional -import com.intellij.lang.jvm.types.JvmReferenceType +import com.intellij.codeInspection.AbstractBaseUastLocalInspectionTool +import com.intellij.codeInspection.InspectionManager +import com.intellij.codeInspection.IntentionAndQuickFixAction +import com.intellij.codeInspection.LocalQuickFix +import com.intellij.codeInspection.ProblemDescriptor +import com.intellij.codeInspection.ProblemHighlightType +import com.intellij.lang.jvm.JvmMethod +import com.intellij.lang.jvm.actions.createChangeParametersActions +import com.intellij.lang.jvm.actions.expectedParameter +import com.intellij.lang.jvm.actions.updateMethodParametersRequest +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project import com.intellij.psi.JavaPsiFacade -import com.intellij.psi.PsiClass import com.intellij.psi.PsiClassType -import com.intellij.psi.PsiMethod -import com.intellij.psi.PsiParameter +import com.intellij.psi.PsiFile import com.intellij.psi.PsiPrimitiveType +import com.intellij.psi.PsiSubstitutor import com.intellij.psi.PsiType -import com.intellij.psi.PsiTypeElement -import com.siyeh.ig.BaseInspection -import com.siyeh.ig.BaseInspectionVisitor +import com.intellij.psi.util.TypeConversionUtil import com.siyeh.ig.InspectionGadgetsFix +import java.util.function.Supplier +import org.jetbrains.uast.UMethod +import org.jetbrains.uast.getContainingUClass -class SpongeWrongGetterTypeInspection : BaseInspection() { +class SpongeWrongGetterTypeInspection : AbstractBaseUastLocalInspectionTool() { override fun getDisplayName() = "Parameter's type is not assignable to its @Getter method return type" - override fun buildErrorString(vararg infos: Any?) = staticDescription - override fun getStaticDescription() = "@Getter requires the parameter's type to be assignable from the annotation's target method return type" - override fun buildVisitor(): BaseInspectionVisitor { - return object : BaseInspectionVisitor() { - override fun visitMethod(method: PsiMethod) { - if (method.parameters.size < 2 || !method.isValidSpongeListener()) { - return - } + override fun checkMethod( + method: UMethod, + manager: InspectionManager, + isOnTheFly: Boolean + ): Array? { + val parameters = method.uastParameters + if (parameters.size < 2 || !method.isValidSpongeListener()) { + return null + } - // We start at 1 because the first parameter is the event - for (i in 1 until method.parameters.size) { - val parameter = method.parameterList.parameters[i] - val getterAnnotation = - parameter.getAnnotation(SpongeConstants.GETTER_ANNOTATION) ?: continue + val eventClassType = parameters.first().type as? PsiClassType + val resolveEventClassTypeGenerics = eventClassType?.resolveGenerics() + val eventTypeSubstitutor = resolveEventClassTypeGenerics?.substitutor ?: PsiSubstitutor.EMPTY + resolveEventClassTypeGenerics?.element?.let { eventTypeSubstitutor.putAll(it, eventClassType.parameters) } + + val problems = mutableListOf() + // We start at 1 because the first parameter is the event + for (i in 1 until parameters.size) { + val parameter = parameters[i] + val getterAnnotation = parameter.findAnnotation(SpongeConstants.GETTER_ANNOTATION) ?: continue + + val getterMethod = getterAnnotation.resolveSpongeGetterTarget() ?: continue + + val getterClass = getterMethod.getContainingUClass() ?: continue + val eventClass = eventClassType?.resolve() ?: continue + val getterSubst = TypeConversionUtil.getSuperClassSubstitutor( + getterClass.javaPsi, + eventClass, + eventTypeSubstitutor + ) + val getterReturnType = getterMethod.returnType?.let(getterSubst::substitute) ?: continue + val parameterType = parameter.type + if (getterReturnType.isAssignableFrom(parameterType)) { + continue + } - val getterMethod = getterAnnotation.resolveSpongeGetterTarget() ?: continue - val getterReturnType = getterMethod.returnType ?: continue - val parameterType = parameter.type - if (getterReturnType.isAssignableFrom(parameterType)) { - continue - } + if (isOptional(getterReturnType)) { + val getterOptionalType = getFirstGenericType(getterReturnType) + if (getterOptionalType != null && areInSameHierarchy(getterOptionalType, parameterType)) { + continue + } - if (isOptional(getterReturnType)) { - val getterOptionalType = getterMethod.returnTypeElement?.let(::getFirstGenericType) - if (getterOptionalType != null && areInSameHierarchy(getterOptionalType, parameterType)) { - continue - } - - if (getterOptionalType != null && isOptional(parameterType)) { - val paramOptionalType = parameter.typeElement?.let(::getFirstGenericType) - if (paramOptionalType != null && - areInSameHierarchy(getterOptionalType, paramOptionalType) - ) { - continue - } - } + if (getterOptionalType != null && isOptional(parameterType)) { + val paramOptionalType = getFirstGenericType(parameterType) + if (paramOptionalType != null && areInSameHierarchy(getterOptionalType, paramOptionalType)) { + continue } - - registerError(parameter.typeElement ?: parameter, parameter, getterMethod.returnTypeElement!!) } } + + // Prefer highlighting the type, but if type is absent use the whole parameter instead + val typeReference = parameter.typeReference ?: continue + val location = typeReference.sourcePsi?.takeUnless { it.textRange.isEmpty } ?: continue + val methodJava = method.javaPsi as JvmMethod + val fixes = this.createFixes(methodJava, getterReturnType, i, manager.project) + problems += manager.createProblemDescriptor( + location, + this.staticDescription, + isOnTheFly, + fixes, + ProblemHighlightType.GENERIC_ERROR_OR_WARNING + ) } + return problems.toTypedArray() } private fun areInSameHierarchy(aType: PsiType, otherType: PsiType): Boolean = aType.isAssignableFrom(otherType) || otherType.isAssignableFrom(aType) private fun isOptional(type: PsiType): Boolean { - if (type is PsiPrimitiveType) { - return false - } - - val typeClass = (type as? JvmReferenceType)?.resolve() as? PsiClass ?: return false - return typeClass.isJavaOptional() && typeClass.hasTypeParameters() + val typeClass = type as? PsiClassType ?: return false + return typeClass.isJavaOptional() && typeClass.hasParameters() } - private fun getFirstGenericType(typeElement: PsiTypeElement): PsiType? { - val paramRefType = typeElement.type as? JvmReferenceType ?: return null - return paramRefType.typeArguments().firstOrNull() as? PsiType - } - - override fun buildFixes(vararg infos: Any?): Array { - val param = infos[0] as PsiParameter - if (!param.isWritable) { - return InspectionGadgetsFix.EMPTY_ARRAY - } - - val newTypeElement = infos[1] as PsiTypeElement - val newType = newTypeElement.type - if (newType is PsiPrimitiveType || - newType is PsiClassType && newType.hasParameters() && !newType.isJavaOptional() + private fun getFirstGenericType(typeElement: PsiType): PsiType? = + (typeElement as? PsiClassType)?.parameters?.firstOrNull() + + private fun createFixes( + method: JvmMethod, + expectedType: PsiType, + paramIndex: Int, + project: Project + ): Array { + if (expectedType is PsiPrimitiveType || + expectedType is PsiClassType && !isOptional(expectedType) ) { - return arrayOf(createFix(param, newTypeElement)) + // The getter does not return an Optional, simply suggest the return type + return arrayOf(Fix(method, paramIndex, expectedType)) } - val elementFactory = JavaPsiFacade.getElementFactory(param.project) - - val newTypeRef = newTypeElement.type as? JvmReferenceType - val newClassType = (newTypeRef?.resolve() as? PsiClass)?.let { - if (it.isJavaOptional() && newTypeRef.typeArguments().count() > 0) { - val wrappedType = newTypeRef.typeArguments().first() - val resolveResult = (wrappedType as? PsiClassType)?.resolveGenerics() ?: return@let null - val element = resolveResult.element ?: return@let null - return@let elementFactory.createType(element, resolveResult.substitutor) - } - - return@let elementFactory.createType(it) - } ?: return InspectionGadgetsFix.EMPTY_ARRAY + val elementFactory = JavaPsiFacade.getElementFactory(project) + + val expectedClassType = expectedType as? PsiClassType + ?: return InspectionGadgetsFix.EMPTY_ARRAY + val fixedClassType = if (isOptional(expectedClassType)) { + val wrappedType = expectedClassType.parameters.first() + val resolveResult = (wrappedType as? PsiClassType)?.resolveGenerics() + ?: return InspectionGadgetsFix.EMPTY_ARRAY + val element = resolveResult.element + ?: return InspectionGadgetsFix.EMPTY_ARRAY + elementFactory.createType(element, resolveResult.substitutor) + } else { + val resolvedClass = expectedClassType.resolve() + ?: return InspectionGadgetsFix.EMPTY_ARRAY + elementFactory.createType(resolvedClass) + } - val unwrappedNewTypeElement = elementFactory.createTypeElement(newClassType) + // Suggest a non-Optional version too return arrayOf( - createFix(param, newTypeElement), - createFix(param, unwrappedNewTypeElement) + Fix(method, paramIndex, expectedType), + Fix(method, paramIndex, fixedClassType) ) } - private fun createFix(parameter: PsiParameter, typeElement: PsiTypeElement): InspectionGadgetsFix = - UseGetterReturnTypeInspectionGadgetsFix( - parameter, - typeElement, - "Set parameter type to ${typeElement.type.presentableText}" - ) + private class Fix( + method: JvmMethod, + val paramIndex: Int, + val expectedType: PsiType, + ) : IntentionAndQuickFixAction() { + + private val myText: String = "Set parameter type to ${expectedType.presentableText}" + private val methodPointer = Supplier { method } + + override fun applyFix(project: Project, file: PsiFile, editor: Editor?) { + val changeParamsRequest = updateMethodParametersRequest(methodPointer) { actualParams -> + val existingParam = actualParams[paramIndex] + val newParam = expectedParameter( + expectedType, + existingParam.semanticNames.first(), + existingParam.expectedAnnotations + ) + actualParams.toMutableList().also { it[paramIndex] = newParam } + } + val waw = createChangeParametersActions(methodPointer.get()!!, changeParamsRequest).firstOrNull() ?: return + waw.invoke(project, editor, file) + } + + override fun getName(): String = myText + override fun getFamilyName(): String = myText + } } diff --git a/src/main/kotlin/platform/sponge/inspection/UseGetterReturnTypeInspectionGadgetsFix.kt b/src/main/kotlin/platform/sponge/inspection/UseGetterReturnTypeInspectionGadgetsFix.kt deleted file mode 100644 index d79a8cec8..000000000 --- a/src/main/kotlin/platform/sponge/inspection/UseGetterReturnTypeInspectionGadgetsFix.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Minecraft Dev for IntelliJ - * - * https://minecraftdev.org - * - * Copyright (c) 2021 minecraft-dev - * - * MIT License - */ - -package com.demonwav.mcdev.platform.sponge.inspection - -import com.intellij.codeInspection.ProblemDescriptor -import com.intellij.lang.jvm.types.JvmReferenceType -import com.intellij.openapi.project.Project -import com.intellij.psi.PsiClass -import com.intellij.psi.PsiParameter -import com.intellij.psi.PsiPrimitiveType -import com.intellij.psi.PsiTypeElement -import com.intellij.psi.impl.source.PsiClassReferenceType -import com.intellij.structuralsearch.plugin.util.SmartPsiPointer -import com.siyeh.ig.InspectionGadgetsFix -import com.siyeh.ig.psiutils.ImportUtils - -class UseGetterReturnTypeInspectionGadgetsFix( - parameter: PsiParameter, - newType: PsiTypeElement, - private val name: String -) : InspectionGadgetsFix() { - - private val paramPointer: SmartPsiPointer = SmartPsiPointer(parameter) - private val newTypePointer: SmartPsiPointer = SmartPsiPointer(newType) - - override fun doFix(project: Project, descriptor: ProblemDescriptor) { - val parameter = paramPointer.element as? PsiParameter ?: return - val newTypeElement = newTypePointer.element as? PsiTypeElement ?: return - val newType = newTypeElement.type - if (newType is PsiPrimitiveType) { - parameter.typeElement!!.replace(newTypeElement) - return - } - - val newTypeRef = newTypeElement.type as JvmReferenceType - for (typeParam in newTypeRef.typeArguments()) { - val resolvedTypeParam = (typeParam as? PsiClassReferenceType)?.resolve() ?: continue - ImportUtils.addImportIfNeeded(resolvedTypeParam, parameter) - } - - val newTypeClass = newTypeRef.resolve() as? PsiClass ?: return - ImportUtils.addImportIfNeeded(newTypeClass, parameter) - parameter.typeElement!!.replace(newTypeElement) - } - - override fun getName() = name - - override fun getFamilyName() = name -} diff --git a/src/main/kotlin/platform/sponge/reference/GetterEventListenerReferenceResolver.kt b/src/main/kotlin/platform/sponge/reference/GetterEventListenerReferenceResolver.kt deleted file mode 100644 index fc6227cc9..000000000 --- a/src/main/kotlin/platform/sponge/reference/GetterEventListenerReferenceResolver.kt +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Minecraft Dev for IntelliJ - * - * https://minecraftdev.org - * - * Copyright (c) 2021 minecraft-dev - * - * MIT License - */ - -package com.demonwav.mcdev.platform.sponge.reference - -import com.demonwav.mcdev.platform.sponge.util.SpongeConstants -import com.demonwav.mcdev.platform.sponge.util.isValidSpongeListener -import com.demonwav.mcdev.util.findContainingMethod -import com.demonwav.mcdev.util.reference.ReferenceResolver -import com.intellij.codeInsight.completion.JavaLookupElementBuilder -import com.intellij.codeInsight.lookup.AutoCompletionPolicy -import com.intellij.codeInsight.lookup.LookupElement -import com.intellij.lang.jvm.JvmModifier -import com.intellij.lang.jvm.types.JvmReferenceType -import com.intellij.psi.CommonClassNames -import com.intellij.psi.PsiAnnotation -import com.intellij.psi.PsiClass -import com.intellij.psi.PsiElement -import com.intellij.psi.PsiLiteralExpression -import com.intellij.psi.PsiNameValuePair -import com.intellij.psi.PsiSubstitutor -import com.intellij.psi.PsiType -import com.intellij.psi.util.parentOfTypes -import com.intellij.util.ArrayUtil - -object GetterEventListenerReferenceResolver : ReferenceResolver() { - - override fun resolveReference(context: PsiElement): PsiElement? { - val method = context.findContainingMethod() ?: return null - if (!method.hasParameters()) { - return null - } - - val eventType = method.parameters[0].type as? JvmReferenceType ?: return null - - val methodName = (context as? PsiLiteralExpression)?.value?.toString() ?: return null - val methods = (eventType.resolve() as? PsiClass)?.findMethodsByName(methodName, true) ?: return null - return methods.firstOrNull { !it.hasParameters() } - } - - override fun collectVariants(context: PsiElement): Array { - val memberValue = context.parentOfTypes(PsiNameValuePair::class) ?: return ArrayUtil.EMPTY_OBJECT_ARRAY - if (memberValue.attributeName != "value") { - return ArrayUtil.EMPTY_OBJECT_ARRAY - } - - val annotation = memberValue.parentOfTypes(PsiAnnotation::class) - if (annotation?.hasQualifiedName(SpongeConstants.GETTER_ANNOTATION) == false) { - return ArrayUtil.EMPTY_OBJECT_ARRAY - } - - val eventHandler = context.findContainingMethod() ?: return ArrayUtil.EMPTY_OBJECT_ARRAY - if (!eventHandler.isValidSpongeListener()) { - return ArrayUtil.EMPTY_OBJECT_ARRAY - } - - val eventReferenceType = eventHandler.parameters[0].type as? JvmReferenceType - ?: return ArrayUtil.EMPTY_OBJECT_ARRAY - val eventClass = eventReferenceType.resolve() as? PsiClass ?: return ArrayUtil.EMPTY_OBJECT_ARRAY - val methods = mutableListOf() - for (method in eventClass.allMethods) { - if (method.returnType != PsiType.VOID && method.hasModifier(JvmModifier.PUBLIC) && - !method.hasParameters() && method.containingClass?.qualifiedName != CommonClassNames.JAVA_LANG_OBJECT - ) { - methods += JavaLookupElementBuilder.forMethod(method, PsiSubstitutor.EMPTY) - .withAutoCompletionPolicy(AutoCompletionPolicy.GIVE_CHANCE_TO_OVERWRITE) - } - } - - return methods.toTypedArray() - } -} diff --git a/src/main/kotlin/platform/sponge/reference/SpongeReferenceContributor.kt b/src/main/kotlin/platform/sponge/reference/SpongeReferenceContributor.kt index ec09c364c..116133f40 100644 --- a/src/main/kotlin/platform/sponge/reference/SpongeReferenceContributor.kt +++ b/src/main/kotlin/platform/sponge/reference/SpongeReferenceContributor.kt @@ -10,20 +10,92 @@ package com.demonwav.mcdev.platform.sponge.reference +import com.demonwav.mcdev.insight.uastEventListener import com.demonwav.mcdev.platform.sponge.util.SpongeConstants -import com.demonwav.mcdev.util.insideAnnotationAttribute -import com.intellij.patterns.PsiJavaPatterns -import com.intellij.patterns.StandardPatterns +import com.intellij.codeInsight.completion.JavaLookupElementBuilder +import com.intellij.patterns.PlatformPatterns +import com.intellij.psi.CommonClassNames +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiLanguageInjectionHost +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiModifier +import com.intellij.psi.PsiReference +import com.intellij.psi.PsiReferenceBase import com.intellij.psi.PsiReferenceContributor +import com.intellij.psi.PsiReferenceProvider import com.intellij.psi.PsiReferenceRegistrar +import com.intellij.psi.PsiSubstitutor +import com.intellij.psi.PsiType +import com.intellij.psi.filters.ElementFilter +import com.intellij.psi.filters.position.FilterPattern +import com.intellij.util.ArrayUtil +import com.intellij.util.ProcessingContext +import com.intellij.util.containers.map2Array +import org.jetbrains.uast.UAnnotation +import org.jetbrains.uast.ULiteralExpression +import org.jetbrains.uast.evaluateString +import org.jetbrains.uast.getParentOfType +import org.jetbrains.uast.toUElement +import org.jetbrains.uast.toUElementOfType class SpongeReferenceContributor : PsiReferenceContributor() { override fun registerReferenceProviders(registrar: PsiReferenceRegistrar) { registrar.registerReferenceProvider( - PsiJavaPatterns.psiLiteral(StandardPatterns.string()) - .insideAnnotationAttribute(SpongeConstants.GETTER_ANNOTATION), - GetterEventListenerReferenceResolver + PlatformPatterns.psiElement(PsiLanguageInjectionHost::class.java) + .and(FilterPattern(GetterAnnotationFilter)), + UastGetterEventListenerReferenceResolver, + PsiReferenceRegistrar.HIGHER_PRIORITY ) } } + +object UastGetterEventListenerReferenceResolver : PsiReferenceProvider() { + + override fun getReferencesByElement(element: PsiElement, context: ProcessingContext): Array = + arrayOf(GetterReference(element as PsiLanguageInjectionHost)) +} + +private class GetterReference(element: PsiLanguageInjectionHost) : PsiReferenceBase(element) { + override fun resolve(): PsiElement? { + val literal = element.toUElementOfType() ?: return null + val targetName = literal.evaluateString() ?: return null + + val (eventClass, _) = literal.uastEventListener ?: return null + return eventClass.javaPsi.findMethodsByName(targetName, true).firstOrNull(::isValidCandidate) + } + + override fun getVariants(): Array { + val literal = element.toUElementOfType() ?: return ArrayUtil.EMPTY_OBJECT_ARRAY + val (eventClass, _) = literal.uastEventListener + ?: return ArrayUtil.EMPTY_OBJECT_ARRAY + val methodByClass = mutableMapOf>() + for (method in eventClass.javaPsi.allMethods) { + if (!isValidCandidate(method)) { + continue + } + + val existingPair = methodByClass[method.name] + if (existingPair == null || method.containingClass!!.isInheritor(existingPair.second, true)) { + methodByClass[method.name] = method to method.containingClass!! + } + } + return methodByClass.values.map2Array { JavaLookupElementBuilder.forMethod(it.first, PsiSubstitutor.EMPTY) } + } +} + +private object GetterAnnotationFilter : ElementFilter { + override fun isAcceptable(element: Any, context: PsiElement?): Boolean { + val type = context.toUElement() ?: return false + val annotation = type.getParentOfType() ?: return false + return annotation.qualifiedName == SpongeConstants.GETTER_ANNOTATION + } + + override fun isClassAcceptable(hintClass: Class<*>): Boolean = + PsiLanguageInjectionHost::class.java.isAssignableFrom(hintClass) +} + +private fun isValidCandidate(method: PsiMethod): Boolean = method.returnType != PsiType.VOID && + !method.isConstructor && method.hasModifierProperty(PsiModifier.PUBLIC) && !method.hasParameters() && + method.containingClass?.qualifiedName != CommonClassNames.JAVA_LANG_OBJECT diff --git a/src/main/kotlin/platform/sponge/util/Events.kt b/src/main/kotlin/platform/sponge/util/Events.kt index 7e7cd6f8b..8f026299b 100644 --- a/src/main/kotlin/platform/sponge/util/Events.kt +++ b/src/main/kotlin/platform/sponge/util/Events.kt @@ -12,12 +12,19 @@ package com.demonwav.mcdev.platform.sponge.util import com.demonwav.mcdev.util.constantValue import com.demonwav.mcdev.util.findContainingMethod +import com.demonwav.mcdev.util.resolve import com.intellij.lang.jvm.types.JvmReferenceType import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiClass import com.intellij.psi.PsiMethod import com.intellij.psi.search.ProjectScope +import org.jetbrains.uast.UAnnotation +import org.jetbrains.uast.UClass +import org.jetbrains.uast.UMethod +import org.jetbrains.uast.evaluateString +import org.jetbrains.uast.getContainingUMethod +import org.jetbrains.uast.toUElementOfType fun PsiMethod.isValidSpongeListener(): Boolean { if (!this.hasAnnotation(SpongeConstants.LISTENER_ANNOTATION) || this.isConstructor || !this.hasParameters()) { @@ -45,3 +52,30 @@ fun PsiAnnotation.resolveSpongeGetterTarget(): PsiMethod? { val getterMethodName = this.findAttributeValue("value")?.constantValue?.toString() ?: return null return eventClass.findMethodsByName(getterMethodName, true).firstOrNull { !it.isConstructor && !it.hasParameters() } } + +fun UMethod.isValidSpongeListener(): Boolean { + if (this.findAnnotation(SpongeConstants.LISTENER_ANNOTATION) == null || + this.isConstructor || this.uastParameters.isEmpty() + ) { + return false + } + + val eventClass = this.uastParameters[0].typeReference?.resolve() ?: return false + val baseEventClass = JavaPsiFacade.getInstance(this.project) + .findClass(SpongeConstants.EVENT, ProjectScope.getAllScope(this.project)) ?: return false + return eventClass.isInheritor(baseEventClass, true) +} + +fun UMethod.resolveSpongeEventClass(): UClass? { + val eventParam = this.uastParameters.firstOrNull() ?: return null + return eventParam.typeReference?.resolve() +} + +fun UAnnotation.resolveSpongeGetterTarget(): UMethod? { + val method = this.getContainingUMethod() ?: return null + val eventClass = method.resolveSpongeEventClass() ?: return null + val getterMethodName = this.findAttributeValue("value")?.evaluateString() ?: return null + return eventClass.findMethodsByName(getterMethodName, true) + .firstOrNull { !it.isConstructor && !it.hasParameters() } + .toUElementOfType() +} diff --git a/src/main/kotlin/util/uast-utils.kt b/src/main/kotlin/util/uast-utils.kt new file mode 100644 index 000000000..47a7d9367 --- /dev/null +++ b/src/main/kotlin/util/uast-utils.kt @@ -0,0 +1,18 @@ +/* + * Minecraft Dev for IntelliJ + * + * https://minecraftdev.org + * + * Copyright (c) 2021 minecraft-dev + * + * MIT License + */ + +package com.demonwav.mcdev.util + +import com.intellij.psi.PsiClassType +import org.jetbrains.uast.UClass +import org.jetbrains.uast.UTypeReferenceExpression +import org.jetbrains.uast.toUElementOfType + +fun UTypeReferenceExpression.resolve(): UClass? = (this.type as? PsiClassType)?.resolve().toUElementOfType() diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 38c8c9f18..6ec9a472d 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -171,23 +171,25 @@ - + - + - - - + - - + + - - + + @@ -358,14 +360,14 @@