User: joe Date: 22 Jun 25 17:45 Revision: 3ef92c6f6e75c20372c9ca2890716b3295452a2a Summary: Add inspection to replace INVOKE_ASSIGN with expressions where possible TeamCity URL: https://ci.mcdev.io/viewModification.html?tab=vcsModificationFiles&modId=10076&personal=false Index: src/main/kotlin/platform/mixin/inspection/mixinextras/InvokeAssignReplaceWithExpressionInspection.kt =================================================================== --- src/main/kotlin/platform/mixin/inspection/mixinextras/InvokeAssignReplaceWithExpressionInspection.kt (revision 3ef92c6f6e75c20372c9ca2890716b3295452a2a) +++ src/main/kotlin/platform/mixin/inspection/mixinextras/InvokeAssignReplaceWithExpressionInspection.kt (revision 3ef92c6f6e75c20372c9ca2890716b3295452a2a) @@ -0,0 +1,192 @@ +/* + * 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.mixinextras + +import com.demonwav.mcdev.MinecraftProjectSettings +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.reference.MixinSelector +import com.demonwav.mcdev.platform.mixin.reference.parseMixinSelector +import com.demonwav.mcdev.platform.mixin.util.MethodTargetMember +import com.demonwav.mcdev.platform.mixin.util.MixinConstants +import com.demonwav.mcdev.platform.mixin.util.nextRealInsn +import com.demonwav.mcdev.util.BeforeOrAfter +import com.demonwav.mcdev.util.constantStringValue +import com.demonwav.mcdev.util.toJavaIdentifier +import com.intellij.codeInspection.LocalQuickFixOnPsiElement +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.PsiElement +import com.intellij.psi.PsiElementVisitor +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiModifierList +import com.intellij.psi.codeStyle.JavaCodeStyleManager +import org.objectweb.asm.Opcodes +import org.objectweb.asm.Type +import org.objectweb.asm.tree.MethodInsnNode +import org.objectweb.asm.tree.VarInsnNode + +class InvokeAssignReplaceWithExpressionInspection : MixinInspection() { + override fun getStaticDescription() = "Reports when INVOKE_ASSIGN could be replaced with a MixinExtras expression. " + + "Expressions are preferred over INVOKE_ASSIGN because they fail when an assignment doesn't exist." + + override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor { + val hasExpressions = JavaPsiFacade.getInstance(holder.project).findClass( + MixinConstants.MixinExtras.EXPRESSION, + holder.file.resolveScope + ) != null + if (!hasExpressions) { + return PsiElementVisitor.EMPTY_VISITOR + } + return object : JavaElementVisitor() { + override fun visitAnnotation(annotation: PsiAnnotation) { + if (!annotation.hasQualifiedName(MixinConstants.Annotations.AT)) { + return + } + val atValue = annotation.findDeclaredAttributeValue("value") ?: return + if (atValue.constantStringValue != "INVOKE_ASSIGN") { + return + } + val atTarget = annotation.findDeclaredAttributeValue("target") + if (atTarget == null) { + return + } + val target = parseMixinSelector(atTarget) ?: return + val methodInsn = resolveMethodInsn(annotation, target) ?: return + val customArgs = AtResolver.getArgs(annotation) + if (customArgs.containsKey("fuzz") || customArgs.containsKey("skip")) { + return + } + + holder.registerProblem( + atValue, + "INVOKE_ASSIGN could be replaced with expression", + ReplaceWithExpressionFix( + annotation, + methodInsn.name.toJavaIdentifier(), + methodInsn.opcode == Opcodes.INVOKESTATIC, + Type.getArgumentCount(methodInsn.desc) + ) + ) + } + } + } + + private fun resolveMethodInsn(at: PsiAnnotation, target: MixinSelector): MethodInsnNode? { + val injectorAnnotation = AtResolver.findInjectorAnnotation(at) ?: return null + val insns = MixinAnnotationHandler.resolveTarget(injectorAnnotation) + .flatMap { targetMember -> + if (targetMember !is MethodTargetMember) { + return@flatMap emptyList() + } + val instructions = targetMember.classAndMethod.method.instructions ?: return@flatMap emptyList() + instructions.asSequence() + .filterIsInstance() + .filter { target.matchMethod(it.owner, it.name, it.desc) } + .asIterable() + } + if (insns.isEmpty()) { + return null + } + for (insn in insns) { + val assignmentInsn = insn.nextRealInsn as? VarInsnNode ?: return null + if (assignmentInsn.opcode < Opcodes.ISTORE) { + // it's a load insn + return null + } + } + + val result = insns.first() + for (insn in insns) { + if (insn.opcode != result.opcode || insn.owner != result.owner || insn.name != result.name || insn.desc != result.desc) { + return null + } + } + + return result + } + + private class ReplaceWithExpressionFix( + at: PsiAnnotation, + private val definitionId: String, + private val isStatic: Boolean, + private val argCount: Int, + ) : LocalQuickFixOnPsiElement(at) { + override fun getFamilyName() = "Replace with expression" + override fun getText() = "Replace with expression" + + override fun invoke(project: Project, file: PsiFile, startElement: PsiElement, endElement: PsiElement) { + val at = startElement as? PsiAnnotation ?: return + val modifierList = AtResolver.findInjectorAnnotation(at)?.parent as? PsiModifierList ?: return + val atTarget = at.findDeclaredAttributeValue("target") ?: return + + val factory = JavaPsiFacade.getElementFactory(project) + + val definition = factory.createAnnotationFromText( + "@${MixinConstants.MixinExtras.DEFINITION}(id = \"$definitionId\")", + at + ) + definition.setDeclaredAttributeValue("method", atTarget) + + val exprText = buildString { + append("? = ") + if (!isStatic) { + append("?.") + } + append(definitionId) + append("(") + if (argCount > 0) { + append("?") + repeat(argCount - 1) { + append(", ?") + } + } + append(")") + } + val expression = factory.createAnnotationFromText( + "@${MixinConstants.MixinExtras.EXPRESSION}(\"$exprText\")", + at + ) + + val definitionPosRelativeToExpression = + MinecraftProjectSettings.getInstance(project).definitionPosRelativeToExpression + if (definitionPosRelativeToExpression == BeforeOrAfter.BEFORE) { + modifierList.addAfter(expression, null) + modifierList.addAfter(definition, null) + } else { + modifierList.addAfter(definition, null) + modifierList.addAfter(expression, null) + } + + val newAt = factory.createAnnotationFromText( + "@${MixinConstants.Annotations.AT}(value = \"MIXINEXTRAS:EXPRESSION\", shift = ${MixinConstants.Classes.SHIFT}.AFTER)", + at + ) + at.replace(newAt) + + JavaCodeStyleManager.getInstance(project).shortenClassReferences(modifierList) + } + } +} Index: src/main/kotlin/platform/mixin/util/AsmUtil.kt =================================================================== --- src/main/kotlin/platform/mixin/util/AsmUtil.kt (revision 8019549957aaa3d9a98ffd3aed0173166482481e) +++ src/main/kotlin/platform/mixin/util/AsmUtil.kt (revision 3ef92c6f6e75c20372c9ca2890716b3295452a2a) @@ -1100,6 +1100,26 @@ return ClassAndMethodNode(clazz, method) } +// AbstractInsnNode + +val AbstractInsnNode.nextRealInsn: AbstractInsnNode? + get() { + var insn = next + while (insn != null && insn.opcode < 0) { + insn = insn.next + } + return insn + } + +val AbstractInsnNode.previousRealInsn: AbstractInsnNode? + get() { + var insn = previous + while (insn != null && insn.opcode < 0) { + insn = insn.previous + } + return insn + } + // Textifier fun ClassNode.textify(): String { Index: src/main/resources/META-INF/plugin.xml =================================================================== --- src/main/resources/META-INF/plugin.xml (revision 8019549957aaa3d9a98ffd3aed0173166482481e) +++ src/main/resources/META-INF/plugin.xml (revision 3ef92c6f6e75c20372c9ca2890716b3295452a2a) @@ -1293,6 +1293,14 @@ level="WARNING" hasStaticDescription="true" implementationClass="com.demonwav.mcdev.platform.mixin.inspection.mixinextras.InjectLocalCaptureReplaceWithLocalInspection"/> +