User: joe Date: 06 Nov 25 11:26 Revision: 4a481131a25e50573ba9e876841ce2310c8a3ed5 Summary: Add inspection for @ModifyVariable being able to use names instead of ordinals and indexes TeamCity URL: http://ci.mcdev.io:80/viewModification.html?tab=vcsModificationFiles&modId=10229&personal=false Index: src/main/kotlin/platform/mixin/inspection/injector/ModifyVariableMayUseNameInspection.kt =================================================================== --- src/main/kotlin/platform/mixin/inspection/injector/ModifyVariableMayUseNameInspection.kt (revision 4a481131a25e50573ba9e876841ce2310c8a3ed5) +++ src/main/kotlin/platform/mixin/inspection/injector/ModifyVariableMayUseNameInspection.kt (revision 4a481131a25e50573ba9e876841ce2310c8a3ed5) @@ -0,0 +1,118 @@ +/* + * 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.InjectorAnnotationHandler +import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler +import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.CollectVisitor +import com.demonwav.mcdev.platform.mixin.inspection.MixinInspection +import com.demonwav.mcdev.platform.mixin.inspection.fix.AnnotationAttributeFix +import com.demonwav.mcdev.platform.mixin.util.LocalInfo +import com.demonwav.mcdev.platform.mixin.util.MethodTargetMember +import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Annotations.MODIFY_VARIABLE +import com.demonwav.mcdev.platform.mixin.util.hasNamedLocalVariables +import com.demonwav.mcdev.platform.mixin.util.mixinTargets +import com.demonwav.mcdev.util.findAnnotation +import com.demonwav.mcdev.util.findContainingClass +import com.demonwav.mcdev.util.findModule +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.psi.JavaElementVisitor +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiMethod + +class ModifyVariableMayUseNameInspection : MixinInspection() { + override fun getStaticDescription() = "Reports @ModifyVariable injectors relying on index or ordinal that may use a name instead" + + override fun buildVisitor(holder: ProblemsHolder) = object : JavaElementVisitor() { + override fun visitMethod(method: PsiMethod) { + val modifyVariable = method.findAnnotation(MODIFY_VARIABLE) ?: return + val localType = method.parameterList.getParameter(0)?.type ?: return + val problemElement = modifyVariable.nameReferenceElement ?: return + + val injector = + MixinAnnotationHandler.forMixinAnnotation(MODIFY_VARIABLE) as? InjectorAnnotationHandler ?: return + val localInfo = LocalInfo.fromAnnotation(localType, modifyVariable) + + val variableName = getVariableNameToIntroduce(localInfo, injector, modifyVariable) ?: return + holder.registerProblem( + problemElement, + "@ModifyVariable can use variable name", + ReplaceWithNameFix(modifyVariable, variableName), + ) + } + } + + class ReplaceWithNameFix( + annotation: PsiAnnotation, + private val variableName: String, + ) : AnnotationAttributeFix(annotation, "name" to variableName, "ordinal" to null, "index" to null) { + override fun getText() = "Use variable name '$variableName'" + override fun getFamilyName() = "Use variable name '$variableName'" + } + + companion object { + fun getVariableNameToIntroduce( + localInfo: LocalInfo, + injector: InjectorAnnotationHandler, + injectorAnnotation: PsiAnnotation, + ): String? { + if (localInfo.index == null && localInfo.ordinal == null && localInfo.names.isNotEmpty()) { + // already a named local + return null + } + + val module = injectorAnnotation.findModule() ?: return null + val mixinTargets = injectorAnnotation.findContainingClass()?.mixinTargets ?: return null + + var variableName: String? = null + for (targetClass in mixinTargets) { + if (!module.hasNamedLocalVariables(targetClass.name.replace('/', '.'))) { + return null + } + + for (target in injector.resolveTarget(injectorAnnotation, targetClass)) { + val (clazz, method) = (target as? MethodTargetMember)?.classAndMethod ?: continue + for (insn in injector.resolveInstructions(injectorAnnotation, clazz, method)) { + val matchedLocals = localInfo.matchLocals( + module, + clazz, + method, + insn.insn, + CollectVisitor.Mode.RESOLUTION + ) ?: return null + + for (local in matchedLocals) { + if (!local.isNamed) { + return null + } + if (variableName != null && local.name != variableName) { + return null + } + variableName = local.name + } + } + } + } + + return variableName + } + } +} Index: src/main/kotlin/platform/mixin/util/LocalVariables.kt =================================================================== --- src/main/kotlin/platform/mixin/util/LocalVariables.kt (revision e10f75298ac8e65d7ce4aa2224dfff4519a34fb1) +++ src/main/kotlin/platform/mixin/util/LocalVariables.kt (revision 4a481131a25e50573ba9e876841ce2310c8a3ed5) @@ -805,7 +805,7 @@ localType.descriptor } } - localVars[j] = LocalVariable("var$j", desc, null, i, null, j) + localVars[j] = LocalVariable("var$j", desc, null, i, null, j, isNamed = false) if (desc != null) { lastKnownType[j] = desc } @@ -881,6 +881,7 @@ val start: Int?, var end: Int?, val index: Int, + val isNamed: Boolean = true, ) { fun isInRange(index: Int): Boolean { val end = this.end @@ -904,6 +905,7 @@ ancestor.start, ancestor.end, ancestor.index, + ancestor.isNamed, ) { var lifetime = 0 var frames = 0 Index: src/main/kotlin/platform/mixin/util/Mixin.kt =================================================================== --- src/main/kotlin/platform/mixin/util/Mixin.kt (revision e10f75298ac8e65d7ce4aa2224dfff4519a34fb1) +++ src/main/kotlin/platform/mixin/util/Mixin.kt (revision 4a481131a25e50573ba9e876841ce2310c8a3ed5) @@ -35,6 +35,7 @@ import com.demonwav.mcdev.util.resolveClassArray import com.intellij.openapi.module.Module import com.intellij.openapi.project.Project +import com.intellij.openapi.util.registry.Registry import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiArrayType @@ -299,3 +300,12 @@ val version = (versionField.initializer as? PsiLiteralExpression)?.value as? String ?: return null return SemanticVersion.tryParse(version) } + +fun Module.hasNamedLocalVariables(className: String): Boolean { + if (className.startsWith("net.minecraft.") || className.startsWith("com.mojang.blaze3d.")) { + // TODO: we'll be able to handle this better when we know which Minecraft version will be unobfuscated + return Registry.`is`("mcdev.unobfuscated.minecraft") + } else { + return true + } +} Index: src/main/resources/META-INF/plugin.xml =================================================================== --- src/main/resources/META-INF/plugin.xml (revision e10f75298ac8e65d7ce4aa2224dfff4519a34fb1) +++ src/main/resources/META-INF/plugin.xml (revision 4a481131a25e50573ba9e876841ce2310c8a3ed5) @@ -1230,6 +1230,14 @@ level="WARNING" hasStaticDescription="true" implementationClass="com.demonwav.mcdev.platform.mixin.inspection.injector.ModifyVariableArgsOnlyInspection"/> + +