User: joe Date: 03 Nov 25 23:12 Revision: f6b51c91904eef599b5e8c0d7213d51750644e8f Summary: Add opcode warnings. Closes #2530 TeamCity URL: http://ci.mcdev.io:80/viewModification.html?tab=vcsModificationFiles&modId=10194&personal=false Index: src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt =================================================================== --- src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt (revision bf45f4e1399b0f66bf8b5e1ca16bbb357a81a466) +++ src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt (revision f6b51c91904eef599b5e8c0d7213d51750644e8f) @@ -88,7 +88,7 @@ private val targetMethod: MethodNode, ) { companion object { - private fun getInjectionPoint(at: PsiAnnotation): InjectionPoint<*>? { + fun getInjectionPoint(at: PsiAnnotation): InjectionPoint<*>? { var atCode = at.qualifiedName?.let { InjectionPointAnnotation.atCodeFor(it) } ?: at.findDeclaredAttributeValue("value")?.constantStringValue ?: return null Index: src/main/kotlin/platform/mixin/handlers/injectionPoint/FieldInjectionPoint.kt =================================================================== --- src/main/kotlin/platform/mixin/handlers/injectionPoint/FieldInjectionPoint.kt (revision bf45f4e1399b0f66bf8b5e1ca16bbb357a81a466) +++ src/main/kotlin/platform/mixin/handlers/injectionPoint/FieldInjectionPoint.kt (revision f6b51c91904eef599b5e8c0d7213d51750644e8f) @@ -76,6 +76,8 @@ } } + override val validOpcodes = VALID_OPCODES + override fun createNavigationVisitor( at: PsiAnnotation, target: MixinSelector?, Index: src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.kt =================================================================== --- src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.kt (revision bf45f4e1399b0f66bf8b5e1ca16bbb357a81a466) +++ src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.kt (revision f6b51c91904eef599b5e8c0d7213d51750644e8f) @@ -108,6 +108,8 @@ open fun isShiftDiscouraged(shift: Int, at: PsiAnnotation): Boolean = shift != 0 + open val validOpcodes: Set = emptySet() + abstract fun createNavigationVisitor( at: PsiAnnotation, target: MixinSelector?, Index: src/main/kotlin/platform/mixin/inspection/MixinCancellableInspection.kt =================================================================== --- src/main/kotlin/platform/mixin/inspection/MixinCancellableInspection.kt (revision bf45f4e1399b0f66bf8b5e1ca16bbb357a81a466) +++ src/main/kotlin/platform/mixin/inspection/MixinCancellableInspection.kt (revision bf45f4e1399b0f66bf8b5e1ca16bbb357a81a466) @@ -1,148 +0,0 @@ -/* - * Minecraft Development for IntelliJ - * - * https://mcdev.io/ - * - * Copyright (C) 2025 minecraft-dev - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published - * by the Free Software Foundation, version 3.0 only. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -package com.demonwav.mcdev.platform.mixin.inspection - -import com.demonwav.mcdev.platform.mixin.inspection.injector.CancellableBeforeSuperCallInspection -import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Annotations.INJECT -import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Classes.CALLBACK_INFO -import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Classes.CALLBACK_INFO_RETURNABLE -import com.demonwav.mcdev.util.fullQualifiedName -import com.intellij.codeInspection.LocalQuickFix -import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement -import com.intellij.codeInspection.ProblemHighlightType -import com.intellij.codeInspection.ProblemsHolder -import com.intellij.openapi.editor.Editor -import com.intellij.openapi.project.Project -import com.intellij.psi.JavaElementVisitor -import com.intellij.psi.PsiAnnotation -import com.intellij.psi.PsiClassType -import com.intellij.psi.PsiElement -import com.intellij.psi.PsiElementFactory -import com.intellij.psi.PsiElementVisitor -import com.intellij.psi.PsiExpressionList -import com.intellij.psi.PsiFile -import com.intellij.psi.PsiLiteral -import com.intellij.psi.PsiMethod -import com.intellij.psi.PsiReferenceExpression -import com.intellij.psi.search.searches.ReferencesSearch -import com.intellij.psi.util.PsiUtil - -class MixinCancellableInspection : MixinInspection() { - - override fun getStaticDescription(): String = - "Reports missing or unused cancellable @Inject usages" - - override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor = Visitor(holder) - - private class Visitor(private val holder: ProblemsHolder) : JavaElementVisitor() { - override fun visitMethod(method: PsiMethod) { - val injectAnnotation = method.getAnnotation(INJECT) ?: return - - val cancellableAttribute = injectAnnotation.findAttributeValue("cancellable") as? PsiLiteral ?: return - val isCancellable = cancellableAttribute.value == true - - val ciParam = method.parameterList.parameters.firstOrNull { - val className = (it.type as? PsiClassType)?.fullQualifiedName ?: return@firstOrNull false - className == CALLBACK_INFO || className == CALLBACK_INFO_RETURNABLE - } ?: return - - val ciType = (ciParam.type as? PsiClassType)?.resolve() ?: return - val searchingFor = ciType.findMethodsByName("setReturnValue", false) + - ciType.findMethodsByName("cancel", true) - searchingFor.ifEmpty { return } - - var mayUseCancel = false - var definitelyUsesCancel = false - for (ref in ReferencesSearch.search(ciParam)) { - val parent = PsiUtil.skipParenthesizedExprUp(ref.element.parent) - if (parent is PsiExpressionList) { - // method argument, we can't tell whether it uses cancel - mayUseCancel = true - } - val methodCall = parent as? PsiReferenceExpression ?: continue - if (methodCall.references.any { reference -> searchingFor.any(reference::isReferenceTo) }) { - definitelyUsesCancel = true - break - } - } - - if (definitelyUsesCancel && !isCancellable) { - val fixes = mutableListOf() - if (!CancellableBeforeSuperCallInspection.doesInjectBeforeSuperConstructorCall(injectAnnotation)) { - fixes += MakeInjectCancellableFix(injectAnnotation) - } - - holder.registerProblem( - method.nameIdentifier ?: method, - "@Inject must be marked as cancellable in order to be cancelled", - ProblemHighlightType.GENERIC_ERROR_OR_WARNING, - *fixes.toTypedArray(), - ) - } else if (!definitelyUsesCancel && !mayUseCancel && isCancellable) { - holder.registerProblem( - cancellableAttribute.parent, - "@Inject is cancellable but is never cancelled", - ProblemHighlightType.GENERIC_ERROR_OR_WARNING, - RemoveInjectCancellableFix(injectAnnotation), - ) - } - } - } - - private class MakeInjectCancellableFix(element: PsiAnnotation) : - LocalQuickFixAndIntentionActionOnPsiElement(element) { - - override fun getFamilyName(): String = "Mark as cancellable" - - override fun getText(): String = familyName - - override fun invoke( - project: Project, - file: PsiFile, - editor: Editor?, - startElement: PsiElement, - endElement: PsiElement, - ) { - val annotation = startElement as PsiAnnotation - val value = PsiElementFactory.getInstance(project).createExpressionFromText("true", annotation) - annotation.setDeclaredAttributeValue("cancellable", value) - } - } - - class RemoveInjectCancellableFix(element: PsiAnnotation) : - LocalQuickFixAndIntentionActionOnPsiElement(element) { - - override fun getFamilyName(): String = "Remove cancellable attribute" - - override fun getText(): String = familyName - - override fun invoke( - project: Project, - file: PsiFile, - editor: Editor?, - startElement: PsiElement, - endElement: PsiElement, - ) { - val annotation = startElement as PsiAnnotation - annotation.setDeclaredAttributeValue("cancellable", null) - } - } -} Index: src/main/kotlin/platform/mixin/inspection/fix/AnnotationAttributeFix.kt =================================================================== --- src/main/kotlin/platform/mixin/inspection/fix/AnnotationAttributeFix.kt (revision bf45f4e1399b0f66bf8b5e1ca16bbb357a81a466) +++ src/main/kotlin/platform/mixin/inspection/fix/AnnotationAttributeFix.kt (revision f6b51c91904eef599b5e8c0d7213d51750644e8f) @@ -29,7 +29,9 @@ import com.intellij.psi.PsiAnnotationMemberValue import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile +import com.intellij.psi.PsiLiteralExpression import com.intellij.psi.codeStyle.CodeStyleManager +import com.intellij.psi.codeStyle.JavaCodeStyleManager open class AnnotationAttributeFix( annotation: PsiAnnotation, @@ -93,9 +95,15 @@ override fun invoke(project: Project, file: PsiFile, startElement: PsiElement, endElement: PsiElement) { val annotation = startElement as? PsiAnnotation ?: return + var needsShortenClassReferences = false + for ((key, value) in attributes) { annotation.setDeclaredAttributeValue(key, value) + + if (value != null && value !is PsiLiteralExpression) { + needsShortenClassReferences = true - } + } + } // replace single "value = foo" with "foo" val attrs = annotation.parameterList.attributes @@ -108,5 +116,9 @@ } CodeStyleManager.getInstance(project).reformat(annotation.parameterList) + + if (needsShortenClassReferences) { + JavaCodeStyleManager.getInstance(project).shortenClassReferences(annotation) - } -} + } + } +} Index: src/main/kotlin/platform/mixin/inspection/injector/CancellableBeforeSuperCallInspection.kt =================================================================== --- src/main/kotlin/platform/mixin/inspection/injector/CancellableBeforeSuperCallInspection.kt (revision bf45f4e1399b0f66bf8b5e1ca16bbb357a81a466) +++ src/main/kotlin/platform/mixin/inspection/injector/CancellableBeforeSuperCallInspection.kt (revision f6b51c91904eef599b5e8c0d7213d51750644e8f) @@ -22,7 +22,6 @@ import com.demonwav.mcdev.platform.mixin.handlers.InjectorAnnotationHandler import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler -import com.demonwav.mcdev.platform.mixin.inspection.MixinCancellableInspection import com.demonwav.mcdev.platform.mixin.inspection.MixinInspection import com.demonwav.mcdev.platform.mixin.util.MethodTargetMember import com.demonwav.mcdev.platform.mixin.util.MixinConstants Index: src/main/kotlin/platform/mixin/inspection/injector/InjectorOpcodeInspection.kt =================================================================== --- src/main/kotlin/platform/mixin/inspection/injector/InjectorOpcodeInspection.kt (revision f6b51c91904eef599b5e8c0d7213d51750644e8f) +++ src/main/kotlin/platform/mixin/inspection/injector/InjectorOpcodeInspection.kt (revision f6b51c91904eef599b5e8c0d7213d51750644e8f) @@ -0,0 +1,109 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2025 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.mixin.inspection.injector + +import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler +import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.AtResolver +import com.demonwav.mcdev.platform.mixin.inspection.MixinInspection +import com.demonwav.mcdev.platform.mixin.inspection.fix.AnnotationAttributeFix +import com.demonwav.mcdev.platform.mixin.util.MethodTargetMember +import com.demonwav.mcdev.platform.mixin.util.MixinConstants +import com.demonwav.mcdev.util.constantValue +import com.intellij.codeInspection.LocalQuickFix +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.openapi.project.Project +import com.intellij.psi.JavaElementVisitor +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiExpression +import org.objectweb.asm.util.Printer + +class InjectorOpcodeInspection : MixinInspection() { + override fun getStaticDescription() = "Reports missing or invalid usages of opcode" + + override fun buildVisitor(holder: ProblemsHolder) = object : JavaElementVisitor() { + override fun visitAnnotation(annotation: PsiAnnotation) { + if (!annotation.hasQualifiedName(MixinConstants.Annotations.AT)) { + return + } + + val injectionPoint = AtResolver.getInjectionPoint(annotation) ?: return + val validOpcodes = injectionPoint.validOpcodes + val opcodeAttribute = annotation.findDeclaredAttributeValue("opcode") + val opcode = opcodeAttribute?.constantValue as? Int + + if (validOpcodes.isEmpty()) { + if (opcode != null) { + holder.registerProblem( + opcodeAttribute, + "Opcode is ignored by this injection point", + AnnotationAttributeFix(annotation, "opcode" to null) + ) + } + } else { + if (opcode == null) { + holder.registerProblem( + annotation.nameReferenceElement ?: annotation, + "Missing opcode", + *listOfNotNull(makeSpecifyOpcodeFix(holder.project, annotation)).toTypedArray() + ) + } else if (opcode !in validOpcodes) { + holder.registerProblem( + opcodeAttribute, + "Invalid opcode for this injection point" + ) + } + } + } + } + + private fun makeSpecifyOpcodeFix(project: Project, at: PsiAnnotation): LocalQuickFix? { + val injector = AtResolver.findInjectorAnnotation(at) ?: return null + val targetMethods = + MixinAnnotationHandler.resolveTarget(injector).filterIsInstance() + + val possibleOpcodes = mutableSetOf() + + for (method in targetMethods) { + val instructions = AtResolver(at, method.classAndMethod.clazz, method.classAndMethod.method) + .resolveInstructions() + for (insn in instructions) { + possibleOpcodes += insn.originalInsn.opcode + } + } + + val opcode = possibleOpcodes.singleOrNull()?.takeIf { it != -1 } ?: return null + val opcodeName = Printer.OPCODES[opcode] + val opcodeValue = JavaPsiFacade.getElementFactory(project).createExpressionFromText( + "org.objectweb.asm.Opcodes.$opcodeName", + at + ) + return AddOpcodeFix(at, opcodeName, opcodeValue) + } + + private class AddOpcodeFix(at: PsiAnnotation, private val opcodeName: String, opcodeValue: PsiExpression) : AnnotationAttributeFix( + at, + "opcode" to opcodeValue + ) { + override fun getFamilyName() = "Add opcode = Opcodes.$opcodeName" + override fun getText() = "Add opcode = Opcodes.$opcodeName" + } +} Index: src/main/kotlin/platform/mixin/inspection/injector/MixinCancellableInspection.kt =================================================================== --- src/main/kotlin/platform/mixin/inspection/injector/MixinCancellableInspection.kt (revision f6b51c91904eef599b5e8c0d7213d51750644e8f) +++ src/main/kotlin/platform/mixin/inspection/injector/MixinCancellableInspection.kt (revision f6b51c91904eef599b5e8c0d7213d51750644e8f) @@ -0,0 +1,148 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2025 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.mixin.inspection.injector + +import com.demonwav.mcdev.platform.mixin.inspection.MixinInspection +import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Annotations.INJECT +import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Classes.CALLBACK_INFO +import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Classes.CALLBACK_INFO_RETURNABLE +import com.demonwav.mcdev.util.fullQualifiedName +import com.intellij.codeInspection.LocalQuickFix +import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement +import com.intellij.codeInspection.ProblemHighlightType +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.psi.JavaElementVisitor +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiClassType +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiElementFactory +import com.intellij.psi.PsiElementVisitor +import com.intellij.psi.PsiExpressionList +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiLiteral +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiReferenceExpression +import com.intellij.psi.search.searches.ReferencesSearch +import com.intellij.psi.util.PsiUtil + +class MixinCancellableInspection : MixinInspection() { + + override fun getStaticDescription(): String = + "Reports missing or unused cancellable @Inject usages" + + override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor = Visitor(holder) + + private class Visitor(private val holder: ProblemsHolder) : JavaElementVisitor() { + override fun visitMethod(method: PsiMethod) { + val injectAnnotation = method.getAnnotation(INJECT) ?: return + + val cancellableAttribute = injectAnnotation.findAttributeValue("cancellable") as? PsiLiteral ?: return + val isCancellable = cancellableAttribute.value == true + + val ciParam = method.parameterList.parameters.firstOrNull { + val className = (it.type as? PsiClassType)?.fullQualifiedName ?: return@firstOrNull false + className == CALLBACK_INFO || className == CALLBACK_INFO_RETURNABLE + } ?: return + + val ciType = (ciParam.type as? PsiClassType)?.resolve() ?: return + val searchingFor = ciType.findMethodsByName("setReturnValue", false) + + ciType.findMethodsByName("cancel", true) + searchingFor.ifEmpty { return } + + var mayUseCancel = false + var definitelyUsesCancel = false + for (ref in ReferencesSearch.search(ciParam)) { + val parent = PsiUtil.skipParenthesizedExprUp(ref.element.parent) + if (parent is PsiExpressionList) { + // method argument, we can't tell whether it uses cancel + mayUseCancel = true + } + val methodCall = parent as? PsiReferenceExpression ?: continue + if (methodCall.references.any { reference -> searchingFor.any(reference::isReferenceTo) }) { + definitelyUsesCancel = true + break + } + } + + if (definitelyUsesCancel && !isCancellable) { + val fixes = mutableListOf() + if (!CancellableBeforeSuperCallInspection.doesInjectBeforeSuperConstructorCall(injectAnnotation)) { + fixes += MakeInjectCancellableFix(injectAnnotation) + } + + holder.registerProblem( + method.nameIdentifier ?: method, + "@Inject must be marked as cancellable in order to be cancelled", + ProblemHighlightType.GENERIC_ERROR_OR_WARNING, + *fixes.toTypedArray(), + ) + } else if (!definitelyUsesCancel && !mayUseCancel && isCancellable) { + holder.registerProblem( + cancellableAttribute.parent, + "@Inject is cancellable but is never cancelled", + ProblemHighlightType.GENERIC_ERROR_OR_WARNING, + RemoveInjectCancellableFix(injectAnnotation), + ) + } + } + } + + private class MakeInjectCancellableFix(element: PsiAnnotation) : + LocalQuickFixAndIntentionActionOnPsiElement(element) { + + override fun getFamilyName(): String = "Mark as cancellable" + + override fun getText(): String = familyName + + override fun invoke( + project: Project, + file: PsiFile, + editor: Editor?, + startElement: PsiElement, + endElement: PsiElement, + ) { + val annotation = startElement as PsiAnnotation + val value = PsiElementFactory.getInstance(project).createExpressionFromText("true", annotation) + annotation.setDeclaredAttributeValue("cancellable", value) + } + } + + class RemoveInjectCancellableFix(element: PsiAnnotation) : + LocalQuickFixAndIntentionActionOnPsiElement(element) { + + override fun getFamilyName(): String = "Remove cancellable attribute" + + override fun getText(): String = familyName + + override fun invoke( + project: Project, + file: PsiFile, + editor: Editor?, + startElement: PsiElement, + endElement: PsiElement, + ) { + val annotation = startElement as PsiAnnotation + annotation.setDeclaredAttributeValue("cancellable", null) + } + } +} Index: src/main/resources/META-INF/plugin.xml =================================================================== --- src/main/resources/META-INF/plugin.xml (revision bf45f4e1399b0f66bf8b5e1ca16bbb357a81a466) +++ src/main/resources/META-INF/plugin.xml (revision f6b51c91904eef599b5e8c0d7213d51750644e8f) @@ -1057,7 +1057,7 @@ enabledByDefault="true" level="WARNING" hasStaticDescription="true" - implementationClass="com.demonwav.mcdev.platform.mixin.inspection.MixinCancellableInspection"/> + implementationClass="com.demonwav.mcdev.platform.mixin.inspection.injector.MixinCancellableInspection"/> +