User: joe Date: 06 May 25 20:20 Revision: d1e8b460e193d919889bf7ae16bee9bff5778097 Summary: Split off InvokeAssignInjectionPoint, and support fuzz TeamCity URL: https://ci.mcdev.io/viewModification.html?tab=vcsModificationFiles&modId=9973&personal=false Index: src/main/kotlin/platform/mixin/completion/AtArgsCompletionContributor.kt =================================================================== --- src/main/kotlin/platform/mixin/completion/AtArgsCompletionContributor.kt (revision 6206927048f158a5a7bf53d8918e6977f8ddb147) +++ src/main/kotlin/platform/mixin/completion/AtArgsCompletionContributor.kt (revision d1e8b460e193d919889bf7ae16bee9bff5778097) @@ -86,9 +86,13 @@ val key = beforeCursor.substring(0, equalsIndex) val argsValues = injectionPoint.getArgsValues(atAnnotation, key) var prefix = beforeCursor.substring(equalsIndex + 1) - if (injectionPoint.isArgValueList(atAnnotation, key)) { - prefix = prefix.substringAfterLast(',').trimStart() + val argValueListDelimieter = injectionPoint.getArgValueListDelimiter(atAnnotation, key) + if (argValueListDelimieter != null) { + val delimiterEndIndex = argValueListDelimieter.findAll(prefix).lastOrNull()?.range?.last + if (delimiterEndIndex != null) { + prefix = prefix.substring(delimiterEndIndex + 1).trimStart() - } + } + } result.withPrefixMatcher(prefix).addAllElements( argsValues.map { completion -> when (completion) { Index: src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt =================================================================== --- src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt (revision 6206927048f158a5a7bf53d8918e6977f8ddb147) +++ src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt (revision d1e8b460e193d919889bf7ae16bee9bff5778097) @@ -89,7 +89,7 @@ // remove slice selector val isInSlice = at.parentOfType()?.hasQualifiedName(SLICE) ?: false if (isInSlice) { - if (SliceSelector.values().any { atCode.endsWith(":${it.name}") }) { + if (SliceSelector.entries.any { atCode.endsWith(":${it.name}") }) { atCode = atCode.substringBeforeLast(':') } } Index: src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantInjectionPoint.kt =================================================================== --- src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantInjectionPoint.kt (revision 6206927048f158a5a7bf53d8918e6977f8ddb147) +++ src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantInjectionPoint.kt (revision d1e8b460e193d919889bf7ae16bee9bff5778097) @@ -76,6 +76,8 @@ "classValue", "expandZeroConditions" ) + + private val COMMA_LIST_DELIMITER = ",".toRegex() } override fun onCompleted(editor: Editor, reference: PsiLiteral) { @@ -84,7 +86,7 @@ override fun getArgsKeys(at: PsiAnnotation) = ARGS_KEYS - override fun getArgsValues(at: PsiAnnotation, key: String): Array { + override fun getArgsValues(at: PsiAnnotation, key: String): Array { fun collectTargets(constantToCompletion: (Any) -> Any?): Array { val injectorAnnotation = AtResolver.findInjectorAnnotation(at) ?: return ArrayUtilRt.EMPTY_OBJECT_ARRAY val expandConditions = parseExpandConditions(AtResolver.getArgs(at)) @@ -103,7 +105,7 @@ } return when (key) { - "expandZeroConditions" -> ExpandCondition.values().mapToArray { it.name.lowercase(Locale.ROOT) } + "expandZeroConditions" -> ExpandCondition.entries.toTypedArray().mapToArray { it.name.lowercase(Locale.ROOT) } "intValue" -> collectTargets { cst -> cst.takeIf { it is Int } } "floatValue" -> collectTargets { cst -> cst.takeIf { it is Float } } "longValue" -> collectTargets { cst -> cst.takeIf { it is Long } } @@ -124,7 +126,8 @@ } } - override fun isArgValueList(at: PsiAnnotation, key: String) = key == "expandZeroConditions" + override fun getArgValueListDelimiter(at: PsiAnnotation, key: String) = + COMMA_LIST_DELIMITER.takeIf { key == "expandZeroConditions" } fun getConstantInfo(at: PsiAnnotation): ConstantInfo? { val args = AtResolver.getArgs(at) Index: src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantStringMethodInjectionPoint.kt =================================================================== --- src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantStringMethodInjectionPoint.kt (revision 6206927048f158a5a7bf53d8918e6977f8ddb147) +++ src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantStringMethodInjectionPoint.kt (revision d1e8b460e193d919889bf7ae16bee9bff5778097) @@ -93,7 +93,7 @@ override fun getArgsKeys(at: PsiAnnotation) = ARGS_KEYS - override fun getArgsValues(at: PsiAnnotation, key: String): Array { + override fun getArgsValues(at: PsiAnnotation, key: String): Array { if (key != "ldc") { return ArrayUtilRt.EMPTY_OBJECT_ARRAY } Index: src/main/kotlin/platform/mixin/handlers/injectionPoint/CtorHeadInjectionPoint.kt =================================================================== --- src/main/kotlin/platform/mixin/handlers/injectionPoint/CtorHeadInjectionPoint.kt (revision 6206927048f158a5a7bf53d8918e6977f8ddb147) +++ src/main/kotlin/platform/mixin/handlers/injectionPoint/CtorHeadInjectionPoint.kt (revision d1e8b460e193d919889bf7ae16bee9bff5778097) @@ -80,8 +80,8 @@ } override fun getArgsKeys(at: PsiAnnotation) = ARGS_KEYS - override fun getArgsValues(at: PsiAnnotation, key: String): Array = if (key == "enforce") { - EnforceMode.values().mapToArray { it.name } + override fun getArgsValues(at: PsiAnnotation, key: String): Array = if (key == "enforce") { + EnforceMode.entries.mapToArray { it.name } } else { ArrayUtilRt.EMPTY_OBJECT_ARRAY } Index: src/main/kotlin/platform/mixin/handlers/injectionPoint/FieldInjectionPoint.kt =================================================================== --- src/main/kotlin/platform/mixin/handlers/injectionPoint/FieldInjectionPoint.kt (revision 6206927048f158a5a7bf53d8918e6977f8ddb147) +++ src/main/kotlin/platform/mixin/handlers/injectionPoint/FieldInjectionPoint.kt (revision d1e8b460e193d919889bf7ae16bee9bff5778097) @@ -52,7 +52,7 @@ companion object { private val VALID_OPCODES = setOf(Opcodes.GETFIELD, Opcodes.GETSTATIC, Opcodes.PUTFIELD, Opcodes.PUTSTATIC) private val ARGS_KEYS = arrayOf("array") - private val ARRAY_VALUES = arrayOf("length", "get", "set") + private val ARRAY_VALUES = arrayOf("length", "get", "set") } override fun onCompleted(editor: Editor, reference: PsiLiteral) { @@ -66,7 +66,7 @@ override fun getArgsKeys(at: PsiAnnotation) = ARGS_KEYS - override fun getArgsValues(at: PsiAnnotation, key: String): Array = + override fun getArgsValues(at: PsiAnnotation, key: String): Array = ARRAY_VALUES.takeIf { key == "array" } ?: ArrayUtilRt.EMPTY_OBJECT_ARRAY private fun getArrayAccessType(args: Map): ArrayAccessType? { Index: src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.kt =================================================================== --- src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.kt (revision 6206927048f158a5a7bf53d8918e6977f8ddb147) +++ src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.kt (revision d1e8b460e193d919889bf7ae16bee9bff5778097) @@ -98,11 +98,11 @@ return ArrayUtilRt.EMPTY_STRING_ARRAY } - open fun getArgsValues(at: PsiAnnotation, key: String): Array { + open fun getArgsValues(at: PsiAnnotation, key: String): Array { return ArrayUtilRt.EMPTY_OBJECT_ARRAY } - open fun isArgValueList(at: PsiAnnotation, key: String) = false + open fun getArgValueListDelimiter(at: PsiAnnotation, key: String): Regex? = null open val discouragedMessage: String? = null Index: src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeAssignInjectionPoint.kt =================================================================== --- src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeAssignInjectionPoint.kt (revision d1e8b460e193d919889bf7ae16bee9bff5778097) +++ src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeAssignInjectionPoint.kt (revision d1e8b460e193d919889bf7ae16bee9bff5778097) @@ -0,0 +1,197 @@ +/* + * 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.handlers.injectionPoint + +import com.demonwav.mcdev.platform.mixin.reference.MixinSelector +import com.demonwav.mcdev.util.MemberReference +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.psi.CommonClassNames +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiLiteral +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiMethodCallExpression +import com.intellij.util.ArrayUtilRt +import org.objectweb.asm.Opcodes +import org.objectweb.asm.tree.ClassNode +import org.objectweb.asm.tree.MethodInsnNode +import org.objectweb.asm.tree.MethodNode +import org.objectweb.asm.tree.VarInsnNode +import org.objectweb.asm.util.Printer + +class InvokeAssignInjectionPoint : AbstractMethodInjectionPoint() { + companion object { + private val ARGS_KEYS = arrayOf("fuzz", "skip") + private val SKIP_LIST_DELIMITER = "[ ,;]".toRegex() + private val OPCODES_BY_NAME = Printer.OPCODES.withIndex().associate { it.value to it.index } + private val DEFAULT_SKIP = setOf( + // Opcodes which may appear if the targetted method is part of an + // expression eg. int foo = 2 + this.bar(); + Opcodes.DUP, Opcodes.IADD, Opcodes.LADD, Opcodes.FADD, Opcodes.DADD, + Opcodes.ISUB, Opcodes.LSUB, Opcodes.FSUB, Opcodes.DSUB, Opcodes.IMUL, + Opcodes.LMUL, Opcodes.FMUL, Opcodes.DMUL, Opcodes.IDIV, Opcodes.LDIV, + Opcodes.FDIV, Opcodes.DDIV, Opcodes.IREM, Opcodes.LREM, Opcodes.FREM, + Opcodes.DREM, Opcodes.INEG, Opcodes.LNEG, Opcodes.FNEG, Opcodes.DNEG, + Opcodes.ISHL, Opcodes.LSHL, Opcodes.ISHR, Opcodes.LSHR, Opcodes.IUSHR, + Opcodes.LUSHR, Opcodes.IAND, Opcodes.LAND, Opcodes.IOR, Opcodes.LOR, + Opcodes.IXOR, Opcodes.LXOR, Opcodes.IINC, + + // Opcodes which may appear if the targetted method is cast before + // assignment eg. int foo = (int)this.getFloat(); + Opcodes.I2L, Opcodes.I2F, Opcodes.I2D, Opcodes.L2I, Opcodes.L2F, + Opcodes.L2D, Opcodes.F2I, Opcodes.F2L, Opcodes.F2D, Opcodes.D2I, + Opcodes.D2L, Opcodes.D2F, Opcodes.I2B, Opcodes.I2C, Opcodes.I2S, + Opcodes.CHECKCAST, Opcodes.INSTANCEOF + ) + } + + override fun onCompleted(editor: Editor, reference: PsiLiteral) { + completeExtraStringAtAttribute(editor, reference, "target") + } + + override fun getArgsKeys(at: PsiAnnotation) = ARGS_KEYS + + override fun getArgsValues(at: PsiAnnotation, key: String): Array { + if (key == "skip") { + return Printer.OPCODES + } + return ArrayUtilRt.EMPTY_OBJECT_ARRAY + } + + override fun getArgValueListDelimiter(at: PsiAnnotation, key: String) = + SKIP_LIST_DELIMITER.takeIf { key == "skip" } + + override fun isShiftDiscouraged(shift: Int): Boolean { + // Allow shifting before INVOKE_ASSIGN + return shift != 0 && shift != -1 + } + + override fun createNavigationVisitor( + at: PsiAnnotation, + target: MixinSelector?, + targetClass: PsiClass, + ): NavigationVisitor? { + return target?.let { MyNavigationVisitor(targetClass, it) } + } + + override fun doCreateCollectVisitor( + at: PsiAnnotation, + target: MixinSelector?, + targetClass: ClassNode, + mode: CollectVisitor.Mode, + ): CollectVisitor? { + val args = AtResolver.getArgs(at) + val fuzz = args["fuzz"]?.toIntOrNull()?.coerceAtLeast(1) ?: 1 + val skip = args["skip"]?.let { parseSkip(it) } ?: DEFAULT_SKIP + + if (mode == CollectVisitor.Mode.COMPLETION) { + return MyCollectVisitor(mode, at.project, MemberReference(""), fuzz, skip) + } + return target?.let { MyCollectVisitor(mode, at.project, it, fuzz, skip) } + } + + private fun parseSkip(string: String): Set { + return string.split(SKIP_LIST_DELIMITER) + .asSequence() + .mapNotNull { part -> + val trimmedPart = part.trim() + OPCODES_BY_NAME[trimmedPart.removePrefix("Opcodes.")] + ?: trimmedPart.toIntOrNull()?.takeIf { it >= 0 && it < Printer.OPCODES.size } + } + .toSet() + } + + private class MyNavigationVisitor( + private val targetClass: PsiClass, + private val selector: MixinSelector, + ) : NavigationVisitor() { + + private fun visitMethodUsage(method: PsiMethod, qualifier: PsiClass?, expression: PsiElement) { + if (selector.matchMethod(method, qualifier ?: targetClass)) { + addResult(expression) + } + } + + override fun visitMethodCallExpression(expression: PsiMethodCallExpression) { + val method = expression.resolveMethod() + if (method != null) { + val containingClass = method.containingClass + + // Normally, Java uses the type of the instance to qualify the method calls + // However, if the method is part of java.lang.Object (e.g. equals or toString) + // and no class in the hierarchy of the instance overrides the method, Java will + // insert the call using java.lang.Object as the owner + val qualifier = + if (method.isConstructor || containingClass?.qualifiedName == CommonClassNames.JAVA_LANG_OBJECT) { + containingClass + } else { + QualifiedMember.resolveQualifier(expression.methodExpression) + } + + visitMethodUsage(method, qualifier, expression) + } + + super.visitMethodCallExpression(expression) + } + } + + private class MyCollectVisitor( + mode: Mode, + private val project: Project, + private val selector: MixinSelector, + private val fuzz: Int, + private val skip: Set, + ) : CollectVisitor(mode) { + override fun accept(methodNode: MethodNode) { + val insns = methodNode.instructions ?: return + insns.iterator().forEachRemaining { insn -> + if (insn !is MethodInsnNode) { + return@forEachRemaining + } + + val sourceMethod = nodeMatchesSelector(insn, mode, selector, project) ?: return@forEachRemaining + + val offset = insns.indexOf(insn) + val maxOffset = (offset + fuzz + 1).coerceAtMost(insns.size()) + var resultingInsn = insn + for (i in offset + 1 until maxOffset) { + val candidate = insns[i] + if (candidate is VarInsnNode && candidate.opcode >= Opcodes.ISTORE) { + resultingInsn = candidate + break + } else if (skip.isNotEmpty() && candidate.opcode !in skip) { + break + } + } + + resultingInsn = resultingInsn.next ?: resultingInsn + + addResult( + resultingInsn, + sourceMethod, + qualifier = insn.owner.replace('/', '.'), + ) + } + } + } +} Index: src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeInjectionPoint.kt =================================================================== --- src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeInjectionPoint.kt (revision 6206927048f158a5a7bf53d8918e6977f8ddb147) +++ src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeInjectionPoint.kt (revision d1e8b460e193d919889bf7ae16bee9bff5778097) @@ -39,28 +39,15 @@ import org.objectweb.asm.tree.MethodInsnNode import org.objectweb.asm.tree.MethodNode -abstract class AbstractInvokeInjectionPoint(private val assign: Boolean) : AbstractMethodInjectionPoint() { +class InvokeInjectionPoint : AbstractMethodInjectionPoint() { override fun onCompleted(editor: Editor, reference: PsiLiteral) { completeExtraStringAtAttribute(editor, reference, "target") } override fun isShiftDiscouraged(shift: Int): Boolean { - if (shift == 0) { - return false + // Allow shifting after INVOKE + return shift != 0 && shift != 1 - } + } - if (assign) { - // allow shifting before the INVOKE_ASSIGN - if (shift == -1) { - return false - } - } else { - // allow shifting after the INVOKE - if (shift == 1) { - return false - } - } - return true - } override fun createNavigationVisitor( at: PsiAnnotation, @@ -77,9 +64,9 @@ mode: CollectVisitor.Mode, ): CollectVisitor? { if (mode == CollectVisitor.Mode.COMPLETION) { - return MyCollectVisitor(mode, at.project, MemberReference(""), assign) + return MyCollectVisitor(mode, at.project, MemberReference("")) } - return target?.let { MyCollectVisitor(mode, at.project, it, assign) } + return target?.let { MyCollectVisitor(mode, at.project, it) } } private class MyNavigationVisitor( @@ -165,7 +152,6 @@ mode: Mode, private val project: Project, private val selector: MixinSelector, - private val assign: Boolean, ) : CollectVisitor(mode) { override fun accept(methodNode: MethodNode) { val insns = methodNode.instructions ?: return @@ -175,19 +161,12 @@ } val sourceMethod = nodeMatchesSelector(insn, mode, selector, project) ?: return@forEachRemaining - val actualInsn = if (assign) insn.next else insn - if (actualInsn != null) { - addResult( + addResult( - actualInsn, + insn, - sourceMethod, - qualifier = insn.owner.replace('/', '.'), - ) - } - } - } - } + sourceMethod, + qualifier = insn.owner.replace('/', '.'), + ) + } + } + } +} -} - -class InvokeInjectionPoint : AbstractInvokeInjectionPoint(false) - -class InvokeAssignInjectionPoint : AbstractInvokeInjectionPoint(true) Index: src/main/kotlin/platform/mixin/handlers/injectionPoint/NewInsnInjectionPoint.kt =================================================================== --- src/main/kotlin/platform/mixin/handlers/injectionPoint/NewInsnInjectionPoint.kt (revision 6206927048f158a5a7bf53d8918e6977f8ddb147) +++ src/main/kotlin/platform/mixin/handlers/injectionPoint/NewInsnInjectionPoint.kt (revision d1e8b460e193d919889bf7ae16bee9bff5778097) @@ -64,7 +64,7 @@ override fun getArgsKeys(at: PsiAnnotation) = ARGS_KEYS - override fun getArgsValues(at: PsiAnnotation, key: String): Array { + override fun getArgsValues(at: PsiAnnotation, key: String): Array { if (key != "class") { return ArrayUtilRt.EMPTY_OBJECT_ARRAY } Index: src/main/kotlin/util/patterns.kt =================================================================== --- src/main/kotlin/util/patterns.kt (revision 6206927048f158a5a7bf53d8918e6977f8ddb147) +++ src/main/kotlin/util/patterns.kt (revision d1e8b460e193d919889bf7ae16bee9bff5778097) @@ -24,11 +24,15 @@ import com.intellij.patterns.PsiAnnotationPattern import com.intellij.patterns.PsiJavaElementPattern import com.intellij.patterns.PsiJavaPatterns +import com.intellij.patterns.StandardPatterns +import com.intellij.psi.PsiAnnotationMemberValue import com.intellij.psi.PsiAnnotationParameterList import com.intellij.psi.PsiElement private val ANNOTATION_ATTRIBUTE_STOP = - PlatformPatterns.not(PsiJavaPatterns.psiExpression()).andNot(PsiJavaPatterns.psiNameValuePair()) + PlatformPatterns.not(PsiJavaPatterns.psiExpression()) + .andNot(StandardPatterns.instanceOf(PsiAnnotationMemberValue::class.java)) + .andNot(PsiJavaPatterns.psiNameValuePair()) // PsiJavaElementPattern.insideAnnotationParam checks for the parameter list only up to 3 levels // It can be more if the value is for example enclosed in parentheses