User: joe Date: 14 Nov 26 05:52 Revision: c46cd568bbfbea09e8875244eea1f8b274494eff Summary: Add UNSURE option to MissingLVTChecker, and don't warn about not using `Local.name` when unsure. Also add inspections about using `Local.name` when there is definitely no LVT. Fixes #2646 TeamCity URL: http://ci.mcdev.io:80/viewModification.html?tab=vcsModificationFiles&modId=10777&personal=false Index: src/main/kotlin/platform/mixin/inspection/injector/ModifyVariableNameWithoutLVTInspection.kt =================================================================== --- src/main/kotlin/platform/mixin/inspection/injector/ModifyVariableNameWithoutLVTInspection.kt (revision c46cd568bbfbea09e8875244eea1f8b274494eff) +++ src/main/kotlin/platform/mixin/inspection/injector/ModifyVariableNameWithoutLVTInspection.kt (revision c46cd568bbfbea09e8875244eea1f8b274494eff) @@ -0,0 +1,76 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2026 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.LocalInfo +import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Annotations.MODIFY_VARIABLE +import com.demonwav.mcdev.platform.mixin.util.hasMissingLVT +import com.demonwav.mcdev.platform.mixin.util.mixinTargets +import com.demonwav.mcdev.util.findAnnotation +import com.demonwav.mcdev.util.findContainingClass +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.psi.JavaElementVisitor +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiMethod +import com.intellij.util.ThreeState + +class ModifyVariableNameWithoutLVTInspection : MixinInspection() { + 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 localInfo = LocalInfo.fromAnnotation(localType, modifyVariable) + + val problemElement = Util.getProblemElement(localInfo, modifyVariable, modifyVariable) ?: return + holder.registerProblem( + problemElement, + "@ModifyVariable uses a variable name, but the target class has no local variable names at runtime", + ) + } + } + + object Util { + private val GENERATED_NAME_REGEX = Regex("(?:arg|var)\\d+") + + fun getProblemElement( + localInfo: LocalInfo, + localAnnotation: PsiAnnotation, + injectorAnnotation: PsiAnnotation, + ): PsiElement? { + // names generated by Mixin don't depend on the local variable table + if (localInfo.names.all { GENERATED_NAME_REGEX.matches(it) }) { + return null + } + + val mixinTargets = injectorAnnotation.findContainingClass()?.mixinTargets ?: return null + val isMissingLVT = mixinTargets.any { + injectorAnnotation.hasMissingLVT(it.name.replace('/', '.')) == ThreeState.YES + } + if (!isMissingLVT) { + return null + } + + return localAnnotation.findDeclaredAttributeValue("name") ?: localAnnotation.nameReferenceElement + } + } +} Index: src/main/kotlin/platform/mixin/inspection/mixinextras/LocalNameWithoutLVTInspection.kt =================================================================== --- src/main/kotlin/platform/mixin/inspection/mixinextras/LocalNameWithoutLVTInspection.kt (revision c46cd568bbfbea09e8875244eea1f8b274494eff) +++ src/main/kotlin/platform/mixin/inspection/mixinextras/LocalNameWithoutLVTInspection.kt (revision c46cd568bbfbea09e8875244eea1f8b274494eff) @@ -0,0 +1,65 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2026 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.platform.mixin.handlers.InsnInjectorAnnotationHandler +import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler +import com.demonwav.mcdev.platform.mixin.inspection.MixinInspection +import com.demonwav.mcdev.platform.mixin.inspection.injector.ModifyVariableNameWithoutLVTInspection +import com.demonwav.mcdev.platform.mixin.util.LocalInfo +import com.demonwav.mcdev.platform.mixin.util.MixinConstants +import com.demonwav.mcdev.platform.mixin.util.unwrapLocalRef +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.psi.JavaElementVisitor +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiParameter +import com.intellij.psi.util.parentOfType + +class LocalNameWithoutLVTInspection : MixinInspection() { + override fun buildVisitor(holder: ProblemsHolder) = object : JavaElementVisitor() { + override fun visitAnnotation(localAnnotation: PsiAnnotation) { + if (!localAnnotation.hasQualifiedName(MixinConstants.MixinExtras.LOCAL)) { + return + } + + val parameter = localAnnotation.parentOfType() ?: return + val method = parameter.declarationScope as? PsiMethod ?: return + + val localType = parameter.type.unwrapLocalRef() + val localInfo = LocalInfo.fromAnnotation(localType, localAnnotation) + + val injectorAnnotation = method.annotations.firstOrNull { annotation -> + MixinAnnotationHandler.forMixinAnnotation(annotation, holder.project) is InsnInjectorAnnotationHandler + } ?: return + + val problemElement = ModifyVariableNameWithoutLVTInspection.Util.getProblemElement( + localInfo, + localAnnotation, + injectorAnnotation, + ) ?: return + holder.registerProblem( + problemElement, + "@Local uses a variable name, but the target class has no local variable names at runtime", + ) + } + } +} Index: src/main/kotlin/platform/mixin/util/MinecraftMissingLVTChecker.kt =================================================================== --- src/main/kotlin/platform/mixin/util/MinecraftMissingLVTChecker.kt (revision 8223d0a3669b4fd9f6d4dc5ac2beae977a35a67b) +++ src/main/kotlin/platform/mixin/util/MinecraftMissingLVTChecker.kt (revision c46cd568bbfbea09e8875244eea1f8b274494eff) @@ -3,7 +3,7 @@ * * https://mcdev.io/ * - * Copyright (C) 2025 minecraft-dev + * Copyright (C) 2026 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 @@ -24,15 +24,16 @@ import com.demonwav.mcdev.util.mcVersion import com.intellij.openapi.util.registry.Registry import com.intellij.psi.PsiElement +import com.intellij.util.ThreeState class MinecraftMissingLVTChecker : MissingLVTChecker { - override fun hasMissingLVT(context: PsiElement, className: String): Boolean { + override fun hasMissingLVT(context: PsiElement, className: String): ThreeState { return when { - Registry.`is`("mcdev.unobfuscated.minecraft") -> false - !className.startsWith("net.minecraft.") && !className.startsWith("com.mojang.blaze3d.") -> false + Registry.`is`("mcdev.unobfuscated.minecraft") -> ThreeState.NO + !className.startsWith("net.minecraft.") && !className.startsWith("com.mojang.blaze3d.") -> ThreeState.NO else -> { - val mcVersion = context.mcVersion - mcVersion != null && mcVersion <= MinecraftVersions.MC1_21_11 + val mcVersion = context.mcVersion ?: return ThreeState.UNSURE + ThreeState.fromBoolean(mcVersion <= MinecraftVersions.MC1_21_11) } } } Index: src/main/kotlin/platform/mixin/util/MissingLVTChecker.kt =================================================================== --- src/main/kotlin/platform/mixin/util/MissingLVTChecker.kt (revision 8223d0a3669b4fd9f6d4dc5ac2beae977a35a67b) +++ src/main/kotlin/platform/mixin/util/MissingLVTChecker.kt (revision c46cd568bbfbea09e8875244eea1f8b274494eff) @@ -3,7 +3,7 @@ * * https://mcdev.io/ * - * Copyright (C) 2025 minecraft-dev + * Copyright (C) 2026 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 @@ -22,11 +22,19 @@ import com.intellij.openapi.extensions.ExtensionPointName import com.intellij.psi.PsiElement +import com.intellij.util.ThreeState interface MissingLVTChecker { companion object { val EP_NAME = ExtensionPointName.create("com.demonwav.minecraft-dev.missingLVTChecker") } - fun hasMissingLVT(context: PsiElement, className: String): Boolean + /** + * Returns [ThreeState.YES] if the given class definitely has no local variable table at runtime, + * [ThreeState.NO] if it definitely does, or [ThreeState.UNSURE] if this cannot be determined. + * + * Implementations should return [ThreeState.NO] if they do not handle the given class name. [ThreeState.UNSURE] is + * for if they are designed to handle the given class name, but were unable to determine whether it's missing an LVT. + */ + fun hasMissingLVT(context: PsiElement, className: String): ThreeState } Index: src/main/kotlin/platform/mixin/util/Mixin.kt =================================================================== --- src/main/kotlin/platform/mixin/util/Mixin.kt (revision 8223d0a3669b4fd9f6d4dc5ac2beae977a35a67b) +++ src/main/kotlin/platform/mixin/util/Mixin.kt (revision c46cd568bbfbea09e8875244eea1f8b274494eff) @@ -57,6 +57,7 @@ import com.intellij.psi.util.PsiModificationTracker import com.intellij.psi.util.PsiTypesUtil import com.intellij.psi.util.TypeConversionUtil +import com.intellij.util.ThreeState import org.objectweb.asm.Opcodes import org.objectweb.asm.tree.ClassNode @@ -313,16 +314,29 @@ return SemanticVersion.tryParse(version) } -fun PsiElement.hasNamedLocalVariables(className: String): Boolean { +/** + * Returns [ThreeState.YES] if any [MissingLVTChecker] knows that the given class has no local variable table at runtime, + * otherwise [ThreeState.UNSURE] if any checker is unsure, otherwise [ThreeState.NO]. Note that this is unusual in that + * [ThreeState.UNSURE] overrides [ThreeState.NO], as [ThreeState.UNSURE] is only supposed to be returned by + * [MissingLVTChecker]s that own the class name. + */ +fun PsiElement.hasMissingLVT(className: String): ThreeState { + var result = ThreeState.NO for (checker in MissingLVTChecker.EP_NAME.extensionList) { - if (checker.hasMissingLVT(this, className)) { - return false + when (checker.hasMissingLVT(this, className)) { + ThreeState.YES -> return ThreeState.YES + ThreeState.UNSURE -> result = ThreeState.UNSURE + ThreeState.NO -> {} } } - return true + return result } +fun PsiElement.hasNamedLocalVariables(className: String): Boolean { + return hasMissingLVT(className) == ThreeState.NO +} + fun PsiModifierListOwner.shouldDoMixinAccessChecks(): Boolean { if (this is PsiMethod && isConstructor) { return false Index: src/main/resources/META-INF/plugin.xml =================================================================== --- src/main/resources/META-INF/plugin.xml (revision 8223d0a3669b4fd9f6d4dc5ac2beae977a35a67b) +++ src/main/resources/META-INF/plugin.xml (revision c46cd568bbfbea09e8875244eea1f8b274494eff) @@ -1187,6 +1187,13 @@ enabledByDefault="true" level="WARNING" implementationClass="com.demonwav.mcdev.platform.mixin.inspection.injector.ModifyVariableMayUseNameInspection"/> + + . +--> + + + +

+ Reports @Local using a variable name where the target class does not have local variable names at runtime +

+ +

+ Local variable names are read from the local variable table of the target class at runtime. Obfuscated versions of + Minecraft (1.21.11 and below) don't have a local variable table at runtime, even if one is present in the development + environment, so the injector will fail to find the variable. Use an ordinal or index instead. +

+ + Index: src/main/resources/inspectionDescriptions/ModifyVariableNameWithoutLVT.html =================================================================== --- src/main/resources/inspectionDescriptions/ModifyVariableNameWithoutLVT.html (revision c46cd568bbfbea09e8875244eea1f8b274494eff) +++ src/main/resources/inspectionDescriptions/ModifyVariableNameWithoutLVT.html (revision c46cd568bbfbea09e8875244eea1f8b274494eff) @@ -0,0 +1,33 @@ + + + + +

+ Reports @ModifyVariable using a variable name where the target class does not have local variable names at runtime +

+ +

+ Local variable names are read from the local variable table of the target class at runtime. Obfuscated versions of + Minecraft (1.21.11 and below) don't have a local variable table at runtime, even if one is present in the development + environment, so the injector will fail to find the variable. Use an ordinal or index instead. +

+ + Index: src/main/resources/messages/MinecraftDevelopmentBundle.properties =================================================================== --- src/main/resources/messages/MinecraftDevelopmentBundle.properties (revision 8223d0a3669b4fd9f6d4dc5ac2beae977a35a67b) +++ src/main/resources/messages/MinecraftDevelopmentBundle.properties (revision c46cd568bbfbea09e8875244eea1f8b274494eff) @@ -274,6 +274,7 @@ inspection.local_capture_fail_exception.name=Usage of LocalCapture.CAPTURE_FAILEXCEPTION inspection.local_may_be_args_only.name=@Local may be argsOnly = true inspection.local_may_use_name.name=@Local may use name +inspection.local_name_without_lvt.name=@Local uses name without local variable table inspection.local_variable_declaration_side_only.name=Invalid usage of local variable declaration annotated with @SideOnly inspection.method_call_side_only.name=Invalid usage of method annotated with @SideOnly inspection.method_side_only.name=Invalid usage of @SideOnly in method declaration @@ -291,6 +292,7 @@ inspection.mixin_wrong_class_type.name=Mixin is wrong class type inspection.modify_variable_may_be_args_only.name=@ModifyVariable may be argsOnly = true inspection.modify_variable_may_use_name.name=@ModifyVariable may use variable name +inspection.modify_variable_name_without_lvt.name=@ModifyVariable uses name without local variable table inspection.mods_toml_validation.name=Forge's mods.toml validation inspection.name_doesnt_match_target_class.name=Mixin parameter names don't match the target class inspection.nested_class_side_only.name=Invalid usage of @SideOnly in nested class declaration