Skip to content

Commit d1e8b46

Browse files
committed
Split off InvokeAssignInjectionPoint, and support fuzz
1 parent 6206927 commit d1e8b46

File tree

11 files changed

+233
-46
lines changed

11 files changed

+233
-46
lines changed

src/main/kotlin/platform/mixin/completion/AtArgsCompletionContributor.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,12 @@ class AtArgsCompletionContributor : CompletionContributor() {
8686
val key = beforeCursor.substring(0, equalsIndex)
8787
val argsValues = injectionPoint.getArgsValues(atAnnotation, key)
8888
var prefix = beforeCursor.substring(equalsIndex + 1)
89-
if (injectionPoint.isArgValueList(atAnnotation, key)) {
90-
prefix = prefix.substringAfterLast(',').trimStart()
89+
val argValueListDelimieter = injectionPoint.getArgValueListDelimiter(atAnnotation, key)
90+
if (argValueListDelimieter != null) {
91+
val delimiterEndIndex = argValueListDelimieter.findAll(prefix).lastOrNull()?.range?.last
92+
if (delimiterEndIndex != null) {
93+
prefix = prefix.substring(delimiterEndIndex + 1).trimStart()
94+
}
9195
}
9296
result.withPrefixMatcher(prefix).addAllElements(
9397
argsValues.map { completion ->

src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ class AtResolver(
8989
// remove slice selector
9090
val isInSlice = at.parentOfType<PsiAnnotation>()?.hasQualifiedName(SLICE) ?: false
9191
if (isInSlice) {
92-
if (SliceSelector.values().any { atCode.endsWith(":${it.name}") }) {
92+
if (SliceSelector.entries.any { atCode.endsWith(":${it.name}") }) {
9393
atCode = atCode.substringBeforeLast(':')
9494
}
9595
}

src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantInjectionPoint.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ class ConstantInjectionPoint : InjectionPoint<PsiElement>() {
7676
"classValue",
7777
"expandZeroConditions"
7878
)
79+
80+
private val COMMA_LIST_DELIMITER = ",".toRegex()
7981
}
8082

8183
override fun onCompleted(editor: Editor, reference: PsiLiteral) {
@@ -84,7 +86,7 @@ class ConstantInjectionPoint : InjectionPoint<PsiElement>() {
8486

8587
override fun getArgsKeys(at: PsiAnnotation) = ARGS_KEYS
8688

87-
override fun getArgsValues(at: PsiAnnotation, key: String): Array<Any> {
89+
override fun getArgsValues(at: PsiAnnotation, key: String): Array<out Any> {
8890
fun collectTargets(constantToCompletion: (Any) -> Any?): Array<Any> {
8991
val injectorAnnotation = AtResolver.findInjectorAnnotation(at) ?: return ArrayUtilRt.EMPTY_OBJECT_ARRAY
9092
val expandConditions = parseExpandConditions(AtResolver.getArgs(at))
@@ -103,7 +105,7 @@ class ConstantInjectionPoint : InjectionPoint<PsiElement>() {
103105
}
104106

105107
return when (key) {
106-
"expandZeroConditions" -> ExpandCondition.values().mapToArray { it.name.lowercase(Locale.ROOT) }
108+
"expandZeroConditions" -> ExpandCondition.entries.toTypedArray().mapToArray { it.name.lowercase(Locale.ROOT) }
107109
"intValue" -> collectTargets { cst -> cst.takeIf { it is Int } }
108110
"floatValue" -> collectTargets { cst -> cst.takeIf { it is Float } }
109111
"longValue" -> collectTargets { cst -> cst.takeIf { it is Long } }
@@ -124,7 +126,8 @@ class ConstantInjectionPoint : InjectionPoint<PsiElement>() {
124126
}
125127
}
126128

127-
override fun isArgValueList(at: PsiAnnotation, key: String) = key == "expandZeroConditions"
129+
override fun getArgValueListDelimiter(at: PsiAnnotation, key: String) =
130+
COMMA_LIST_DELIMITER.takeIf { key == "expandZeroConditions" }
128131

129132
fun getConstantInfo(at: PsiAnnotation): ConstantInfo? {
130133
val args = AtResolver.getArgs(at)

src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantStringMethodInjectionPoint.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ class ConstantStringMethodInjectionPoint : AbstractMethodInjectionPoint() {
9393

9494
override fun getArgsKeys(at: PsiAnnotation) = ARGS_KEYS
9595

96-
override fun getArgsValues(at: PsiAnnotation, key: String): Array<Any> {
96+
override fun getArgsValues(at: PsiAnnotation, key: String): Array<out Any> {
9797
if (key != "ldc") {
9898
return ArrayUtilRt.EMPTY_OBJECT_ARRAY
9999
}

src/main/kotlin/platform/mixin/handlers/injectionPoint/CtorHeadInjectionPoint.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ class CtorHeadInjectionPoint : InjectionPoint<PsiElement>() {
8080
}
8181

8282
override fun getArgsKeys(at: PsiAnnotation) = ARGS_KEYS
83-
override fun getArgsValues(at: PsiAnnotation, key: String): Array<Any> = if (key == "enforce") {
84-
EnforceMode.values().mapToArray { it.name }
83+
override fun getArgsValues(at: PsiAnnotation, key: String): Array<out Any> = if (key == "enforce") {
84+
EnforceMode.entries.mapToArray { it.name }
8585
} else {
8686
ArrayUtilRt.EMPTY_OBJECT_ARRAY
8787
}

src/main/kotlin/platform/mixin/handlers/injectionPoint/FieldInjectionPoint.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ class FieldInjectionPoint : QualifiedInjectionPoint<PsiField>() {
5252
companion object {
5353
private val VALID_OPCODES = setOf(Opcodes.GETFIELD, Opcodes.GETSTATIC, Opcodes.PUTFIELD, Opcodes.PUTSTATIC)
5454
private val ARGS_KEYS = arrayOf("array")
55-
private val ARRAY_VALUES = arrayOf<Any>("length", "get", "set")
55+
private val ARRAY_VALUES = arrayOf("length", "get", "set")
5656
}
5757

5858
override fun onCompleted(editor: Editor, reference: PsiLiteral) {
@@ -66,7 +66,7 @@ class FieldInjectionPoint : QualifiedInjectionPoint<PsiField>() {
6666

6767
override fun getArgsKeys(at: PsiAnnotation) = ARGS_KEYS
6868

69-
override fun getArgsValues(at: PsiAnnotation, key: String): Array<Any> =
69+
override fun getArgsValues(at: PsiAnnotation, key: String): Array<out Any> =
7070
ARRAY_VALUES.takeIf { key == "array" } ?: ArrayUtilRt.EMPTY_OBJECT_ARRAY
7171

7272
private fun getArrayAccessType(args: Map<String, String>): ArrayAccessType? {

src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,11 @@ abstract class InjectionPoint<T : PsiElement> {
9898
return ArrayUtilRt.EMPTY_STRING_ARRAY
9999
}
100100

101-
open fun getArgsValues(at: PsiAnnotation, key: String): Array<Any> {
101+
open fun getArgsValues(at: PsiAnnotation, key: String): Array<out Any> {
102102
return ArrayUtilRt.EMPTY_OBJECT_ARRAY
103103
}
104104

105-
open fun isArgValueList(at: PsiAnnotation, key: String) = false
105+
open fun getArgValueListDelimiter(at: PsiAnnotation, key: String): Regex? = null
106106

107107
open val discouragedMessage: String? = null
108108

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
/*
2+
* Minecraft Development for IntelliJ
3+
*
4+
* https://mcdev.io/
5+
*
6+
* Copyright (C) 2025 minecraft-dev
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the GNU Lesser General Public License as published
10+
* by the Free Software Foundation, version 3.0 only.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU Lesser General Public License
18+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
19+
*/
20+
21+
package com.demonwav.mcdev.platform.mixin.handlers.injectionPoint
22+
23+
import com.demonwav.mcdev.platform.mixin.reference.MixinSelector
24+
import com.demonwav.mcdev.util.MemberReference
25+
import com.intellij.openapi.editor.Editor
26+
import com.intellij.openapi.project.Project
27+
import com.intellij.psi.CommonClassNames
28+
import com.intellij.psi.PsiAnnotation
29+
import com.intellij.psi.PsiClass
30+
import com.intellij.psi.PsiElement
31+
import com.intellij.psi.PsiLiteral
32+
import com.intellij.psi.PsiMethod
33+
import com.intellij.psi.PsiMethodCallExpression
34+
import com.intellij.util.ArrayUtilRt
35+
import org.objectweb.asm.Opcodes
36+
import org.objectweb.asm.tree.ClassNode
37+
import org.objectweb.asm.tree.MethodInsnNode
38+
import org.objectweb.asm.tree.MethodNode
39+
import org.objectweb.asm.tree.VarInsnNode
40+
import org.objectweb.asm.util.Printer
41+
42+
class InvokeAssignInjectionPoint : AbstractMethodInjectionPoint() {
43+
companion object {
44+
private val ARGS_KEYS = arrayOf("fuzz", "skip")
45+
private val SKIP_LIST_DELIMITER = "[ ,;]".toRegex()
46+
private val OPCODES_BY_NAME = Printer.OPCODES.withIndex().associate { it.value to it.index }
47+
private val DEFAULT_SKIP = setOf(
48+
// Opcodes which may appear if the targetted method is part of an
49+
// expression eg. int foo = 2 + this.bar();
50+
Opcodes.DUP, Opcodes.IADD, Opcodes.LADD, Opcodes.FADD, Opcodes.DADD,
51+
Opcodes.ISUB, Opcodes.LSUB, Opcodes.FSUB, Opcodes.DSUB, Opcodes.IMUL,
52+
Opcodes.LMUL, Opcodes.FMUL, Opcodes.DMUL, Opcodes.IDIV, Opcodes.LDIV,
53+
Opcodes.FDIV, Opcodes.DDIV, Opcodes.IREM, Opcodes.LREM, Opcodes.FREM,
54+
Opcodes.DREM, Opcodes.INEG, Opcodes.LNEG, Opcodes.FNEG, Opcodes.DNEG,
55+
Opcodes.ISHL, Opcodes.LSHL, Opcodes.ISHR, Opcodes.LSHR, Opcodes.IUSHR,
56+
Opcodes.LUSHR, Opcodes.IAND, Opcodes.LAND, Opcodes.IOR, Opcodes.LOR,
57+
Opcodes.IXOR, Opcodes.LXOR, Opcodes.IINC,
58+
59+
// Opcodes which may appear if the targetted method is cast before
60+
// assignment eg. int foo = (int)this.getFloat();
61+
Opcodes.I2L, Opcodes.I2F, Opcodes.I2D, Opcodes.L2I, Opcodes.L2F,
62+
Opcodes.L2D, Opcodes.F2I, Opcodes.F2L, Opcodes.F2D, Opcodes.D2I,
63+
Opcodes.D2L, Opcodes.D2F, Opcodes.I2B, Opcodes.I2C, Opcodes.I2S,
64+
Opcodes.CHECKCAST, Opcodes.INSTANCEOF
65+
)
66+
}
67+
68+
override fun onCompleted(editor: Editor, reference: PsiLiteral) {
69+
completeExtraStringAtAttribute(editor, reference, "target")
70+
}
71+
72+
override fun getArgsKeys(at: PsiAnnotation) = ARGS_KEYS
73+
74+
override fun getArgsValues(at: PsiAnnotation, key: String): Array<out Any> {
75+
if (key == "skip") {
76+
return Printer.OPCODES
77+
}
78+
return ArrayUtilRt.EMPTY_OBJECT_ARRAY
79+
}
80+
81+
override fun getArgValueListDelimiter(at: PsiAnnotation, key: String) =
82+
SKIP_LIST_DELIMITER.takeIf { key == "skip" }
83+
84+
override fun isShiftDiscouraged(shift: Int): Boolean {
85+
// Allow shifting before INVOKE_ASSIGN
86+
return shift != 0 && shift != -1
87+
}
88+
89+
override fun createNavigationVisitor(
90+
at: PsiAnnotation,
91+
target: MixinSelector?,
92+
targetClass: PsiClass,
93+
): NavigationVisitor? {
94+
return target?.let { MyNavigationVisitor(targetClass, it) }
95+
}
96+
97+
override fun doCreateCollectVisitor(
98+
at: PsiAnnotation,
99+
target: MixinSelector?,
100+
targetClass: ClassNode,
101+
mode: CollectVisitor.Mode,
102+
): CollectVisitor<PsiMethod>? {
103+
val args = AtResolver.getArgs(at)
104+
val fuzz = args["fuzz"]?.toIntOrNull()?.coerceAtLeast(1) ?: 1
105+
val skip = args["skip"]?.let { parseSkip(it) } ?: DEFAULT_SKIP
106+
107+
if (mode == CollectVisitor.Mode.COMPLETION) {
108+
return MyCollectVisitor(mode, at.project, MemberReference(""), fuzz, skip)
109+
}
110+
return target?.let { MyCollectVisitor(mode, at.project, it, fuzz, skip) }
111+
}
112+
113+
private fun parseSkip(string: String): Set<Int> {
114+
return string.split(SKIP_LIST_DELIMITER)
115+
.asSequence()
116+
.mapNotNull { part ->
117+
val trimmedPart = part.trim()
118+
OPCODES_BY_NAME[trimmedPart.removePrefix("Opcodes.")]
119+
?: trimmedPart.toIntOrNull()?.takeIf { it >= 0 && it < Printer.OPCODES.size }
120+
}
121+
.toSet()
122+
}
123+
124+
private class MyNavigationVisitor(
125+
private val targetClass: PsiClass,
126+
private val selector: MixinSelector,
127+
) : NavigationVisitor() {
128+
129+
private fun visitMethodUsage(method: PsiMethod, qualifier: PsiClass?, expression: PsiElement) {
130+
if (selector.matchMethod(method, qualifier ?: targetClass)) {
131+
addResult(expression)
132+
}
133+
}
134+
135+
override fun visitMethodCallExpression(expression: PsiMethodCallExpression) {
136+
val method = expression.resolveMethod()
137+
if (method != null) {
138+
val containingClass = method.containingClass
139+
140+
// Normally, Java uses the type of the instance to qualify the method calls
141+
// However, if the method is part of java.lang.Object (e.g. equals or toString)
142+
// and no class in the hierarchy of the instance overrides the method, Java will
143+
// insert the call using java.lang.Object as the owner
144+
val qualifier =
145+
if (method.isConstructor || containingClass?.qualifiedName == CommonClassNames.JAVA_LANG_OBJECT) {
146+
containingClass
147+
} else {
148+
QualifiedMember.resolveQualifier(expression.methodExpression)
149+
}
150+
151+
visitMethodUsage(method, qualifier, expression)
152+
}
153+
154+
super.visitMethodCallExpression(expression)
155+
}
156+
}
157+
158+
private class MyCollectVisitor(
159+
mode: Mode,
160+
private val project: Project,
161+
private val selector: MixinSelector,
162+
private val fuzz: Int,
163+
private val skip: Set<Int>,
164+
) : CollectVisitor<PsiMethod>(mode) {
165+
override fun accept(methodNode: MethodNode) {
166+
val insns = methodNode.instructions ?: return
167+
insns.iterator().forEachRemaining { insn ->
168+
if (insn !is MethodInsnNode) {
169+
return@forEachRemaining
170+
}
171+
172+
val sourceMethod = nodeMatchesSelector(insn, mode, selector, project) ?: return@forEachRemaining
173+
174+
val offset = insns.indexOf(insn)
175+
val maxOffset = (offset + fuzz + 1).coerceAtMost(insns.size())
176+
var resultingInsn = insn
177+
for (i in offset + 1 until maxOffset) {
178+
val candidate = insns[i]
179+
if (candidate is VarInsnNode && candidate.opcode >= Opcodes.ISTORE) {
180+
resultingInsn = candidate
181+
break
182+
} else if (skip.isNotEmpty() && candidate.opcode !in skip) {
183+
break
184+
}
185+
}
186+
187+
resultingInsn = resultingInsn.next ?: resultingInsn
188+
189+
addResult(
190+
resultingInsn,
191+
sourceMethod,
192+
qualifier = insn.owner.replace('/', '.'),
193+
)
194+
}
195+
}
196+
}
197+
}

src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeInjectionPoint.kt

Lines changed: 10 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -39,27 +39,14 @@ import org.objectweb.asm.tree.ClassNode
3939
import org.objectweb.asm.tree.MethodInsnNode
4040
import org.objectweb.asm.tree.MethodNode
4141

42-
abstract class AbstractInvokeInjectionPoint(private val assign: Boolean) : AbstractMethodInjectionPoint() {
42+
class InvokeInjectionPoint : AbstractMethodInjectionPoint() {
4343
override fun onCompleted(editor: Editor, reference: PsiLiteral) {
4444
completeExtraStringAtAttribute(editor, reference, "target")
4545
}
4646

4747
override fun isShiftDiscouraged(shift: Int): Boolean {
48-
if (shift == 0) {
49-
return false
50-
}
51-
if (assign) {
52-
// allow shifting before the INVOKE_ASSIGN
53-
if (shift == -1) {
54-
return false
55-
}
56-
} else {
57-
// allow shifting after the INVOKE
58-
if (shift == 1) {
59-
return false
60-
}
61-
}
62-
return true
48+
// Allow shifting after INVOKE
49+
return shift != 0 && shift != 1
6350
}
6451

6552
override fun createNavigationVisitor(
@@ -77,9 +64,9 @@ abstract class AbstractInvokeInjectionPoint(private val assign: Boolean) : Abstr
7764
mode: CollectVisitor.Mode,
7865
): CollectVisitor<PsiMethod>? {
7966
if (mode == CollectVisitor.Mode.COMPLETION) {
80-
return MyCollectVisitor(mode, at.project, MemberReference(""), assign)
67+
return MyCollectVisitor(mode, at.project, MemberReference(""))
8168
}
82-
return target?.let { MyCollectVisitor(mode, at.project, it, assign) }
69+
return target?.let { MyCollectVisitor(mode, at.project, it) }
8370
}
8471

8572
private class MyNavigationVisitor(
@@ -165,7 +152,6 @@ abstract class AbstractInvokeInjectionPoint(private val assign: Boolean) : Abstr
165152
mode: Mode,
166153
private val project: Project,
167154
private val selector: MixinSelector,
168-
private val assign: Boolean,
169155
) : CollectVisitor<PsiMethod>(mode) {
170156
override fun accept(methodNode: MethodNode) {
171157
val insns = methodNode.instructions ?: return
@@ -175,19 +161,12 @@ abstract class AbstractInvokeInjectionPoint(private val assign: Boolean) : Abstr
175161
}
176162

177163
val sourceMethod = nodeMatchesSelector(insn, mode, selector, project) ?: return@forEachRemaining
178-
val actualInsn = if (assign) insn.next else insn
179-
if (actualInsn != null) {
180-
addResult(
181-
actualInsn,
182-
sourceMethod,
183-
qualifier = insn.owner.replace('/', '.'),
184-
)
185-
}
164+
addResult(
165+
insn,
166+
sourceMethod,
167+
qualifier = insn.owner.replace('/', '.'),
168+
)
186169
}
187170
}
188171
}
189172
}
190-
191-
class InvokeInjectionPoint : AbstractInvokeInjectionPoint(false)
192-
193-
class InvokeAssignInjectionPoint : AbstractInvokeInjectionPoint(true)

src/main/kotlin/platform/mixin/handlers/injectionPoint/NewInsnInjectionPoint.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ class NewInsnInjectionPoint : InjectionPoint<PsiMember>() {
6464

6565
override fun getArgsKeys(at: PsiAnnotation) = ARGS_KEYS
6666

67-
override fun getArgsValues(at: PsiAnnotation, key: String): Array<Any> {
67+
override fun getArgsValues(at: PsiAnnotation, key: String): Array<out Any> {
6868
if (key != "class") {
6969
return ArrayUtilRt.EMPTY_OBJECT_ARRAY
7070
}

0 commit comments

Comments
 (0)