diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 6a3b0075c..f0af3ce33 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -68,7 +68,7 @@ fuel-coroutines = { module = "com.github.kittinunf.fuel:fuel-coroutines", versio
sentry = "io.sentry:sentry:8.37.1"
# Testing
-test-mixin = "org.spongepowered:mixin:0.8.5"
+test-mixin = "net.fabricmc:sponge-mixin:0.17.4+mixin.0.8.7"
test-spigotapi = "org.spigotmc:spigot-api:1.21.11-R0.2-SNAPSHOT"
test-bungeecord = "net.md-5:bungeecord-api:1.21-R0.4"
test-spongeapi = "org.spongepowered:spongeapi:7.4.0"
diff --git a/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInComplex.java b/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInComplex.java
new file mode 100644
index 000000000..4dab24dd6
--- /dev/null
+++ b/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInComplex.java
@@ -0,0 +1,66 @@
+/*
+ * 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.mixintestdata.invalidInjectorMethodSignatureFix;
+
+public class MixedInComplex {
+ public void method1(String arg) {
+ int local1 = 0;
+ CharSequence local2 = null;
+ char local3 = 'a';
+ Integer local4 = 10;
+ }
+
+ public void method2(int arg) {
+ int local1 = 0;
+ }
+
+ public String method3(Integer arg) {
+ return arg.toString();
+ }
+
+ public Integer method4() {
+ return 1;
+ }
+
+ public void method5(CharSequence arg) {
+ int local1 = 0;
+ String local2 = null;
+ int local3 = 0;
+ int local4 = 0;
+ char local5 = 0;
+ char local6 = 0;
+ }
+
+ public void method6(CharSequence arg) {
+ int local1 = 0;
+ String local2 = null;
+ int local3 = 0;
+ int local4 = 0;
+ char local5 = 0;
+ short local6 = 0;
+ }
+
+ public void method7(char a) {
+ }
+
+ public void method8(short a) {
+ }
+}
diff --git a/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInModifyArg.java b/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInModifyArg.java
new file mode 100644
index 000000000..3c63c7532
--- /dev/null
+++ b/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInModifyArg.java
@@ -0,0 +1,38 @@
+/*
+ * 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.mixintestdata.invalidInjectorMethodSignatureFix;
+
+public class MixedInModifyArg {
+ public void caller(Object obj) {
+ callee1("hello", 1);
+ callee2('a', (short) 3, 4);
+ callee3(1, 2, 3);
+ }
+
+ private static void callee1(String x, int y) {
+ }
+
+ private static void callee2(char w, short x, int y) {
+ }
+
+ private static void callee3(int a, int b, int c) {
+ }
+}
diff --git a/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInModifyConstant.java b/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInModifyConstant.java
new file mode 100644
index 000000000..c87d587bb
--- /dev/null
+++ b/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInModifyConstant.java
@@ -0,0 +1,37 @@
+/*
+ * 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.mixintestdata.invalidInjectorMethodSignatureFix;
+
+public class MixedInModifyConstant {
+ public void method1(Class> arg) {
+ System.out.println(42);
+ System.out.println(101);
+ System.out.println(Long.class);
+ System.out.println((Object) 5 instanceof Integer);
+ System.out.println("hello");
+ }
+
+ public void method2(String arg) {
+ System.out.println(Long.class);
+ System.out.println((Object) 5 instanceof Integer);
+ System.out.println((Object) null);
+ }
+}
diff --git a/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInModifyExpressionValue.java b/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInModifyExpressionValue.java
new file mode 100644
index 000000000..9762a95bb
--- /dev/null
+++ b/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInModifyExpressionValue.java
@@ -0,0 +1,57 @@
+/*
+ * 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.mixintestdata.invalidInjectorMethodSignatureFix;
+
+public class MixedInModifyExpressionValue {
+ public void method1() {
+ System.out.println(42);
+ System.out.println(101);
+ System.out.println(callee1());
+ System.out.println(callee2());
+ System.out.println(callee5());
+ }
+
+ public void method2(String a, long b) {
+ System.out.println('a');
+ System.out.println(callee3());
+ System.out.println(callee4());
+ }
+
+ private static char callee1() {
+ return 'a';
+ }
+
+ private static int callee2() {
+ return 1;
+ }
+
+ private static Long[] callee3() {
+ return new Long[0];
+ }
+
+ private static Double[] callee4() {
+ return new Double[0];
+ }
+
+ private static short callee5() {
+ return (short) 1;
+ }
+}
diff --git a/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInModifyReceiver.java b/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInModifyReceiver.java
new file mode 100644
index 000000000..962f7d524
--- /dev/null
+++ b/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInModifyReceiver.java
@@ -0,0 +1,45 @@
+/*
+ * 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.mixintestdata.invalidInjectorMethodSignatureFix;
+
+public abstract class MixedInModifyReceiver implements Comparable {
+ public String test;
+ public char test2;
+
+ public void caller(char c) {
+ callee1("hello", 1);
+ callee2('a', (short) 3, 4);
+ callee3(1, 2, 3);
+ this.test = "hello";
+ this.test2 = 'a';
+ System.out.println(this.test2);
+ System.out.println(Integer.valueOf(5).longValue());
+ }
+
+ private void callee1(String x, int y) {
+ }
+
+ private void callee2(char w, short x, int y) {
+ }
+
+ private void callee3(int a, int b, int c) {
+ }
+}
diff --git a/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInModifyReturnValue.java b/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInModifyReturnValue.java
new file mode 100644
index 000000000..9dae1b57b
--- /dev/null
+++ b/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInModifyReturnValue.java
@@ -0,0 +1,43 @@
+/*
+ * 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.mixintestdata.invalidInjectorMethodSignatureFix;
+
+public class MixedInModifyReturnValue {
+ public String method1() {
+ return "a";
+ }
+
+ public Integer method2() {
+ return 2;
+ }
+
+ public static Long method3() {
+ return 3L;
+ }
+
+ public int method4(String a, int b) {
+ return b;
+ }
+
+ public char method5(String a, short b) {
+ return (char) b;
+ }
+}
diff --git a/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInModifyVariable.java b/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInModifyVariable.java
new file mode 100644
index 000000000..f6b028253
--- /dev/null
+++ b/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInModifyVariable.java
@@ -0,0 +1,41 @@
+/*
+ * 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.mixintestdata.invalidInjectorMethodSignatureFix;
+
+public class MixedInModifyVariable {
+ public void method1(String arg, Object arg2) {
+ int local1 = 0;
+ CharSequence local2 = null;
+ char local3 = 'a';
+ Integer local4 = 10;
+ }
+
+ public void method2(int arg) {
+ int local1 = 0;
+ }
+
+ public void method3(CharSequence arg) {
+ int local1 = 0;
+ String local2 = null;
+ int local3 = 0;
+ int local4 = 0;
+ }
+}
diff --git a/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInRedirect.java b/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInRedirect.java
new file mode 100644
index 000000000..725c9d7ee
--- /dev/null
+++ b/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInRedirect.java
@@ -0,0 +1,60 @@
+/*
+ * 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.mixintestdata.invalidInjectorMethodSignatureFix;
+
+public abstract class MixedInRedirect implements Comparable {
+ public String test;
+ public char test2;
+
+ public void caller(char c) {
+ callee1("hello", 1);
+ callee2('a', (short) 3, 4);
+ callee3(1, 2, 3);
+ callee4("hello", void.class);
+ callee5("hello", void.class);
+ this.test = "hello";
+ this.test2 = 'a';
+ System.out.println(this.test2);
+ System.out.println(Integer.valueOf(5).intValue());
+ System.out.println((Object) 5 instanceof Integer);
+ }
+
+ public void caller2(String arg) {
+ System.out.println((Object) 5 instanceof Integer);
+ }
+
+ private void callee1(String x, int y) {
+ }
+
+ private void callee2(char w, short x, int y) {
+ }
+
+ private void callee3(int a, int b, int c) {
+ }
+
+ private static Class> callee4(String instance, Class> clazz) {
+ return clazz;
+ }
+
+ private static Class> callee5(String instance, Object clazz) {
+ return null;
+ }
+}
diff --git a/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInWrapMethod.java b/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInWrapMethod.java
new file mode 100644
index 000000000..6afd46238
--- /dev/null
+++ b/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInWrapMethod.java
@@ -0,0 +1,54 @@
+/*
+ * 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.mixintestdata.invalidInjectorMethodSignatureFix;
+
+import java.util.Collection;
+import java.util.List;
+
+public class MixedInWrapMethod {
+ public void method1(String arg) {
+ }
+
+ public Integer method2() {
+ return 1;
+ }
+
+ public void method3(CharSequence arg) {
+ }
+
+ public Long method4() {
+ return 1L;
+ }
+
+ public Number method5(List list, int b) {
+ return null;
+ }
+
+ public Integer method6(Collection list, char c) {
+ return null;
+ }
+
+ public static void method7(String arg) {
+ }
+
+ public static void method8(int arg) {
+ }
+}
diff --git a/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInWrapOperation.java b/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInWrapOperation.java
new file mode 100644
index 000000000..f07d4455b
--- /dev/null
+++ b/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInWrapOperation.java
@@ -0,0 +1,82 @@
+/*
+ * 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.mixintestdata.invalidInjectorMethodSignatureFix;
+
+public abstract class MixedInWrapOperation implements Comparable {
+ public String test;
+ public char test2;
+
+ public void caller(char c) {
+ callee1("hello", 1);
+ callee2('a', (short) 3, 4);
+ callee3(1, 2, 3);
+ this.test = "hello";
+ this.test2 = 'a';
+ System.out.println(this.test2);
+ System.out.println(Integer.valueOf(5).intValue());
+ System.out.println((Object) 5 instanceof Integer);
+ }
+
+ public void caller2(int arg) {
+ System.out.println(arg == 3);
+ System.out.println(arg == 4);
+ callee4('a', 'b');
+ callee5(1, (short) 2);
+ callee6((short) 1, 2);
+ callee7((short) 1, true);
+ }
+
+ public void caller3(Object arg) {
+ callee8(arg);
+ callee9(arg, arg);
+ }
+
+ private void callee1(String x, int y) {
+ }
+
+ private void callee2(char w, short x, int y) {
+ }
+
+ private void callee3(int a, int b, int c) {
+ }
+
+ private static int callee4(char a, char b) {
+ return 0;
+ }
+
+ private static boolean callee5(int a, short b) {
+ return true;
+ }
+
+ private static boolean callee6(short a, int b) {
+ return true;
+ }
+
+ private static boolean callee7(short a, boolean b) {
+ return true;
+ }
+
+ private static void callee8(Object arg) {
+ }
+
+ private static void callee9(Object arg, Object arg2) {
+ }
+}
diff --git a/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInWrapWithCondition.java b/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInWrapWithCondition.java
new file mode 100644
index 000000000..ccd150a89
--- /dev/null
+++ b/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInWrapWithCondition.java
@@ -0,0 +1,44 @@
+/*
+ * 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.mixintestdata.invalidInjectorMethodSignatureFix;
+
+public class MixedInWrapWithCondition {
+ public String test;
+ public char test2;
+
+ public void caller(char c) {
+ callee1("hello", 1);
+ callee2('a', (short) 3, 4);
+ callee3(1, 2, 3);
+ this.test = "hello";
+ this.test2 = 'a';
+ System.out.println(this.test2);
+ }
+
+ private void callee1(String x, int y) {
+ }
+
+ private void callee2(char w, short x, int y) {
+ }
+
+ private void callee3(int a, int b, int c) {
+ }
+}
diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionMatchUtil.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionMatchUtil.kt
index 5982f9562..6e5264d9f 100644
--- a/src/main/kotlin/platform/mixin/expression/MEExpressionMatchUtil.kt
+++ b/src/main/kotlin/platform/mixin/expression/MEExpressionMatchUtil.kt
@@ -20,7 +20,7 @@
package com.demonwav.mcdev.platform.mixin.expression
-import com.demonwav.mcdev.platform.mixin.handlers.InjectorAnnotationHandler
+import com.demonwav.mcdev.platform.mixin.handlers.InsnInjectorAnnotationHandler
import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler
import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.CollectVisitor
import com.demonwav.mcdev.platform.mixin.util.LocalInfo
@@ -249,7 +249,7 @@ object MEExpressionMatchUtil {
return ExpressionContext.Type.SLICE
}
- val handler = MixinAnnotationHandler.forMixinAnnotation(annotationName, project) as? InjectorAnnotationHandler
+ val handler = MixinAnnotationHandler.forMixinAnnotation(annotationName, project) as? InsnInjectorAnnotationHandler
?: return ExpressionContext.Type.CUSTOM
return handler.mixinExtrasExpressionContextType
}
diff --git a/src/main/kotlin/platform/mixin/expression/gui/MEShowFlowAction.kt b/src/main/kotlin/platform/mixin/expression/gui/MEShowFlowAction.kt
index d1f5d12a2..2e53784b0 100644
--- a/src/main/kotlin/platform/mixin/expression/gui/MEShowFlowAction.kt
+++ b/src/main/kotlin/platform/mixin/expression/gui/MEShowFlowAction.kt
@@ -20,10 +20,9 @@
package com.demonwav.mcdev.platform.mixin.expression.gui
-import com.demonwav.mcdev.platform.mixin.handlers.InjectorAnnotationHandler
+import com.demonwav.mcdev.platform.mixin.handlers.InsnInjectorAnnotationHandler
import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler
import com.demonwav.mcdev.platform.mixin.reference.MethodReference
-import com.demonwav.mcdev.platform.mixin.util.MethodTargetMember
import com.demonwav.mcdev.platform.mixin.util.MixinConstants
import com.demonwav.mcdev.platform.mixin.util.findClassNodeByPsiClass
import com.demonwav.mcdev.platform.mixin.util.isMixin
@@ -51,7 +50,6 @@ import com.intellij.psi.PsiMethod
import com.intellij.psi.PsiNameValuePair
import com.intellij.psi.util.isAncestor
import com.intellij.psi.util.parentOfType
-import com.intellij.psi.util.parents
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.LineNumberNode
import org.objectweb.asm.tree.MethodNode
@@ -133,12 +131,11 @@ class MEShowFlowAction : AnAction() {
val modifierList = string.parentOfType()?.modifierList ?: return emptySequence()
val (injectorAnnotation, injector) =
modifierList.annotations.firstNotNullOfOrNull { ann ->
- (MixinAnnotationHandler.forMixinAnnotation(ann, project) as? InjectorAnnotationHandler)
+ (MixinAnnotationHandler.forMixinAnnotation(ann, project) as? InsnInjectorAnnotationHandler)
?.let { ann to it }
} ?: return emptySequence()
return psiClass.mixinTargets.asSequence()
.flatMap { injector.resolveTarget(injectorAnnotation, it) }
- .filterIsInstance()
.map { target ->
Resolved(target.classAndMethod.clazz, target.classAndMethod.method) {
it.populateMatchStatuses(module, string, modifierList)
diff --git a/src/main/kotlin/platform/mixin/handlers/InjectAnnotationHandler.kt b/src/main/kotlin/platform/mixin/handlers/InjectAnnotationHandler.kt
index 0c56329d3..eda238ce8 100644
--- a/src/main/kotlin/platform/mixin/handlers/InjectAnnotationHandler.kt
+++ b/src/main/kotlin/platform/mixin/handlers/InjectAnnotationHandler.kt
@@ -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
@@ -20,23 +20,23 @@
package com.demonwav.mcdev.platform.mixin.handlers
-import com.demonwav.mcdev.platform.mixin.inspection.injector.MethodSignature
-import com.demonwav.mcdev.platform.mixin.inspection.injector.ParameterGroup
+import com.demonwav.mcdev.platform.mixin.handlers.mixinextras.TargetInsn
+import com.demonwav.mcdev.platform.mixin.inspection.injector.ExpectedSignatures
+import com.demonwav.mcdev.platform.mixin.inspection.injector.InjectSignatures
+import com.demonwav.mcdev.platform.mixin.inspection.injector.SuggestedSignature
+import com.demonwav.mcdev.platform.mixin.inspection.injector.collectSignatures
+import com.demonwav.mcdev.platform.mixin.util.ClassAndMethodNode
import com.demonwav.mcdev.platform.mixin.util.LocalVariables
-import com.demonwav.mcdev.platform.mixin.util.callbackInfoReturnableType
-import com.demonwav.mcdev.platform.mixin.util.callbackInfoType
import com.demonwav.mcdev.platform.mixin.util.getGenericReturnType
import com.demonwav.mcdev.platform.mixin.util.hasAccess
import com.demonwav.mcdev.platform.mixin.util.isFabricMixin
import com.demonwav.mcdev.platform.mixin.util.toPsiType
import com.demonwav.mcdev.util.Parameter
import com.demonwav.mcdev.util.findModule
-import com.demonwav.mcdev.util.firstIndexOrNull
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.PsiAnnotation
import com.intellij.psi.PsiMethod
import com.intellij.psi.PsiQualifiedReference
-import com.intellij.psi.PsiTypes
import com.intellij.psi.util.parentOfType
import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext
import org.objectweb.asm.Opcodes
@@ -44,100 +44,67 @@ import org.objectweb.asm.Type
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.MethodNode
-class InjectAnnotationHandler : InjectorAnnotationHandler() {
+class InjectAnnotationHandler : InsnInjectorAnnotationHandler() {
override fun expectedMethodSignature(
annotation: PsiAnnotation,
targetClass: ClassNode,
targetMethod: MethodNode,
- ): List {
+ targetInsn: TargetInsn,
+ ): ExpectedSignatures {
val returnType = targetMethod.getGenericReturnType(targetClass, annotation.project)
- val result = ArrayList()
-
- // Parameters from injected method (optional)
- result.add(
- ParameterGroup(
- collectTargetMethodParameters(annotation.project, targetClass, targetMethod),
- required = ParameterGroup.RequiredLevel.OPTIONAL,
- default = true,
- ),
- )
-
- // Callback info (required)
- result.add(
- ParameterGroup(
- listOf(
- if (returnType == PsiTypes.voidType()) {
- Parameter("ci", callbackInfoType(annotation.project))
- } else {
- Parameter(
- "cir",
- callbackInfoReturnableType(annotation.project, annotation, returnType)!!,
- )
- },
- ),
- ),
- )
+ // Parameters from injected method
+ val targetParams = collectTargetMethodParameters(annotation.project, targetClass, targetMethod)
// Captured locals (only if local capture is enabled)
+ var capturedLocals = emptyList()
+
val localCapture = (annotation.findDeclaredAttributeValue("locals") as? PsiQualifiedReference)
?.referenceName ?: "NO_CAPTURE"
if (localCapture != "NO_CAPTURE") {
annotation.findModule()?.let { module ->
- var commonLocalsPrefix: MutableList? = null
- val resolvedInsns = resolveInstructions(annotation, targetClass, targetMethod).ifEmpty { return@let }
- for (insn in resolvedInsns) {
- val locals = LocalVariables.getLocals(module, targetClass, targetMethod, insn.insn)
- ?.filterNotNull()
- ?.drop(
- Type.getArgumentTypes(targetMethod.desc).size +
- if (targetMethod.hasAccess(Opcodes.ACC_STATIC)) 0 else 1,
- )
- ?.filter { it.desc != null }
- ?: continue
- if (commonLocalsPrefix == null) {
- commonLocalsPrefix = locals.toMutableList()
- } else {
- val mismatch = commonLocalsPrefix.zip(locals).firstIndexOrNull { (a, b) -> a.desc != b.desc }
- if (mismatch != null) {
- commonLocalsPrefix.subList(mismatch, commonLocalsPrefix.size).clear()
- }
- }
- }
-
- if (commonLocalsPrefix != null) {
- val elementFactory = JavaPsiFacade.getElementFactory(annotation.project)
- val localParams = commonLocalsPrefix.map { local ->
- val type =
- Type.getType(local.desc).toPsiType(elementFactory, annotation.parentOfType())
- sanitizedParameter(type, local.name)
- }
- val requiredLevel = if (localCapture == "CAPTURE_FAILSOFT") {
- ParameterGroup.RequiredLevel.WARN_IF_ABSENT
- } else {
- ParameterGroup.RequiredLevel.ERROR_IF_ABSENT
- }
- result.add(
- ParameterGroup(
- localParams,
- default = true,
- required = requiredLevel,
- isVarargs = true,
- ),
+ val locals = LocalVariables.getLocals(module, targetClass, targetMethod, targetInsn.insn)
+ ?.filterNotNull()
+ ?.drop(
+ Type.getArgumentTypes(targetMethod.desc).size +
+ if (targetMethod.hasAccess(Opcodes.ACC_STATIC)) 0 else 1,
)
+ ?.filter { it.desc != null }
+ ?: return@let
+
+ val elementFactory = JavaPsiFacade.getElementFactory(annotation.project)
+ capturedLocals = locals.map { local ->
+ val type =
+ Type.getType(local.desc).toPsiType(elementFactory, annotation.parentOfType())
+ sanitizedParameter(type, local.name)
}
}
}
- return listOf(MethodSignature(result, PsiTypes.voidType()))
+ return ExpectedSignatures.Valid(
+ InjectSignatures.create(
+ annotation,
+ targetParams,
+ returnType,
+ capturedLocals,
+ ) ?: return ExpectedSignatures.Invalid
+ )
+ }
+
+ override fun suggestedMethodSignature(
+ annotation: PsiAnnotation,
+ targets: List
+ ): SuggestedSignature? {
+ return SuggestedSignature.inject(
+ annotation,
+ expectedMethodSignatures(annotation, targets).collectSignatures() ?: return null,
+ )
}
override fun canAlwaysBeStatic(method: PsiMethod): Boolean {
return method.isFabricMixin
}
- override val allowCoerce = true
-
override val isShiftAlwaysDiscouraged = false
override val mixinExtrasExpressionContextType = ExpressionContext.Type.INJECT
diff --git a/src/main/kotlin/platform/mixin/handlers/InjectorAnnotationHandler.kt b/src/main/kotlin/platform/mixin/handlers/InjectorAnnotationHandler.kt
index a170cdd5f..5e9866dc5 100644
--- a/src/main/kotlin/platform/mixin/handlers/InjectorAnnotationHandler.kt
+++ b/src/main/kotlin/platform/mixin/handlers/InjectorAnnotationHandler.kt
@@ -24,23 +24,20 @@ import com.demonwav.mcdev.asset.MixinAssets
import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.AtResolver
import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.CollectVisitor
import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.InsnResolutionInfo
-import com.demonwav.mcdev.platform.mixin.inspection.injector.MethodSignature
+import com.demonwav.mcdev.platform.mixin.handlers.mixinextras.TargetInsn
+import com.demonwav.mcdev.platform.mixin.inspection.injector.ExpectedSignatures
+import com.demonwav.mcdev.platform.mixin.inspection.injector.SuggestedSignature
import com.demonwav.mcdev.platform.mixin.reference.DescSelectorParser
import com.demonwav.mcdev.platform.mixin.reference.isMiscDynamicSelector
import com.demonwav.mcdev.platform.mixin.reference.parseMixinSelector
import com.demonwav.mcdev.platform.mixin.util.ClassAndMethodNode
import com.demonwav.mcdev.platform.mixin.util.MethodTargetMember
-import com.demonwav.mcdev.platform.mixin.util.MixinTargetMember
import com.demonwav.mcdev.platform.mixin.util.findMethods
import com.demonwav.mcdev.platform.mixin.util.getGenericParameterTypes
import com.demonwav.mcdev.platform.mixin.util.hasAccess
-import com.demonwav.mcdev.platform.mixin.util.mixinTargets
import com.demonwav.mcdev.util.Parameter
-import com.demonwav.mcdev.util.cached
import com.demonwav.mcdev.util.computeStringArray
import com.demonwav.mcdev.util.findAnnotations
-import com.demonwav.mcdev.util.findContainingClass
-import com.demonwav.mcdev.util.ifNullOrEmpty
import com.demonwav.mcdev.util.toJavaIdentifier
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiAnnotation
@@ -49,17 +46,14 @@ import com.intellij.psi.PsiEllipsisType
import com.intellij.psi.PsiMethod
import com.intellij.psi.PsiModifier
import com.intellij.psi.PsiType
-import com.intellij.psi.util.PsiModificationTracker
import com.intellij.psi.util.findParentOfType
import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext
-import java.util.concurrent.ConcurrentHashMap
import org.objectweb.asm.Opcodes
-import org.objectweb.asm.tree.AbstractInsnNode
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.MethodNode
abstract class InjectorAnnotationHandler : MixinAnnotationHandler {
- override fun resolveTarget(annotation: PsiAnnotation, targetClass: ClassNode): List {
+ override fun resolveTarget(annotation: PsiAnnotation, targetClass: ClassNode): List {
val methodAttr = annotation.findAttributeValue("method")
val method = methodAttr?.computeStringArray() ?: emptyList()
val desc = annotation.findAttributeValue("desc")?.findAnnotations() ?: emptyList()
@@ -88,105 +82,48 @@ abstract class InjectorAnnotationHandler : MixinAnnotationHandler {
return null
}
- return resolveTarget(annotation, targetClass).map { targetMember ->
- val targetMethod = targetMember as? MethodTargetMember ?: return@map InsnResolutionInfo.Failure(AtResolver.DEFAULT_UNRESOLVED_MESSAGE)
+ return resolveTarget(annotation, targetClass).map { targetMethod ->
isUnresolved(annotation, targetClass, targetMethod.classAndMethod.method) ?: return@isUnresolved null
}.reduceOrNull(InsnResolutionInfo.Failure::combine) ?: InsnResolutionInfo.Failure(AtResolver.DEFAULT_UNRESOLVED_MESSAGE)
}
- open fun getAtKey(annotation: PsiAnnotation): String = "at"
-
- protected open fun isUnresolved(
+ protected abstract fun isUnresolved(
annotation: PsiAnnotation,
targetClass: ClassNode,
targetMethod: MethodNode,
- ): InsnResolutionInfo.Failure? {
- return annotation.findAttributeValue(getAtKey(annotation))?.findAnnotations()
- .ifNullOrEmpty { return InsnResolutionInfo.Failure(AtResolver.DEFAULT_UNRESOLVED_MESSAGE) }!!
- .firstNotNullOfOrNull { AtResolver(it, targetClass, targetMethod).isUnresolved() }
- }
+ ): InsnResolutionInfo.Failure?
override fun resolveForNavigation(annotation: PsiAnnotation, targetClass: ClassNode): List {
- return resolveTarget(annotation, targetClass).flatMap { targetMember ->
- val targetMethod = targetMember as? MethodTargetMember ?: return@flatMap emptyList()
+ return resolveTarget(annotation, targetClass).flatMap { targetMethod ->
resolveForNavigation(annotation, targetMethod.classAndMethod.clazz, targetMethod.classAndMethod.method)
}
}
- protected open fun resolveForNavigation(
+ protected abstract fun resolveForNavigation(
annotation: PsiAnnotation,
targetClass: ClassNode,
targetMethod: MethodNode,
- ): List {
- return annotation.findAttributeValue(getAtKey(annotation))?.findAnnotations()
- .ifNullOrEmpty { return emptyList() }!!
- .flatMap { AtResolver(it, targetClass, targetMethod).resolveNavigationTargets() }
- }
+ ): List
- fun resolveInstructions(annotation: PsiAnnotation) = annotation.cached(PsiModificationTracker.MODIFICATION_COUNT) {
- val containingClass = annotation.findContainingClass() ?: return@cached emptyList()
- containingClass.mixinTargets.flatMap { resolveInstructions(annotation, it) }
- }
-
- fun resolveInstructions(annotation: PsiAnnotation, targetClass: ClassNode): List {
- return resolveTarget(annotation, targetClass)
- .flatMap { targetMember ->
- val targetMethod = (targetMember as? MethodTargetMember)?.classAndMethod ?: return@flatMap emptyList()
- resolveInstructions(annotation, targetMethod.clazz, targetMethod.method).map { result ->
- InsnResult(targetMethod, result)
- }
- }
- }
-
- open fun resolveInstructions(
+ abstract fun expectedMethodSignatures(
annotation: PsiAnnotation,
- targetClass: ClassNode,
- targetMethod: MethodNode,
- mode: CollectVisitor.Mode = CollectVisitor.Mode.RESOLUTION,
- ): List> {
- val cache = annotation.cached(PsiModificationTracker.MODIFICATION_COUNT) {
- ConcurrentHashMap, List>>()
- }
- return cache.computeIfAbsent(ClassAndMethodNode(targetClass, targetMethod) to mode) {
- annotation.findAttributeValue(getAtKey(annotation))?.findAnnotations()
- .ifNullOrEmpty { return@computeIfAbsent emptyList() }!!
- .flatMap { AtResolver(it, targetClass, targetMethod).resolveInstructions(mode) }
- }
- }
+ targets: List,
+ mode: CollectVisitor.Mode = CollectVisitor.Mode.SUGGESTION,
+ ): List>
- /**
- * Returns a list of valid method signatures for the injector.
- * May return an empty list for no valid signatures, or null for all signatures being valid.
- * Null is usually returned when an error is detected, which is better handled by another inspection.
- */
- abstract fun expectedMethodSignature(
+ abstract fun suggestedMethodSignature(
annotation: PsiAnnotation,
- targetClass: ClassNode,
- targetMethod: MethodNode,
- ): List?
-
- open fun isInsnAllowed(insn: AbstractInsnNode, decorations: Map): Boolean {
- return true
- }
-
- open val allowedInsnDescription = "all instructions"
+ targets: List,
+ ): SuggestedSignature?
open fun canAlwaysBeStatic(method: PsiMethod): Boolean {
return true
}
- open val allowCoerce = false
-
- open val isShiftAlwaysDiscouraged = true
-
override val isImplicitlyUsed = true
override val icon = MixinAssets.MIXIN_INJECTOR_ICON
- abstract val mixinExtrasExpressionContextType: ExpressionContext.Type
-
- data class InsnResult(val method: ClassAndMethodNode, val result: CollectVisitor.Result<*>)
-
companion object {
@JvmStatic
protected fun collectTargetMethodParameters(
@@ -221,12 +158,18 @@ abstract class InjectorAnnotationHandler : MixinAnnotationHandler {
}
}
-object DefaultInjectorAnnotationHandler : InjectorAnnotationHandler() {
+object DefaultInjectorAnnotationHandler : InsnInjectorAnnotationHandler() {
override fun expectedMethodSignature(
annotation: PsiAnnotation,
targetClass: ClassNode,
targetMethod: MethodNode,
- ) = null
+ targetInsn: TargetInsn,
+ ): ExpectedSignatures = ExpectedSignatures.Unknown
+
+ override fun suggestedMethodSignature(
+ annotation: PsiAnnotation,
+ targets: List
+ ): SuggestedSignature? = null
override val isSoft = true
diff --git a/src/main/kotlin/platform/mixin/handlers/InsnInjectorAnnotationHandler.kt b/src/main/kotlin/platform/mixin/handlers/InsnInjectorAnnotationHandler.kt
new file mode 100644
index 000000000..4ee76f775
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/handlers/InsnInjectorAnnotationHandler.kt
@@ -0,0 +1,142 @@
+/*
+ * 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.handlers
+
+import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.AtResolver
+import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.CollectVisitor
+import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.InsnResolutionInfo
+import com.demonwav.mcdev.platform.mixin.handlers.mixinextras.TargetInsn
+import com.demonwav.mcdev.platform.mixin.inspection.injector.ExpectedSignatures
+import com.demonwav.mcdev.platform.mixin.util.ClassAndMethodNode
+import com.demonwav.mcdev.platform.mixin.util.mixinTargets
+import com.demonwav.mcdev.util.cached
+import com.demonwav.mcdev.util.findAnnotations
+import com.demonwav.mcdev.util.findContainingClass
+import com.demonwav.mcdev.util.ifNullOrEmpty
+import com.intellij.psi.PsiAnnotation
+import com.intellij.psi.PsiElement
+import com.intellij.psi.util.PsiModificationTracker
+import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext
+import java.util.concurrent.ConcurrentHashMap
+import org.objectweb.asm.tree.AbstractInsnNode
+import org.objectweb.asm.tree.ClassNode
+import org.objectweb.asm.tree.MethodNode
+
+abstract class InsnInjectorAnnotationHandler : InjectorAnnotationHandler() {
+ open fun getAtKey(annotation: PsiAnnotation): String = "at"
+
+ final override fun isUnresolved(
+ annotation: PsiAnnotation,
+ targetClass: ClassNode,
+ targetMethod: MethodNode
+ ): InsnResolutionInfo.Failure? {
+ val results = annotation.findAttributeValue(getAtKey(annotation))?.findAnnotations()
+ .ifNullOrEmpty { return InsnResolutionInfo.Failure(AtResolver.DEFAULT_UNRESOLVED_MESSAGE) }
+ .map { AtResolver(it, targetClass, targetMethod).isUnresolved() }
+ return if (null in results) null else results.firstNotNullOf { it }
+ }
+
+ final override fun resolveForNavigation(
+ annotation: PsiAnnotation,
+ targetClass: ClassNode,
+ targetMethod: MethodNode
+ ): List {
+ return annotation.findAttributeValue(getAtKey(annotation))?.findAnnotations()
+ .ifNullOrEmpty { return emptyList() }
+ .flatMap { AtResolver(it, targetClass, targetMethod).resolveNavigationTargets() }
+ }
+
+ fun resolveInstructions(annotation: PsiAnnotation) = annotation.cached(PsiModificationTracker.MODIFICATION_COUNT) {
+ val containingClass = annotation.findContainingClass() ?: return@cached emptyList()
+ containingClass.mixinTargets.flatMap { resolveInstructions(annotation, it) }
+ }
+
+ fun resolveInstructions(annotation: PsiAnnotation, targetClass: ClassNode): List {
+ return resolveInstructions(annotation, resolveTarget(annotation, targetClass).map { it.classAndMethod })
+ }
+
+ fun resolveInstructions(
+ annotation: PsiAnnotation,
+ targets: List,
+ mode: CollectVisitor.Mode = CollectVisitor.Mode.RESOLUTION,
+ ): List {
+ return targets.flatMap { (targetClass, targetMethod) ->
+ resolveInstructions(
+ annotation,
+ targetClass,
+ targetMethod,
+ mode,
+ ).map { InsnResult(ClassAndMethodNode(targetClass, targetMethod), it) }
+ }
+ }
+
+ open fun resolveInstructions(
+ annotation: PsiAnnotation,
+ targetClass: ClassNode,
+ targetMethod: MethodNode,
+ mode: CollectVisitor.Mode = CollectVisitor.Mode.RESOLUTION,
+ ): List> {
+ val cache = annotation.cached(PsiModificationTracker.MODIFICATION_COUNT) {
+ ConcurrentHashMap, List>>()
+ }
+ return cache.computeIfAbsent(ClassAndMethodNode(targetClass, targetMethod) to mode) {
+ annotation.findAttributeValue(getAtKey(annotation))?.findAnnotations()
+ .ifNullOrEmpty { return@computeIfAbsent emptyList() }
+ .flatMap { AtResolver(it, targetClass, targetMethod).resolveInstructions(mode) }
+ }
+ }
+
+ final override fun expectedMethodSignatures(
+ annotation: PsiAnnotation,
+ targets: List,
+ mode: CollectVisitor.Mode,
+ ): List> {
+ return resolveInstructions(annotation, targets, mode).map { (target, result) ->
+ val (targetClass, targetMethod) = target
+ val targetInsn = TargetInsn(result.insn, result.decorations)
+ expectedMethodSignature(annotation, targetClass, targetMethod, targetInsn)
+ }
+ }
+
+ /**
+ * Returns a list of valid method signatures for the injector.
+ * May return an empty list for no valid signatures, or null for all signatures being valid.
+ * Null is usually returned when an error is detected, which is better handled by another inspection.
+ */
+ abstract fun expectedMethodSignature(
+ annotation: PsiAnnotation,
+ targetClass: ClassNode,
+ targetMethod: MethodNode,
+ targetInsn: TargetInsn,
+ ): ExpectedSignatures<*>
+
+ open fun isInsnAllowed(insn: AbstractInsnNode, decorations: Map): Boolean {
+ return true
+ }
+
+ open val allowedInsnDescription = "all instructions"
+
+ open val isShiftAlwaysDiscouraged = true
+
+ abstract val mixinExtrasExpressionContextType: ExpressionContext.Type
+
+ data class InsnResult(val method: ClassAndMethodNode, val result: CollectVisitor.Result<*>)
+}
diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt
index 6fb196501..091824491 100644
--- a/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt
+++ b/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt
@@ -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
@@ -20,13 +20,17 @@
package com.demonwav.mcdev.platform.mixin.handlers
-import com.demonwav.mcdev.platform.mixin.inspection.injector.MethodSignature
-import com.demonwav.mcdev.platform.mixin.inspection.injector.ParameterGroup
-import com.demonwav.mcdev.platform.mixin.util.fakeResolve
-import com.demonwav.mcdev.platform.mixin.util.getParameter
+import com.demonwav.mcdev.platform.mixin.handlers.mixinextras.TargetInsn
+import com.demonwav.mcdev.platform.mixin.inspection.injector.ExpectedSignatures
+import com.demonwav.mcdev.platform.mixin.inspection.injector.ModifierSignatures
+import com.demonwav.mcdev.platform.mixin.inspection.injector.SuggestedSignature
+import com.demonwav.mcdev.platform.mixin.inspection.injector.collectSignatures
+import com.demonwav.mcdev.platform.mixin.util.ClassAndMethodNode
+import com.demonwav.mcdev.platform.mixin.util.getBytecodeParameter
import com.demonwav.mcdev.platform.mixin.util.toPsiType
+import com.demonwav.mcdev.util.MemberReference
+import com.demonwav.mcdev.util.SequencedMap
import com.demonwav.mcdev.util.constantValue
-import com.demonwav.mcdev.util.descriptor
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.PsiAnnotation
import com.intellij.psi.PsiMethod
@@ -37,7 +41,7 @@ import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.MethodInsnNode
import org.objectweb.asm.tree.MethodNode
-class ModifyArgHandler : InjectorAnnotationHandler() {
+class ModifyArgHandler : InsnInjectorAnnotationHandler() {
override fun isInsnAllowed(insn: AbstractInsnNode, decorations: Map): Boolean {
return insn is MethodInsnNode
}
@@ -48,95 +52,64 @@ class ModifyArgHandler : InjectorAnnotationHandler() {
annotation: PsiAnnotation,
targetClass: ClassNode,
targetMethod: MethodNode,
- ): List? {
+ targetInsn: TargetInsn,
+ ): ExpectedSignatures {
+ val insn = targetInsn.insn
+ if (insn !is MethodInsnNode) {
+ return ExpectedSignatures.Invalid
+ }
+ val project = annotation.project
val index = annotation.findDeclaredAttributeValue("index")?.constantValue as? Int
- val validSingleArgTypes = mutableSetOf()
- var mayHaveValidFullSignature = true
- var validFullSignature: String? = null
- val insns = resolveInstructions(annotation, targetClass, targetMethod).ifEmpty { return emptyList() }
- for (insn in insns) {
- if (insn.insn !is MethodInsnNode) return null
-
- // normalize return type so whole signature matches
- val desc = insn.insn.desc.replaceAfterLast(')', "V")
- if (index == null) {
- val validArgTypes = Type.getArgumentTypes(desc).mapTo(mutableListOf()) { it.descriptor }
- // remove duplicates completely, they are invalid
- val toRemove = validArgTypes.filter { e -> validArgTypes.count { it == e } > 1 }.toSet()
- validArgTypes.removeIf { toRemove.contains(it) }
- if (validArgTypes.isEmpty()) {
- return listOf()
- }
+ val argTypes = Type.getArgumentTypes(insn.desc)
- if (validSingleArgTypes.isEmpty()) {
- validSingleArgTypes.addAll(validArgTypes)
- } else {
- validSingleArgTypes.retainAll(validArgTypes.toSet())
- if (validSingleArgTypes.isEmpty()) {
- return listOf()
- }
- }
- } else {
- val singleArgType = Type.getArgumentTypes(desc).getOrNull(index)?.descriptor ?: return listOf()
- if (validSingleArgTypes.isEmpty()) {
- validSingleArgTypes += singleArgType
- } else {
- validSingleArgTypes.removeIf { it != singleArgType }
- if (validSingleArgTypes.isEmpty()) {
- return listOf()
- }
- }
- }
+ val validTypes = if (index == null) {
+ argTypes.groupingBy { it }.eachCount().asSequence().filter { it.value == 1 }.map { it.key }.toList()
+ } else {
+ listOfNotNull(argTypes.getOrNull(index))
+ }
- if (mayHaveValidFullSignature) {
- if (validFullSignature == null) {
- validFullSignature = desc
- } else {
- if (desc != validFullSignature) {
- validFullSignature = null
- mayHaveValidFullSignature = false
- }
- }
- }
+ if (validTypes.isEmpty()) {
+ return ExpectedSignatures.Invalid
}
// get the source method for parameter names
- val (bytecodeClass, bytecodeMethod) = (insns[0].insn as MethodInsnNode).fakeResolve()
- val sourceMethod = insns[0].target as? PsiMethod
+ val sourceMethod = MemberReference(
+ insn.name,
+ insn.desc,
+ insn.owner.replace('/', '.')
+ ).resolveMember(project) as PsiMethod?
val elementFactory = JavaPsiFacade.getElementFactory(annotation.project)
- return validSingleArgTypes.flatMap { type ->
- val paramList = sourceMethod?.parameterList
- val psiParameter = paramList?.parameters?.firstOrNull { it.type.descriptor == type }
- val psiType = psiParameter?.type ?: Type.getType(type).toPsiType(elementFactory, null)
- val singleSignature = MethodSignature(
- listOf(
- ParameterGroup(
- listOf(
- sanitizedParameter(psiType, psiParameter?.name),
- ),
- ),
- ),
- psiType,
- )
- if (validFullSignature != null) {
- val fullParamGroup = ParameterGroup(
- Type.getArgumentTypes(validFullSignature).withIndex().map { (index, argType) ->
- val psiParam = paramList?.let { bytecodeMethod.getParameter(bytecodeClass, index, it) }
- sanitizedParameter(
- psiParam?.type ?: argType.toPsiType(elementFactory),
- psiParam?.name,
- )
- },
- )
- listOf(
- singleSignature,
- MethodSignature(listOf(fullParamGroup), psiType),
+ val psiParams = argTypes.indices.map { index -> sourceMethod?.getBytecodeParameter(index) }
+ val paramOptions = validTypes.associateWithTo(linkedMapOf()) { type ->
+ val targetParam = psiParams[index ?: argTypes.indexOf(type)]
+ val psiType = targetParam?.type ?: type.toPsiType(elementFactory)
+ sanitizedParameter(psiType, targetParam?.name)
+ }
+ val fullParams = if (argTypes.size > 1) {
+ psiParams.zip(argTypes) { param, argType ->
+ sanitizedParameter(
+ param?.type ?: argType.toPsiType(elementFactory),
+ param?.name,
)
- } else {
- listOf(singleSignature)
}
- }
+ } else null
+ return ExpectedSignatures.Valid(
+ ModifierSignatures(
+ SequencedMap(paramOptions),
+ fullParams,
+ )
+ )
+ }
+
+ override fun suggestedMethodSignature(
+ annotation: PsiAnnotation,
+ targets: List
+ ): SuggestedSignature? {
+ return SuggestedSignature.modifier(
+ annotation,
+ expectedMethodSignatures(annotation, targets).collectSignatures() ?: return null,
+ )
}
override val mixinExtrasExpressionContextType = ExpressionContext.Type.MODIFY_ARG
diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyArgsHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyArgsHandler.kt
index 3ec63ae73..5d7541d64 100644
--- a/src/main/kotlin/platform/mixin/handlers/ModifyArgsHandler.kt
+++ b/src/main/kotlin/platform/mixin/handlers/ModifyArgsHandler.kt
@@ -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
@@ -20,8 +20,13 @@
package com.demonwav.mcdev.platform.mixin.handlers
+import com.demonwav.mcdev.platform.mixin.handlers.mixinextras.TargetInsn
+import com.demonwav.mcdev.platform.mixin.inspection.injector.BasicSignatures
+import com.demonwav.mcdev.platform.mixin.inspection.injector.ExpectedSignatures
import com.demonwav.mcdev.platform.mixin.inspection.injector.MethodSignature
-import com.demonwav.mcdev.platform.mixin.inspection.injector.ParameterGroup
+import com.demonwav.mcdev.platform.mixin.inspection.injector.SignatureSuggestion
+import com.demonwav.mcdev.platform.mixin.inspection.injector.SuggestedSignature
+import com.demonwav.mcdev.platform.mixin.util.ClassAndMethodNode
import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Classes.ARGS
import com.demonwav.mcdev.util.Parameter
import com.intellij.psi.JavaPsiFacade
@@ -33,7 +38,7 @@ import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.MethodInsnNode
import org.objectweb.asm.tree.MethodNode
-class ModifyArgsHandler : InjectorAnnotationHandler() {
+class ModifyArgsHandler : InsnInjectorAnnotationHandler() {
override fun isInsnAllowed(insn: AbstractInsnNode, decorations: Map): Boolean {
return insn is MethodInsnNode
}
@@ -44,21 +49,36 @@ class ModifyArgsHandler : InjectorAnnotationHandler() {
annotation: PsiAnnotation,
targetClass: ClassNode,
targetMethod: MethodNode,
- ): List {
+ targetInsn: TargetInsn,
+ ): ExpectedSignatures {
val argsType = JavaPsiFacade.getElementFactory(annotation.project)
.createTypeByFQClassName(ARGS, annotation.resolveScope)
- return listOf(
- MethodSignature(
- listOf(
- ParameterGroup(listOf(Parameter("args", argsType))),
- ParameterGroup(
- collectTargetMethodParameters(annotation.project, targetClass, targetMethod),
- required = ParameterGroup.RequiredLevel.OPTIONAL,
- isVarargs = true,
- ),
+ val shortParams = listOf(Parameter("args", argsType))
+ return ExpectedSignatures.Valid(
+ BasicSignatures(
+ MethodSignature(
+ shortParams,
+ PsiTypes.voidType(),
+ allowCoerceRequired = false,
),
- PsiTypes.voidType(),
- ),
+ MethodSignature(
+ shortParams + collectTargetMethodParameters(annotation.project, targetClass, targetMethod),
+ PsiTypes.voidType(),
+ allowCoerceRequired = false,
+ ),
+ )
+ )
+ }
+
+ override fun suggestedMethodSignature(
+ annotation: PsiAnnotation,
+ targets: List
+ ): SuggestedSignature {
+ val argsType = JavaPsiFacade.getElementFactory(annotation.project)
+ .createTypeByFQClassName(ARGS, annotation.resolveScope)
+ return SuggestedSignature(
+ listOf(SignatureSuggestion.Param("args", argsType)),
+ PsiTypes.voidType(),
)
}
diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt
index bc15541f0..d9efc18a9 100644
--- a/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt
+++ b/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt
@@ -22,27 +22,34 @@ package com.demonwav.mcdev.platform.mixin.handlers
import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.ConstantInjectionPoint
import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.InjectionPoint
-import com.demonwav.mcdev.platform.mixin.inspection.injector.MethodSignature
-import com.demonwav.mcdev.platform.mixin.inspection.injector.ParameterGroup
-import com.demonwav.mcdev.util.findAnnotations
-import com.intellij.openapi.project.Project
+import com.demonwav.mcdev.platform.mixin.handlers.mixinextras.TargetInsn
+import com.demonwav.mcdev.platform.mixin.inspection.injector.ExpectedSignatures
+import com.demonwav.mcdev.platform.mixin.inspection.injector.GeneralSignatures
+import com.demonwav.mcdev.platform.mixin.inspection.injector.SuggestedSignature
+import com.demonwav.mcdev.platform.mixin.inspection.injector.collectSignatures
+import com.demonwav.mcdev.platform.mixin.util.ClassAndMethodNode
+import com.demonwav.mcdev.platform.mixin.util.TypeKind
+import com.demonwav.mcdev.util.Parameter
+import com.demonwav.mcdev.util.sequencedMapOf
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.PsiAnnotation
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiManager
-import com.intellij.psi.PsiMethod
import com.intellij.psi.PsiType
import com.intellij.psi.PsiTypes
-import com.intellij.psi.util.parentOfType
import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext
import org.objectweb.asm.Opcodes
import org.objectweb.asm.Type
import org.objectweb.asm.tree.AbstractInsnNode
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.MethodNode
+import org.objectweb.asm.tree.TypeInsnNode
+
+class ModifyConstantHandler : InsnInjectorAnnotationHandler() {
+ private val constantInjectionPoint by lazy { InjectionPoint.byAtCode("CONSTANT") as ConstantInjectionPoint }
-class ModifyConstantHandler : InjectorAnnotationHandler() {
private val allowedOpcodes = setOf(
+ Opcodes.ACONST_NULL,
Opcodes.ICONST_M1,
Opcodes.ICONST_0,
Opcodes.ICONST_1,
@@ -64,128 +71,88 @@ class ModifyConstantHandler : InjectorAnnotationHandler() {
Opcodes.IFGE,
Opcodes.IFGT,
Opcodes.IFLE,
- Opcodes.CHECKCAST,
Opcodes.INSTANCEOF,
)
- private fun getConstantInfos(modifyConstant: PsiAnnotation): List? {
- val constants = modifyConstant.findDeclaredAttributeValue("constant")
- ?.findAnnotations()
- ?.takeIf { it.isNotEmpty() }
- ?: return null
- return constants.map { constant ->
- (InjectionPoint.byAtCode("CONSTANT") as ConstantInjectionPoint).getConstantInfo(constant) ?: return null
- }
- }
-
override fun getAtKey(annotation: PsiAnnotation) = "constant"
override fun expectedMethodSignature(
annotation: PsiAnnotation,
targetClass: ClassNode,
targetMethod: MethodNode,
- ): List {
- val constantInfos = getConstantInfos(annotation)
- if (constantInfos == null) {
- val method = annotation.parentOfType()
- ?: return emptyList()
- val returnType = method.returnType
- ?: return emptyList()
- val constantParamName = method.parameterList.getParameter(0)?.name ?: "constant"
- return listOf(
- MethodSignature(
- listOf(
- ParameterGroup(listOf(sanitizedParameter(returnType, constantParamName))),
- ParameterGroup(
- collectTargetMethodParameters(annotation.project, targetClass, targetMethod),
- isVarargs = true,
- required = ParameterGroup.RequiredLevel.OPTIONAL,
- ),
- ),
- returnType,
- )
+ targetInsn: TargetInsn,
+ ): ExpectedSignatures {
+ val targetParams = collectTargetMethodParameters(annotation.project, targetClass, targetMethod)
+ val cst = constantInjectionPoint.getTargetedConstant(targetInsn.insn) ?: return ExpectedSignatures.Invalid
+ return ExpectedSignatures.Valid(expectedSignatures(annotation, targetInsn.insn, cst, targetParams))
+ }
+
+ override fun suggestedMethodSignature(
+ annotation: PsiAnnotation,
+ targets: List
+ ): SuggestedSignature? {
+ return SuggestedSignature.general(
+ annotation,
+ expectedMethodSignatures(annotation, targets).collectSignatures() ?: return null
+ )
+ }
+
+ private fun expectedSignatures(
+ annotation: PsiAnnotation,
+ targetInsn: AbstractInsnNode,
+ cst: Any,
+ trailingParams: List,
+ ): GeneralSignatures {
+ val psiManager = PsiManager.getInstance(annotation.project)
+
+ return if (targetInsn is TypeInsnNode) {
+ GeneralSignatures(
+ makeTypeCheckParams(psiManager, annotation),
+ sequencedMapOf(
+ TypeKind.INT_LIKE to PsiTypes.booleanType(),
+ TypeKind.OBJECT to getClassType(psiManager, annotation),
+ ),
+ allowCoerce = false,
+ trailingParams,
+ )
+ } else {
+ makeSignatures(
+ getConstantType(annotation, cst)
+ ?: throw IllegalStateException("Unknown constant type: ${cst.javaClass.name}"),
+ trailingParams,
)
}
+ }
- val psiManager = PsiManager.getInstance(annotation.project)
- return constantInfos.asSequence()
- .distinctBy { it.constant?.javaClass }
- .flatMap {
- when (it.constant) {
- null -> sequenceOf(
- makeMethodSignature(annotation.project, targetClass, targetMethod, PsiType.getJavaLangObject(psiManager, annotation.resolveScope))
- )
- is Int -> sequenceOf(
- makeMethodSignature(annotation.project, targetClass, targetMethod, PsiTypes.intType()),
- makeMethodSignature(annotation.project, targetClass, targetMethod, PsiTypes.booleanType()),
- makeMethodSignature(annotation.project, targetClass, targetMethod, PsiTypes.byteType()),
- makeMethodSignature(annotation.project, targetClass, targetMethod, PsiTypes.charType()),
- makeMethodSignature(annotation.project, targetClass, targetMethod, PsiTypes.shortType()),
- )
- is Long -> sequenceOf(
- makeMethodSignature(annotation.project, targetClass, targetMethod, PsiTypes.longType())
- )
- is Float -> sequenceOf(
- makeMethodSignature(annotation.project, targetClass, targetMethod, PsiTypes.floatType())
- )
- is Double -> sequenceOf(
- makeMethodSignature(annotation.project, targetClass, targetMethod, PsiTypes.doubleType())
- )
- is String -> sequenceOf(
- makeMethodSignature(annotation.project, targetClass, targetMethod, PsiType.getJavaLangString(psiManager, annotation.resolveScope))
- )
- is Type -> sequenceOf(
- makeTypeCheckMethodSignature(annotation.project, psiManager, annotation, targetClass, targetMethod, getClassType(psiManager, annotation)),
- makeTypeCheckMethodSignature(annotation.project, psiManager, annotation, targetClass, targetMethod, PsiTypes.booleanType()),
- )
- else -> throw IllegalStateException("Unknown constant type: ${it.constant.javaClass.name}")
- }
- }
- .toList()
+ private fun getConstantType(context: PsiAnnotation, cst: Any) = when (cst) {
+ is Int -> PsiTypes.intType()
+ is Long -> PsiTypes.longType()
+ is Float -> PsiTypes.floatType()
+ is Double -> PsiTypes.doubleType()
+ is String -> PsiType.getJavaLangString(
+ PsiManager.getInstance(context.project),
+ context.resolveScope,
+ )
+ is Type -> getClassType(
+ PsiManager.getInstance(context.project),
+ context,
+ )
+
+ else -> null
}
- private fun makeMethodSignature(
- project: Project,
- targetClass: ClassNode,
- targetMethod: MethodNode,
- type: PsiType,
- ): MethodSignature {
- return MethodSignature(
- listOf(
- ParameterGroup(listOf(sanitizedParameter(type, "constant"))),
- ParameterGroup(
- collectTargetMethodParameters(project, targetClass, targetMethod),
- isVarargs = true,
- required = ParameterGroup.RequiredLevel.OPTIONAL,
- ),
- ),
+ private fun makeSignatures(type: PsiType, trailingParams: List): GeneralSignatures {
+ return GeneralSignatures(
+ listOf(sanitizedParameter(type, "constant")),
type,
+ trailingParams,
)
}
- private fun makeTypeCheckMethodSignature(
- project: Project,
- psiManager: PsiManager,
- context: PsiElement,
- targetClass: ClassNode,
- targetMethod: MethodNode,
- returnType: PsiType,
- ): MethodSignature {
- return MethodSignature(
- listOf(
- ParameterGroup(
- listOf(
- sanitizedParameter(PsiType.getJavaLangObject(psiManager, context.resolveScope), "instance"),
- sanitizedParameter(getClassType(psiManager, context), "type"),
- )
- ),
- ParameterGroup(
- collectTargetMethodParameters(project, targetClass, targetMethod),
- isVarargs = true,
- required = ParameterGroup.RequiredLevel.OPTIONAL,
- ),
- ),
- returnType,
+ private fun makeTypeCheckParams(psiManager: PsiManager, context: PsiElement): List {
+ return listOf(
+ sanitizedParameter(PsiType.getJavaLangObject(psiManager, context.resolveScope), "instance"),
+ sanitizedParameter(getClassType(psiManager, context), "type"),
)
}
diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt
index 11284ecfc..ee8c26b2a 100644
--- a/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt
+++ b/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt
@@ -20,14 +20,17 @@
package com.demonwav.mcdev.platform.mixin.handlers
-import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.AbstractLoadInjectionPoint
import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.CollectVisitor
-import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.InjectionPoint
-import com.demonwav.mcdev.platform.mixin.inspection.injector.MethodSignature
-import com.demonwav.mcdev.platform.mixin.inspection.injector.ParameterGroup
+import com.demonwav.mcdev.platform.mixin.handlers.mixinextras.TargetInsn
+import com.demonwav.mcdev.platform.mixin.inspection.injector.ExpectedSignatures
+import com.demonwav.mcdev.platform.mixin.inspection.injector.ModifierSignatures
+import com.demonwav.mcdev.platform.mixin.inspection.injector.SuggestedSignature
+import com.demonwav.mcdev.platform.mixin.inspection.injector.collectSignatures
+import com.demonwav.mcdev.platform.mixin.util.ClassAndMethodNode
import com.demonwav.mcdev.platform.mixin.util.LocalInfo
import com.demonwav.mcdev.platform.mixin.util.toPsiType
-import com.demonwav.mcdev.util.constantStringValue
+import com.demonwav.mcdev.util.Parameter
+import com.demonwav.mcdev.util.SequencedMap
import com.demonwav.mcdev.util.findContainingMethod
import com.demonwav.mcdev.util.findModule
import com.intellij.psi.JavaPsiFacade
@@ -37,53 +40,49 @@ import org.objectweb.asm.Type
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.MethodNode
-class ModifyVariableHandler : InjectorAnnotationHandler() {
+class ModifyVariableHandler : InsnInjectorAnnotationHandler() {
override fun expectedMethodSignature(
annotation: PsiAnnotation,
targetClass: ClassNode,
targetMethod: MethodNode,
- ): List? {
- val module = annotation.findModule() ?: return null
+ targetInsn: TargetInsn,
+ ): ExpectedSignatures {
+ val module = annotation.findModule() ?: return ExpectedSignatures.Unknown
+ val targetParams = collectTargetMethodParameters(annotation.project, targetClass, targetMethod)
- val at = annotation.findAttributeValue("at") as? PsiAnnotation
- val atCode = at?.findAttributeValue("value")?.constantStringValue
- val isLoadStore = atCode != null && InjectionPoint.byAtCode(atCode) is AbstractLoadInjectionPoint
- val mode = if (isLoadStore) CollectVisitor.Mode.COMPLETION else CollectVisitor.Mode.RESOLUTION
- val targets = resolveInstructions(annotation, targetClass, targetMethod, mode)
-
- val targetParamsGroup = ParameterGroup(
- collectTargetMethodParameters(annotation.project, targetClass, targetMethod),
- required = ParameterGroup.RequiredLevel.OPTIONAL,
- isVarargs = true,
- )
-
- val method = annotation.findContainingMethod() ?: return null
+ val method = annotation.findContainingMethod() ?: return ExpectedSignatures.Unknown
val localType = method.parameterList.getParameter(0)?.type
val info = LocalInfo.fromAnnotation(localType, annotation)
val elementFactory = JavaPsiFacade.getElementFactory(annotation.project)
- val seenParams = mutableSetOf()
- val result = mutableListOf()
- for (insn in targets) {
- val matchedLocals = info.matchLocals(
- module, targetClass, targetMethod, insn.insn,
- CollectVisitor.Mode.COMPLETION, matchType = false
- ) ?: continue
- for (local in matchedLocals) {
- if (seenParams.add(local.desc + local.name)) {
- val localType = Type.getType(local.desc).toPsiType(elementFactory)
- result += MethodSignature(
- listOf(
- ParameterGroup(listOf(sanitizedParameter(localType, local.name, local.isNamed))),
- targetParamsGroup,
- ),
- localType,
- )
- }
+ val result = linkedMapOf()
+ val matchedLocals = info.matchLocals(
+ module, targetClass, targetMethod, targetInsn.insn,
+ CollectVisitor.Mode.SUGGESTION, matchType = false
+ ).orEmpty()
+ for (local in matchedLocals) {
+ val type = Type.getType(local.desc ?: continue)
+ result.computeIfAbsent(type) {
+ sanitizedParameter(type.toPsiType(elementFactory), local.name, local.isNamed)
}
}
- return result
+ return ExpectedSignatures.Valid(
+ ModifierSignatures(
+ SequencedMap(result),
+ trailingParams = targetParams,
+ )
+ )
+ }
+
+ override fun suggestedMethodSignature(
+ annotation: PsiAnnotation,
+ targets: List
+ ): SuggestedSignature? {
+ return SuggestedSignature.modifier(
+ annotation,
+ expectedMethodSignatures(annotation, targets).collectSignatures() ?: return null,
+ )
}
override val isShiftAlwaysDiscouraged = false
diff --git a/src/main/kotlin/platform/mixin/handlers/RedirectInjectorHandler.kt b/src/main/kotlin/platform/mixin/handlers/RedirectInjectorHandler.kt
index 5536a39dc..0dddfd208 100644
--- a/src/main/kotlin/platform/mixin/handlers/RedirectInjectorHandler.kt
+++ b/src/main/kotlin/platform/mixin/handlers/RedirectInjectorHandler.kt
@@ -21,16 +21,22 @@
package com.demonwav.mcdev.platform.mixin.handlers
import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.NewInsnInjectionPoint
-import com.demonwav.mcdev.platform.mixin.inspection.injector.MethodSignature
-import com.demonwav.mcdev.platform.mixin.inspection.injector.ParameterGroup
+import com.demonwav.mcdev.platform.mixin.handlers.mixinextras.TargetInsn
+import com.demonwav.mcdev.platform.mixin.inspection.injector.ExpectedSignatures
+import com.demonwav.mcdev.platform.mixin.inspection.injector.GeneralSignatures
+import com.demonwav.mcdev.platform.mixin.inspection.injector.SuggestedSignature
+import com.demonwav.mcdev.platform.mixin.inspection.injector.collectSignatures
import com.demonwav.mcdev.platform.mixin.util.AsmDfaUtil
+import com.demonwav.mcdev.platform.mixin.util.ClassAndMethodNode
import com.demonwav.mcdev.platform.mixin.util.FieldTargetMember
import com.demonwav.mcdev.platform.mixin.util.MethodTargetMember
+import com.demonwav.mcdev.platform.mixin.util.TypeKind
import com.demonwav.mcdev.platform.mixin.util.getGenericSignature
import com.demonwav.mcdev.platform.mixin.util.getGenericType
import com.demonwav.mcdev.platform.mixin.util.toPsiType
import com.demonwav.mcdev.util.MemberReference
import com.demonwav.mcdev.util.Parameter
+import com.demonwav.mcdev.util.sequencedMapOf
import com.demonwav.mcdev.util.toJavaIdentifier
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.PsiAnnotation
@@ -50,7 +56,7 @@ import org.objectweb.asm.tree.MethodInsnNode
import org.objectweb.asm.tree.MethodNode
import org.objectweb.asm.tree.TypeInsnNode
-class RedirectInjectorHandler : InjectorAnnotationHandler() {
+class RedirectInjectorHandler : InsnInjectorAnnotationHandler() {
private fun getRedirectType(insn: AbstractInsnNode): RedirectType? {
return when (insn) {
is FieldInsnNode -> {
@@ -88,37 +94,43 @@ class RedirectInjectorHandler : InjectorAnnotationHandler() {
annotation: PsiAnnotation,
targetClass: ClassNode,
targetMethod: MethodNode,
- ): List? {
- val insns = resolveInstructions(annotation, targetClass, targetMethod).ifEmpty { return emptyList() }
- return getRedirectType(insns[0].insn)?.expectedMethodSignature(
+ targetInsn: TargetInsn,
+ ): ExpectedSignatures {
+ val extraParams = collectTargetMethodParameters(annotation.project, targetClass, targetMethod)
+ val redirectType = getRedirectType(targetInsn.insn) ?: return ExpectedSignatures.Invalid
+ return redirectType.expectedMethodSignature(
annotation,
targetClass,
targetMethod,
- insns.map { it.insn },
- )?.map { (paramGroups, returnType) ->
- // add a parameter group for capturing the target method parameters
- val extraGroup = ParameterGroup(
- collectTargetMethodParameters(annotation.project, targetClass, targetMethod),
- required = ParameterGroup.RequiredLevel.OPTIONAL,
- isVarargs = true,
- )
- MethodSignature(paramGroups + extraGroup, returnType)
- }
+ targetInsn.insn,
+ extraParams,
+ )?.let(ExpectedSignatures<*>::Valid) ?: ExpectedSignatures.Invalid
}
- override val allowCoerce = true
+ override fun suggestedMethodSignature(
+ annotation: PsiAnnotation,
+ targets: List
+ ): SuggestedSignature? {
+ return SuggestedSignature.general(
+ annotation,
+ expectedMethodSignatures(annotation, targets).collectSignatures() ?: return null
+ )
+ }
override val mixinExtrasExpressionContextType = ExpressionContext.Type.REDIRECT
private interface RedirectType {
+ val allowCoerce: Boolean get() = true
+
fun isInsnAllowed(node: AbstractInsnNode) = true
fun expectedMethodSignature(
annotation: PsiAnnotation,
targetClass: ClassNode,
targetMethod: MethodNode,
- insns: List,
- ): List?
+ insn: AbstractInsnNode,
+ trailingParams: List,
+ ): GeneralSignatures?
}
private abstract class FieldAccess : RedirectType {
@@ -144,31 +156,22 @@ class RedirectInjectorHandler : InjectorAnnotationHandler() {
annotation: PsiAnnotation,
targetClass: ClassNode,
targetMethod: MethodNode,
- insns: List,
- ): List {
- val firstMatch = insns.first() as FieldInsnNode
- val isValid = insns.all {
- val insn = it as? FieldInsnNode ?: return@all false
- if (insn.opcode != firstMatch.opcode) return@all false
- if (insn.opcode == Opcodes.GETFIELD && insn.owner != firstMatch.owner) return@all false
- insn.name == firstMatch.name && insn.desc == firstMatch.desc
- }
- if (!isValid) {
- return emptyList()
- }
+ insn: AbstractInsnNode,
+ trailingParams: List,
+ ): GeneralSignatures {
+ insn as FieldInsnNode
- val (elementFactory, fieldType) = determineFieldType(firstMatch, annotation)
+ val (elementFactory, fieldType) = determineFieldType(insn, annotation)
val parameters = mutableListOf()
- if (firstMatch.opcode == Opcodes.GETFIELD) {
- parameters += Parameter("instance", Type.getObjectType(firstMatch.owner).toPsiType(elementFactory))
+ if (insn.opcode == Opcodes.GETFIELD) {
+ parameters += Parameter("instance", Type.getObjectType(insn.owner).toPsiType(elementFactory))
}
- return listOf(
- MethodSignature(
- listOf(ParameterGroup(parameters)),
- fieldType,
- ),
+ return GeneralSignatures(
+ parameters,
+ fieldType,
+ trailingParams,
)
}
}
@@ -178,32 +181,23 @@ class RedirectInjectorHandler : InjectorAnnotationHandler() {
annotation: PsiAnnotation,
targetClass: ClassNode,
targetMethod: MethodNode,
- insns: List,
- ): List {
- val firstMatch = insns.first() as FieldInsnNode
- val isValid = insns.all {
- val insn = it as? FieldInsnNode ?: return@all false
- if (insn.opcode != firstMatch.opcode) return@all false
- if (insn.opcode == Opcodes.PUTFIELD && insn.owner != firstMatch.owner) return@all false
- insn.name == firstMatch.name && insn.desc == firstMatch.desc
- }
- if (!isValid) {
- return emptyList()
- }
+ insn: AbstractInsnNode,
+ trailingParams: List,
+ ): GeneralSignatures {
+ insn as FieldInsnNode
- val (elementFactory, fieldType) = determineFieldType(firstMatch, annotation)
+ val (elementFactory, fieldType) = determineFieldType(insn, annotation)
val parameters = mutableListOf()
- if (firstMatch.opcode == Opcodes.PUTFIELD) {
- parameters += Parameter("instance", Type.getObjectType(firstMatch.owner).toPsiType(elementFactory))
+ if (insn.opcode == Opcodes.PUTFIELD) {
+ parameters += Parameter("instance", Type.getObjectType(insn.owner).toPsiType(elementFactory))
}
parameters += Parameter("value", fieldType)
- return listOf(
- MethodSignature(
- listOf(ParameterGroup(parameters)),
- PsiTypes.voidType(),
- ),
+ return GeneralSignatures(
+ parameters,
+ PsiTypes.voidType(),
+ trailingParams,
)
}
}
@@ -217,25 +211,15 @@ class RedirectInjectorHandler : InjectorAnnotationHandler() {
annotation: PsiAnnotation,
targetClass: ClassNode,
targetMethod: MethodNode,
- insns: List,
- ): List {
- val firstMatch = insns.first() as MethodInsnNode
- val isValid = insns.all {
- val insn = it as? MethodInsnNode ?: return@all false
- if ((insn.opcode == Opcodes.INVOKESTATIC) != (firstMatch.opcode == Opcodes.INVOKESTATIC)) {
- return@all false
- }
- if (insn.opcode != Opcodes.INVOKESTATIC && insn.owner != firstMatch.owner) return@all false
- insn.name == firstMatch.name && insn.desc == firstMatch.desc
- }
- if (!isValid) {
- return emptyList()
- }
+ insn: AbstractInsnNode,
+ trailingParams: List,
+ ): GeneralSignatures {
+ insn as MethodInsnNode
val elementFactory = JavaPsiFacade.getElementFactory(annotation.project)
val sourceClassAndMethod = (
- MemberReference(firstMatch.name, firstMatch.desc, firstMatch.owner.replace('/', '.'))
+ MemberReference(insn.name, insn.desc, insn.owner.replace('/', '.'))
.resolveAsm(annotation.project) as? MethodTargetMember
)?.classAndMethod
val signature = sourceClassAndMethod?.method?.getGenericSignature(
@@ -244,8 +228,8 @@ class RedirectInjectorHandler : InjectorAnnotationHandler() {
)
val parameters = mutableListOf()
- if (firstMatch.opcode != Opcodes.INVOKESTATIC) {
- parameters += Parameter("instance", Type.getObjectType(firstMatch.owner).toPsiType(elementFactory))
+ if (insn.opcode != Opcodes.INVOKESTATIC) {
+ parameters += Parameter("instance", Type.getObjectType(insn.owner).toPsiType(elementFactory))
}
val sortedLocals = sourceClassAndMethod?.method?.localVariables?.sortedBy { it.index }
@@ -254,20 +238,24 @@ class RedirectInjectorHandler : InjectorAnnotationHandler() {
.asSequence()
.withIndex()
.mapTo(parameters) { (index, type) ->
- val i = if (firstMatch.opcode == Opcodes.INVOKESTATIC) index else index + 1
+ val i = if (insn.opcode == Opcodes.INVOKESTATIC) index else index + 1
val name = sortedLocals?.getOrNull(i)?.name?.toJavaIdentifier()
sanitizedParameter(type, name, name != null)
}
} else {
- Type.getArgumentTypes(firstMatch.desc).withIndex().mapTo(parameters) { (index, type) ->
- val i = if (firstMatch.opcode == Opcodes.INVOKESTATIC) index else index + 1
+ Type.getArgumentTypes(insn.desc).withIndex().mapTo(parameters) { (index, type) ->
+ val i = if (insn.opcode == Opcodes.INVOKESTATIC) index else index + 1
val name = sortedLocals?.getOrNull(i)?.name?.toJavaIdentifier()
sanitizedParameter(type.toPsiType(elementFactory), name, name != null)
}
}
- val returnType = signature?.first ?: Type.getReturnType(firstMatch.desc).toPsiType(elementFactory)
- return listOf(MethodSignature(listOf(ParameterGroup(parameters)), returnType))
+ val returnType = signature?.first ?: Type.getReturnType(insn.desc).toPsiType(elementFactory)
+ return GeneralSignatures(
+ parameters,
+ returnType,
+ trailingParams,
+ )
}
}
@@ -276,13 +264,10 @@ class RedirectInjectorHandler : InjectorAnnotationHandler() {
annotation: PsiAnnotation,
targetClass: ClassNode,
targetMethod: MethodNode,
- insns: List,
- ): List? {
- val firstMatch = insns.first()
- val isValid = insns.all { it.opcode == firstMatch.opcode }
- if (!isValid) return emptyList()
-
- val arrayType = AsmDfaUtil.getStackType(annotation.project, targetClass, targetMethod, firstMatch, 0)
+ insn: AbstractInsnNode,
+ trailingParams: List,
+ ): GeneralSignatures? {
+ val arrayType = AsmDfaUtil.getStackType(annotation.project, targetClass, targetMethod, insn, 0)
?: return null
if (arrayType.sort != Type.ARRAY) {
return null
@@ -290,17 +275,10 @@ class RedirectInjectorHandler : InjectorAnnotationHandler() {
val elementFactory = JavaPsiFacade.getElementFactory(annotation.project)
- return listOf(
- MethodSignature(
- listOf(
- ParameterGroup(
- listOf(
- Parameter("array", arrayType.toPsiType(elementFactory)),
- ),
- ),
- ),
- PsiTypes.intType(),
- ),
+ return GeneralSignatures(
+ listOf(Parameter("array", arrayType.toPsiType(elementFactory))),
+ PsiTypes.intType(),
+ trailingParams,
)
}
}
@@ -310,13 +288,10 @@ class RedirectInjectorHandler : InjectorAnnotationHandler() {
annotation: PsiAnnotation,
targetClass: ClassNode,
targetMethod: MethodNode,
- insns: List,
- ): List? {
- val firstMatch = insns.first()
- val isValid = insns.all { it.opcode == firstMatch.opcode }
- if (!isValid) return emptyList()
-
- val arrayType = AsmDfaUtil.getStackType(annotation.project, targetClass, targetMethod, firstMatch, 1)
+ insn: AbstractInsnNode,
+ trailingParams: List,
+ ): GeneralSignatures? {
+ val arrayType = AsmDfaUtil.getStackType(annotation.project, targetClass, targetMethod, insn, 1)
?: return null
if (arrayType.sort != Type.ARRAY) {
return null
@@ -325,18 +300,13 @@ class RedirectInjectorHandler : InjectorAnnotationHandler() {
val elementFactory = JavaPsiFacade.getElementFactory(annotation.project)
val psiArrayType = arrayType.toPsiType(elementFactory) as PsiArrayType
- return listOf(
- MethodSignature(
- listOf(
- ParameterGroup(
- listOf(
- Parameter("array", psiArrayType),
- Parameter("index", PsiTypes.intType()),
- ),
- ),
- ),
- psiArrayType.componentType,
+ return GeneralSignatures(
+ listOf(
+ Parameter("array", psiArrayType),
+ Parameter("index", PsiTypes.intType()),
),
+ psiArrayType.componentType,
+ trailingParams,
)
}
}
@@ -346,13 +316,10 @@ class RedirectInjectorHandler : InjectorAnnotationHandler() {
annotation: PsiAnnotation,
targetClass: ClassNode,
targetMethod: MethodNode,
- insns: List,
- ): List? {
- val firstMatch = insns.first()
- val isValid = insns.all { it.opcode == firstMatch.opcode }
- if (!isValid) return emptyList()
-
- val arrayType = AsmDfaUtil.getStackType(annotation.project, targetClass, targetMethod, firstMatch, 2)
+ insn: AbstractInsnNode,
+ trailingParams: List,
+ ): GeneralSignatures? {
+ val arrayType = AsmDfaUtil.getStackType(annotation.project, targetClass, targetMethod, insn, 2)
?: return null
if (arrayType.sort != Type.ARRAY) {
return null
@@ -361,19 +328,14 @@ class RedirectInjectorHandler : InjectorAnnotationHandler() {
val elementFactory = JavaPsiFacade.getElementFactory(annotation.project)
val psiArrayType = arrayType.toPsiType(elementFactory) as PsiArrayType
- return listOf(
- MethodSignature(
- listOf(
- ParameterGroup(
- listOf(
- Parameter("array", psiArrayType),
- Parameter("index", PsiTypes.intType()),
- Parameter("value", psiArrayType.componentType),
- ),
- ),
- ),
- PsiTypes.voidType(),
+ return GeneralSignatures(
+ listOf(
+ Parameter("array", psiArrayType),
+ Parameter("index", PsiTypes.intType()),
+ Parameter("value", psiArrayType.componentType),
),
+ PsiTypes.voidType(),
+ trailingParams,
)
}
}
@@ -387,58 +349,56 @@ class RedirectInjectorHandler : InjectorAnnotationHandler() {
annotation: PsiAnnotation,
targetClass: ClassNode,
targetMethod: MethodNode,
- insns: List,
- ): List {
- val firstMatch = insns.first() as TypeInsnNode
- val isValid = insns.all {
- val insn = it as? TypeInsnNode ?: return@all false
- insn.opcode == Opcodes.NEW && insn.desc == firstMatch.desc
- }
- if (!isValid) {
- return emptyList()
- }
+ insn: AbstractInsnNode,
+ trailingParams: List,
+ ): GeneralSignatures? {
+ insn as TypeInsnNode
val elementFactory = JavaPsiFacade.getElementFactory(annotation.project)
- val constructedType = Type.getObjectType(firstMatch.desc).toPsiType(elementFactory)
+ val constructedType = Type.getObjectType(insn.desc).toPsiType(elementFactory)
return Method.expectedMethodSignature(
annotation,
targetClass,
targetMethod,
- insns.mapNotNull {
- NewInsnInjectionPoint.Util.findInitCall(it as TypeInsnNode)
- },
- ).map { (paramGroups, _) ->
+ NewInsnInjectionPoint.Util.findInitCall(insn) ?: return null,
+ trailingParams,
+ ).let { sig ->
// drop the instance parameter, return the constructed type
- MethodSignature(listOf(ParameterGroup(paramGroups[0].parameters.drop(1))), constructedType)
+ sig.copy(
+ params = sig.params.drop(1),
+ returnTypeOptions = sequencedMapOf(TypeKind.OBJECT to constructedType),
+ )
}
}
}
private object InstanceOf : RedirectType {
+ override val allowCoerce: Boolean get() = false
+
override fun expectedMethodSignature(
annotation: PsiAnnotation,
targetClass: ClassNode,
targetMethod: MethodNode,
- insns: List,
- ): List {
- val firstMatch = insns.first()
- val isValid = insns.all { it.opcode == firstMatch.opcode }
- if (!isValid) return emptyList()
-
+ insn: AbstractInsnNode,
+ trailingParams: List,
+ ): GeneralSignatures {
val psiManager = PsiManager.getInstance(annotation.project)
val elementFactory = JavaPsiFacade.getElementFactory(annotation.project)
val objectType = PsiType.getJavaLangObject(psiManager, annotation.resolveScope)
val classType = elementFactory.createTypeFromText("java.lang.Class>", annotation)
- val parameters = ParameterGroup(
- listOf(
- Parameter("instance", objectType),
- Parameter("type", classType),
- ),
+ val parameters = listOf(
+ Parameter("instance", objectType),
+ Parameter("type", classType),
)
- return listOf(
- MethodSignature(listOf(parameters), PsiTypes.booleanType()),
- MethodSignature(listOf(parameters), classType),
+ return GeneralSignatures(
+ parameters,
+ sequencedMapOf(
+ TypeKind.INT_LIKE to PsiTypes.booleanType(),
+ TypeKind.OBJECT to classType,
+ ),
+ allowCoerce = false,
+ trailingParams,
)
}
}
diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt
index 36ebe7060..cc7497965 100644
--- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt
+++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt
@@ -20,7 +20,7 @@
package com.demonwav.mcdev.platform.mixin.handlers.injectionPoint
-import com.demonwav.mcdev.platform.mixin.handlers.InjectorAnnotationHandler
+import com.demonwav.mcdev.platform.mixin.handlers.InsnInjectorAnnotationHandler
import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler
import com.demonwav.mcdev.platform.mixin.handlers.desugar.DesugarContext
import com.demonwav.mcdev.platform.mixin.handlers.desugar.DesugarUtil
@@ -297,7 +297,7 @@ class AtResolver(
val targetAttr = at.findAttributeValue("target")
val target = targetAttr?.let { parseMixinSelector(it) }
val injector = findInjectorAnnotation(at, skipThroughSlice = false)?.let(MixinAnnotationHandler::forMixinAnnotation)
- as? InjectorAnnotationHandler
+ as? InsnInjectorAnnotationHandler
// Collect all possible targets
fun doCollectVariants(injectionPoint: InjectionPoint): List {
diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantInjectionPoint.kt
index 516055526..168943ad6 100644
--- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantInjectionPoint.kt
+++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantInjectionPoint.kt
@@ -50,6 +50,7 @@ import com.intellij.psi.PsiLiteralExpression
import com.intellij.psi.PsiSwitchLabelStatementBase
import com.intellij.psi.util.PsiUtil
import com.intellij.util.ArrayUtilRt
+import java.util.EnumSet
import java.util.Locale
import org.objectweb.asm.Opcodes
import org.objectweb.asm.Type
@@ -195,6 +196,10 @@ class ConstantInjectionPoint : InjectionPoint() {
return null
}
+ fun getTargetedConstant(insn: AbstractInsnNode) =
+ insn.computeConstantValue(EnumSet.allOf(ExpandCondition::class.java))
+ ?.let { if (it === NullSentinel) null else it }
+
class ConstantInfo(val constant: Any?, val expandConditions: Set)
enum class ExpandCondition(vararg val opcodes: Int) {
diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantStringMethodInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantStringMethodInjectionPoint.kt
index 9c544f5c4..4d75baeec 100644
--- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantStringMethodInjectionPoint.kt
+++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantStringMethodInjectionPoint.kt
@@ -148,7 +148,7 @@ class ConstantStringMethodInjectionPoint : AbstractMethodInjectionPoint() {
targetClass: ClassNode,
mode: CollectVisitor.Mode,
): CollectVisitor? {
- if (mode == CollectVisitor.Mode.COMPLETION) {
+ if (!mode.assumeCorrectAt) {
return MyCollectVisitor(mode, at.project, MemberInfo(), null)
}
return target?.let { MyCollectVisitor(mode, at.project, it, AtResolver.getArgs(at)["ldc"]) }
@@ -233,7 +233,7 @@ class ConstantStringMethodInjectionPoint : AbstractMethodInjectionPoint() {
// must take a string and return void
if (insn.desc != "(Ljava/lang/String;)V") return
- if (mode != Mode.COMPLETION) {
+ if (mode.assumeCorrectAt) {
// ensure we match the target
if (!selector.matchMethod(insn.owner, insn.name, insn.desc)) {
return
diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/FieldInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/FieldInjectionPoint.kt
index b81d3fffa..a8d49927a 100644
--- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/FieldInjectionPoint.kt
+++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/FieldInjectionPoint.kt
@@ -97,7 +97,7 @@ class FieldInjectionPoint : QualifiedInjectionPoint() {
targetClass: ClassNode,
mode: CollectVisitor.Mode,
): CollectVisitor? {
- if (mode == CollectVisitor.Mode.COMPLETION) {
+ if (!mode.assumeCorrectAt) {
return MyCollectVisitor(mode, at.project, MemberInfo(), -1, null, 8)
}
val opcode = (at.findDeclaredAttributeValue("opcode")?.constantValue as? Int)
@@ -205,7 +205,7 @@ class FieldInjectionPoint : QualifiedInjectionPoint() {
val insns = methodNode.instructions ?: return@sequence
for (insn in insns) {
if (insn !is FieldInsnNode) continue
- if (mode != Mode.COMPLETION) {
+ if (mode.assumeCorrectAt) {
if (opcode != -1 && opcode != insn.opcode) {
continue
}
diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.kt
index 39e9ac1f8..5ab7e5c75 100644
--- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.kt
+++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.kt
@@ -161,7 +161,7 @@ abstract class InjectionPoint {
// filters have passed, and the specifier acts on the result of them.
// Separately, these happen to also be the filters that we don't want to apply during completion, so that all
// results are shown.
- if (mode != CollectVisitor.Mode.COMPLETION) {
+ if (mode.assumeCorrectAt) {
addOrdinalFilter(at, targetClass, collectVisitor)
addQuantifierFilter(at, targetClass, collectVisitor)
addSpecifierFilter(at, targetClass, collectVisitor, defaultSpecifier)
@@ -458,7 +458,11 @@ abstract class CollectVisitor(protected val mode: Mode) {
val index: Int get() = sourceLocationInfo.index
}
- enum class Mode { RESOLUTION, COMPLETION }
+ enum class Mode(val assumeCorrectSignature: Boolean, val assumeCorrectAt: Boolean) {
+ RESOLUTION(true, true),
+ COMPLETION(false, false),
+ SUGGESTION(false, true),
+ }
}
fun nodeMatchesSelector(
@@ -467,7 +471,7 @@ fun nodeMatchesSelector(
selector: MixinSelector,
project: Project,
): PsiMethod? {
- if (mode != CollectVisitor.Mode.COMPLETION) {
+ if (mode.assumeCorrectAt) {
if (!selector.matchMethod(insn.owner, insn.name, insn.desc)) {
return null
}
diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeAssignInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeAssignInjectionPoint.kt
index 453edb749..3b55657fb 100644
--- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeAssignInjectionPoint.kt
+++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeAssignInjectionPoint.kt
@@ -105,7 +105,7 @@ class InvokeAssignInjectionPoint : AbstractMethodInjectionPoint() {
val fuzz = args["fuzz"]?.toIntOrNull()?.coerceAtLeast(1) ?: 1
val skip = args["skip"]?.let { parseSkip(it) } ?: Const.DEFAULT_SKIP
- if (mode == CollectVisitor.Mode.COMPLETION) {
+ if (!mode.assumeCorrectAt) {
return MyCollectVisitor(mode, at.project, MemberInfo(), fuzz, skip)
}
return target?.let { MyCollectVisitor(mode, at.project, it, fuzz, skip) }
diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeInjectionPoint.kt
index ea5a94aac..894f78eb7 100644
--- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeInjectionPoint.kt
+++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeInjectionPoint.kt
@@ -64,7 +64,7 @@ class InvokeInjectionPoint : AbstractMethodInjectionPoint() {
targetClass: ClassNode,
mode: CollectVisitor.Mode,
): CollectVisitor? {
- if (mode == CollectVisitor.Mode.COMPLETION) {
+ if (!mode.assumeCorrectAt) {
return MyCollectVisitor(mode, at.project, MemberInfo())
}
return target?.let { MyCollectVisitor(mode, at.project, it) }
diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/LoadInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/LoadInjectionPoint.kt
index 5b78a299c..44be2fc54 100644
--- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/LoadInjectionPoint.kt
+++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/LoadInjectionPoint.kt
@@ -53,16 +53,17 @@ import org.objectweb.asm.tree.MethodNode
import org.objectweb.asm.tree.VarInsnNode
abstract class AbstractLoadInjectionPoint(private val store: Boolean) : InjectionPoint() {
- private fun getModifyVariableInfo(at: PsiAnnotation, mode: CollectVisitor.Mode?): LocalInfo? {
+ private fun getModifyVariableInfo(at: PsiAnnotation, mode: CollectVisitor.Mode): LocalInfo? {
val modifyVariable = at.parentOfType() ?: return null
if (!modifyVariable.hasQualifiedName(MODIFY_VARIABLE)) {
return null
}
val method = modifyVariable.findContainingMethod() ?: return null
- val localType = method.parameterList.getParameter(0)?.type
- if (localType == null && mode != CollectVisitor.Mode.COMPLETION) {
- return null
+ var localType = method.parameterList.getParameter(0)?.type
+ when {
+ !mode.assumeCorrectSignature -> localType = null
+ localType == null -> return null
}
return LocalInfo.fromAnnotation(localType, modifyVariable)
}
@@ -90,7 +91,7 @@ abstract class AbstractLoadInjectionPoint(private val store: Boolean) : Injectio
target: MixinSelector?,
targetClass: PsiClass,
): NavigationVisitor? {
- val info = getModifyVariableInfo(at, null) ?: return null
+ val info = getModifyVariableInfo(at, CollectVisitor.Mode.RESOLUTION) ?: return null
return MyNavigationVisitor(info, store)
}
diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/NewInsnInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/NewInsnInjectionPoint.kt
index 6b18fd55d..bf365e44b 100644
--- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/NewInsnInjectionPoint.kt
+++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/NewInsnInjectionPoint.kt
@@ -27,9 +27,7 @@ import com.demonwav.mcdev.platform.mixin.util.MemberInfo
import com.demonwav.mcdev.platform.mixin.util.MethodTargetMember
import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Annotations.AT
import com.demonwav.mcdev.platform.mixin.util.findClassNodeByPsiClass
-import com.demonwav.mcdev.platform.mixin.util.findMethod
import com.demonwav.mcdev.platform.mixin.util.findMethods
-import com.demonwav.mcdev.util.MemberReference
import com.demonwav.mcdev.util.Quantifier
import com.demonwav.mcdev.util.constantStringValue
import com.demonwav.mcdev.util.fullQualifiedName
@@ -104,7 +102,7 @@ class NewInsnInjectionPoint : InjectionPoint() {
targetClass: ClassNode,
mode: CollectVisitor.Mode,
): CollectVisitor? {
- if (mode == CollectVisitor.Mode.COMPLETION) {
+ if (!mode.assumeCorrectAt) {
return MyCollectVisitor(mode, at.project, MemberInfo())
}
return getTarget(at, target)?.let { MyCollectVisitor(mode, at.project, it) }
diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/MixinExtrasInjectorAnnotationHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/MixinExtrasInjectorAnnotationHandler.kt
index 1c3592e32..f8cf7d107 100644
--- a/src/main/kotlin/platform/mixin/handlers/mixinextras/MixinExtrasInjectorAnnotationHandler.kt
+++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/MixinExtrasInjectorAnnotationHandler.kt
@@ -20,10 +20,10 @@
package com.demonwav.mcdev.platform.mixin.handlers.mixinextras
-import com.demonwav.mcdev.platform.mixin.handlers.InjectorAnnotationHandler
+import com.demonwav.mcdev.platform.mixin.handlers.InsnInjectorAnnotationHandler
import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.NewInsnInjectionPoint
-import com.demonwav.mcdev.platform.mixin.inspection.injector.MethodSignature
-import com.demonwav.mcdev.platform.mixin.inspection.injector.ParameterGroup
+import com.demonwav.mcdev.platform.mixin.inspection.injector.ExpectedSignatures
+import com.demonwav.mcdev.platform.mixin.inspection.injector.MethodSignatures
import com.demonwav.mcdev.platform.mixin.util.FieldTargetMember
import com.demonwav.mcdev.platform.mixin.util.MethodTargetMember
import com.demonwav.mcdev.platform.mixin.util.getGenericParameterTypes
@@ -49,7 +49,7 @@ import org.objectweb.asm.tree.MethodInsnNode
import org.objectweb.asm.tree.MethodNode
import org.objectweb.asm.tree.TypeInsnNode
-abstract class MixinExtrasInjectorAnnotationHandler : InjectorAnnotationHandler() {
+abstract class MixinExtrasInjectorAnnotationHandler : InsnInjectorAnnotationHandler() {
open val oldSuperBehavior = false
enum class InstructionType {
@@ -99,101 +99,25 @@ abstract class MixinExtrasInjectorAnnotationHandler : InjectorAnnotationHandler(
return supportedInstructionTypes.any { it.matches(TargetInsn(insn, decorations)) }
}
- abstract fun expectedMethodSignature(
+ abstract fun expectedMethodSignatureImpl(
annotation: PsiAnnotation,
targetClass: ClassNode,
targetMethod: MethodNode,
target: TargetInsn,
- ): Pair?
+ ): MethodSignatures?
- open fun intLikeTypePositions(
- target: TargetInsn
- ): List = emptyList()
-
- override val allowCoerce = true
-
- override fun expectedMethodSignature(
- annotation: PsiAnnotation,
- targetClass: ClassNode,
- targetMethod: MethodNode
- ): List? {
- val insns = resolveInstructions(annotation, targetClass, targetMethod)
- .ifEmpty { return emptyList() }
- .map { TargetInsn(it.insn, it.decorations) }
- val signatures = insns.map { insn ->
- expectedMethodSignature(annotation, targetClass, targetMethod, insn)
- }
- val firstMatch = signatures[0] ?: return emptyList()
- if (signatures.drop(1).any { it != firstMatch }) return emptyList()
- val intLikeTypePositions = insns.map { intLikeTypePositions(it) }.distinct().singleOrNull().orEmpty()
- return allPossibleSignatures(
- annotation,
- targetClass,
- targetMethod,
- firstMatch.first,
- firstMatch.second,
- intLikeTypePositions
- )
- }
-
- private fun allPossibleSignatures(
+ final override fun expectedMethodSignature(
annotation: PsiAnnotation,
targetClass: ClassNode,
targetMethod: MethodNode,
- params: ParameterGroup,
- returnType: PsiType,
- intLikeTypePositions: List
- ): List {
- if (intLikeTypePositions.isEmpty()) {
- return listOf(
- makeSignature(annotation, targetClass, targetMethod, params, returnType, intLikeTypePositions)
- )
- }
- return buildList {
- for (actualType in intLikePsiTypes) {
- val newParams = params.parameters.toMutableList()
- var newReturnType = returnType
- for (pos in intLikeTypePositions) {
- when (pos) {
- is MethodSignature.TypePosition.Return -> newReturnType = actualType
- is MethodSignature.TypePosition.Param ->
- newParams[pos.index] = newParams[pos.index].copy(type = actualType)
- }
- }
- add(
- makeSignature(
- annotation,
- targetClass,
- targetMethod,
- ParameterGroup(newParams),
- newReturnType,
- intLikeTypePositions
- )
- )
- }
- }
+ targetInsn: TargetInsn
+ ): ExpectedSignatures {
+ return ExpectedSignatures.Valid(
+ expectedMethodSignatureImpl(annotation, targetClass, targetMethod, targetInsn)
+ ?: return ExpectedSignatures.Invalid
+ )
}
- private fun makeSignature(
- annotation: PsiAnnotation,
- targetClass: ClassNode,
- targetMethod: MethodNode,
- params: ParameterGroup,
- returnType: PsiType,
- intLikeTypePositions: List
- ) = MethodSignature(
- listOf(
- params,
- ParameterGroup(
- collectTargetMethodParameters(annotation.project, targetClass, targetMethod),
- required = ParameterGroup.RequiredLevel.OPTIONAL,
- isVarargs = true,
- ),
- ),
- returnType,
- intLikeTypePositions
- )
-
protected fun getInsnReturnType(insn: AbstractInsnNode): Type? {
return when {
insn is MethodInsnNode -> Type.getReturnType(insn.desc)
@@ -432,7 +356,3 @@ private fun getConstantType(insn: AbstractInsnNode?): Type? {
}
}
}
-
-private val intLikePsiTypes = listOf(
- PsiTypes.intType(), PsiTypes.booleanType(), PsiTypes.charType(), PsiTypes.byteType(), PsiTypes.shortType()
-)
diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyExpressionValueHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyExpressionValueHandler.kt
index e37cfcb7c..da949c80f 100644
--- a/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyExpressionValueHandler.kt
+++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyExpressionValueHandler.kt
@@ -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
@@ -20,10 +20,16 @@
package com.demonwav.mcdev.platform.mixin.handlers.mixinextras
+import com.demonwav.mcdev.platform.mixin.inspection.injector.GeneralSignatures
import com.demonwav.mcdev.platform.mixin.inspection.injector.MethodSignature
-import com.demonwav.mcdev.platform.mixin.inspection.injector.ParameterGroup
+import com.demonwav.mcdev.platform.mixin.inspection.injector.SuggestedSignature
+import com.demonwav.mcdev.platform.mixin.inspection.injector.collectSignatures
+import com.demonwav.mcdev.platform.mixin.util.ClassAndMethodNode
import com.demonwav.mcdev.platform.mixin.util.toPsiType
import com.demonwav.mcdev.util.Parameter
+import com.demonwav.mcdev.util.SequencedSet
+import com.demonwav.mcdev.util.emptySequencedSet
+import com.demonwav.mcdev.util.sequencedSetOf
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.PsiAnnotation
import com.intellij.psi.PsiType
@@ -56,22 +62,37 @@ class ModifyExpressionValueHandler : MixinExtrasInjectorAnnotationHandler() {
override val allowedInsnDescription = "instructions that return a value"
- override fun expectedMethodSignature(
+ override fun expectedMethodSignatureImpl(
annotation: PsiAnnotation,
targetClass: ClassNode,
targetMethod: MethodNode,
target: TargetInsn
- ): Pair? {
+ ): GeneralSignatures? {
val psiType = getReturnType(target, annotation) ?: return null
- return ParameterGroup(listOf(Parameter("original", psiType))) to psiType
+ return GeneralSignatures(
+ listOf(Parameter("original", psiType)),
+ psiType,
+ trailingParams = collectTargetMethodParameters(annotation.project, targetClass, targetMethod),
+ intLikePositions = intLikeTypePositions(target),
+ )
}
- override fun intLikeTypePositions(target: TargetInsn): List {
+ override fun suggestedMethodSignature(
+ annotation: PsiAnnotation,
+ targets: List
+ ): SuggestedSignature? {
+ return SuggestedSignature.general(
+ annotation,
+ expectedMethodSignatures(annotation, targets).collectSignatures() ?: return null
+ )
+ }
+
+ private fun intLikeTypePositions(target: TargetInsn): SequencedSet {
val expressionType = target.getDecoration(ExpressionDecorations.SIMPLE_EXPRESSION_TYPE)
if (expressionType == ExpressionASMUtils.INTLIKE_TYPE) {
- return listOf(MethodSignature.TypePosition.Return, MethodSignature.TypePosition.Param(0))
+ return sequencedSetOf(MethodSignature.TypePosition.Return, MethodSignature.TypePosition.Param(0))
}
- return emptyList()
+ return emptySequencedSet()
}
private fun getReturnType(
diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReceiverHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReceiverHandler.kt
index ba5eae551..3450e4cec 100644
--- a/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReceiverHandler.kt
+++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReceiverHandler.kt
@@ -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
@@ -20,9 +20,11 @@
package com.demonwav.mcdev.platform.mixin.handlers.mixinextras
-import com.demonwav.mcdev.platform.mixin.inspection.injector.ParameterGroup
+import com.demonwav.mcdev.platform.mixin.inspection.injector.GeneralSignatures
+import com.demonwav.mcdev.platform.mixin.inspection.injector.SuggestedSignature
+import com.demonwav.mcdev.platform.mixin.inspection.injector.collectSignatures
+import com.demonwav.mcdev.platform.mixin.util.ClassAndMethodNode
import com.intellij.psi.PsiAnnotation
-import com.intellij.psi.PsiType
import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext
import org.objectweb.asm.Opcodes
import org.objectweb.asm.tree.AbstractInsnNode
@@ -43,14 +45,28 @@ class ModifyReceiverHandler : MixinExtrasInjectorAnnotationHandler() {
override val allowedInsnDescription = "non-static method invocations and field references"
- override fun expectedMethodSignature(
+ override fun expectedMethodSignatureImpl(
annotation: PsiAnnotation,
targetClass: ClassNode,
targetMethod: MethodNode,
target: TargetInsn
- ): Pair? {
+ ): GeneralSignatures? {
val params = getPsiParameters(target.insn, targetClass, annotation) ?: return null
- return ParameterGroup(params) to params[0].type
+ return GeneralSignatures(
+ params,
+ params[0].type,
+ trailingParams = collectTargetMethodParameters(annotation.project, targetClass, targetMethod),
+ )
+ }
+
+ override fun suggestedMethodSignature(
+ annotation: PsiAnnotation,
+ targets: List
+ ): SuggestedSignature? {
+ return SuggestedSignature.general(
+ annotation,
+ expectedMethodSignatures(annotation, targets).collectSignatures() ?: return null
+ )
}
override val mixinExtrasExpressionContextType = ExpressionContext.Type.MODIFY_RECEIVER
diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReturnValueHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReturnValueHandler.kt
index a78398c64..d6003451c 100644
--- a/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReturnValueHandler.kt
+++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReturnValueHandler.kt
@@ -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
@@ -20,11 +20,13 @@
package com.demonwav.mcdev.platform.mixin.handlers.mixinextras
-import com.demonwav.mcdev.platform.mixin.inspection.injector.ParameterGroup
+import com.demonwav.mcdev.platform.mixin.inspection.injector.GeneralSignatures
+import com.demonwav.mcdev.platform.mixin.inspection.injector.SuggestedSignature
+import com.demonwav.mcdev.platform.mixin.inspection.injector.collectSignatures
+import com.demonwav.mcdev.platform.mixin.util.ClassAndMethodNode
import com.demonwav.mcdev.platform.mixin.util.getGenericReturnType
import com.demonwav.mcdev.util.Parameter
import com.intellij.psi.PsiAnnotation
-import com.intellij.psi.PsiType
import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.MethodNode
@@ -34,14 +36,28 @@ class ModifyReturnValueHandler : MixinExtrasInjectorAnnotationHandler() {
override val allowedInsnDescription = "return instructions"
- override fun expectedMethodSignature(
+ override fun expectedMethodSignatureImpl(
annotation: PsiAnnotation,
targetClass: ClassNode,
targetMethod: MethodNode,
target: TargetInsn
- ): Pair {
+ ): GeneralSignatures {
val returnType = targetMethod.getGenericReturnType(targetClass, annotation.project)
- return ParameterGroup(listOf(Parameter("original", returnType))) to returnType
+ return GeneralSignatures(
+ listOf(Parameter("original", returnType)),
+ returnType,
+ trailingParams = collectTargetMethodParameters(annotation.project, targetClass, targetMethod),
+ )
+ }
+
+ override fun suggestedMethodSignature(
+ annotation: PsiAnnotation,
+ targets: List
+ ): SuggestedSignature? {
+ return SuggestedSignature.general(
+ annotation,
+ expectedMethodSignatures(annotation, targets).collectSignatures() ?: return null
+ )
}
override val mixinExtrasExpressionContextType = ExpressionContext.Type.MODIFY_RETURN_VALUE
diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/ShareUtil.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/ShareUtil.kt
index 119a4da3b..3a89f62f3 100644
--- a/src/main/kotlin/platform/mixin/handlers/mixinextras/ShareUtil.kt
+++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/ShareUtil.kt
@@ -22,7 +22,6 @@ package com.demonwav.mcdev.platform.mixin.handlers.mixinextras
import com.demonwav.mcdev.platform.mixin.handlers.InjectorAnnotationHandler
import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler
-import com.demonwav.mcdev.platform.mixin.util.MethodTargetMember
import com.demonwav.mcdev.platform.mixin.util.mixinTargets
import com.demonwav.mcdev.util.constantStringValue
import com.demonwav.mcdev.util.findContainingClass
@@ -109,11 +108,7 @@ class ShareUtil(private val project: Project) {
val id = this.value ?: return emptyList()
return mixinTargets.flatMap { targetClass ->
- injector.resolveTarget(injectorAnnotation, targetClass).mapNotNull { target ->
- if (target !is MethodTargetMember) {
- return@mapNotNull null
- }
-
+ injector.resolveTarget(injectorAnnotation, targetClass).map { target ->
ShareKey(
namespace,
id,
diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt
index 0cae1c436..1d150c1fa 100644
--- a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt
+++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt
@@ -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
@@ -21,43 +21,48 @@
package com.demonwav.mcdev.platform.mixin.handlers.mixinextras
import com.demonwav.mcdev.platform.mixin.handlers.InjectorAnnotationHandler
+import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.CollectVisitor
import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.InsnResolutionInfo
-import com.demonwav.mcdev.platform.mixin.inspection.injector.MethodSignature
-import com.demonwav.mcdev.platform.mixin.inspection.injector.ParameterGroup
+import com.demonwav.mcdev.platform.mixin.inspection.injector.ExpectedSignatures
+import com.demonwav.mcdev.platform.mixin.inspection.injector.OperationWrapperSignatures
+import com.demonwav.mcdev.platform.mixin.inspection.injector.SuggestedSignature
+import com.demonwav.mcdev.platform.mixin.inspection.injector.collectSignatures
+import com.demonwav.mcdev.platform.mixin.util.ClassAndMethodNode
import com.demonwav.mcdev.platform.mixin.util.findSourceElement
import com.demonwav.mcdev.platform.mixin.util.getGenericReturnType
-import com.demonwav.mcdev.platform.mixin.util.mixinExtrasOperationType
-import com.demonwav.mcdev.util.Parameter
import com.intellij.psi.PsiAnnotation
import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiMethod
import com.intellij.psi.search.GlobalSearchScope
-import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.MethodNode
class WrapMethodHandler : InjectorAnnotationHandler() {
- override val allowCoerce get() = true
-
- override fun expectedMethodSignature(
+ override fun expectedMethodSignatures(
annotation: PsiAnnotation,
- targetClass: ClassNode,
- targetMethod: MethodNode,
- ): List {
- val returnType = targetMethod.getGenericReturnType(targetClass, annotation.project)
+ targets: List,
+ mode: CollectVisitor.Mode,
+ ): List> {
+ return targets.map { (targetClass, targetMethod) ->
+ val returnType = targetMethod.getGenericReturnType(targetClass, annotation.project)
- return listOf(
- MethodSignature(
- listOf(
- ParameterGroup(
- collectTargetMethodParameters(annotation.project, targetClass, targetMethod) +
- Parameter(
- "original",
- mixinExtrasOperationType(annotation, returnType) ?: return emptyList()
- ),
- )
- ),
- returnType
+ ExpectedSignatures.Valid(
+ OperationWrapperSignatures.create(
+ annotation,
+ collectTargetMethodParameters(annotation.project, targetClass, targetMethod),
+ returnType,
+ ) ?: return@map ExpectedSignatures.Invalid
)
+ }
+ }
+
+ override fun suggestedMethodSignature(
+ annotation: PsiAnnotation,
+ targets: List
+ ): SuggestedSignature? {
+ return SuggestedSignature.operationWrapper(
+ annotation,
+ expectedMethodSignatures(annotation, targets).collectSignatures() ?: return null
)
}
@@ -84,5 +89,5 @@ class WrapMethodHandler : InjectorAnnotationHandler() {
)?.let(::listOf).orEmpty()
}
- override val mixinExtrasExpressionContextType = ExpressionContext.Type.CUSTOM
+ override fun canAlwaysBeStatic(method: PsiMethod) = false
}
diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt
index bedc1822f..c8f5289b4 100644
--- a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt
+++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt
@@ -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
@@ -21,10 +21,13 @@
package com.demonwav.mcdev.platform.mixin.handlers.mixinextras
import com.demonwav.mcdev.platform.mixin.inspection.injector.MethodSignature
-import com.demonwav.mcdev.platform.mixin.inspection.injector.ParameterGroup
-import com.demonwav.mcdev.platform.mixin.util.mixinExtrasOperationType
+import com.demonwav.mcdev.platform.mixin.inspection.injector.OperationWrapperSignatures
+import com.demonwav.mcdev.platform.mixin.inspection.injector.SuggestedSignature
+import com.demonwav.mcdev.platform.mixin.inspection.injector.collectSignatures
+import com.demonwav.mcdev.platform.mixin.util.ClassAndMethodNode
import com.demonwav.mcdev.platform.mixin.util.toPsiType
import com.demonwav.mcdev.util.Parameter
+import com.demonwav.mcdev.util.buildSequencedSet
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.PsiAnnotation
import com.intellij.psi.PsiType
@@ -47,32 +50,45 @@ class WrapOperationHandler : MixinExtrasInjectorAnnotationHandler() {
return if (annotation.hasAttribute("constant")) "constant" else "at"
}
- override fun expectedMethodSignature(
+ override fun expectedMethodSignatureImpl(
annotation: PsiAnnotation,
targetClass: ClassNode,
targetMethod: MethodNode,
target: TargetInsn
- ): Pair? {
+ ): OperationWrapperSignatures? {
val params = getParameterTypes(target, targetClass, annotation) ?: return null
val returnType = getReturnType(target, annotation) ?: return null
- val operationType = mixinExtrasOperationType(annotation, returnType) ?: return null
- return ParameterGroup(
- params + Parameter("original", operationType)
- ) to returnType
+ return OperationWrapperSignatures.create(
+ annotation,
+ params,
+ returnType,
+ intLikeTypePositions(target),
+ trailingParams = collectTargetMethodParameters(annotation.project, targetClass, targetMethod),
+ )
}
- override fun intLikeTypePositions(target: TargetInsn) = buildList {
+ override fun suggestedMethodSignature(
+ annotation: PsiAnnotation,
+ targets: List
+ ): SuggestedSignature? {
+ return SuggestedSignature.operationWrapper(
+ annotation,
+ expectedMethodSignatures(annotation, targets).collectSignatures() ?: return null
+ )
+ }
+
+ private fun intLikeTypePositions(target: TargetInsn) = buildSequencedSet {
+ target.getDecoration>(ExpressionDecorations.SIMPLE_OPERATION_ARGS)?.forEachIndexed { i, it ->
+ if (it == ExpressionASMUtils.INTLIKE_TYPE) {
+ add(MethodSignature.TypePosition.Param(i))
+ }
+ }
if (
target.getDecoration(ExpressionDecorations.SIMPLE_OPERATION_RETURN_TYPE)
== ExpressionASMUtils.INTLIKE_TYPE
) {
add(MethodSignature.TypePosition.Return)
}
- target.getDecoration>(ExpressionDecorations.SIMPLE_OPERATION_ARGS)?.forEachIndexed { i, it ->
- if (it == ExpressionASMUtils.INTLIKE_TYPE) {
- add(MethodSignature.TypePosition.Param(i))
- }
- }
}
private fun getParameterTypes(
diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapWithConditionHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapWithConditionHandler.kt
index 4875991c2..af588256f 100644
--- a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapWithConditionHandler.kt
+++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapWithConditionHandler.kt
@@ -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
@@ -20,10 +20,12 @@
package com.demonwav.mcdev.platform.mixin.handlers.mixinextras
-import com.demonwav.mcdev.platform.mixin.inspection.injector.ParameterGroup
+import com.demonwav.mcdev.platform.mixin.inspection.injector.GeneralSignatures
+import com.demonwav.mcdev.platform.mixin.inspection.injector.SuggestedSignature
+import com.demonwav.mcdev.platform.mixin.inspection.injector.collectSignatures
+import com.demonwav.mcdev.platform.mixin.util.ClassAndMethodNode
import com.demonwav.mcdev.platform.mixin.util.nextRealInsn
import com.intellij.psi.PsiAnnotation
-import com.intellij.psi.PsiType
import com.intellij.psi.PsiTypes
import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext
import org.objectweb.asm.Opcodes
@@ -66,14 +68,28 @@ class WrapWithConditionHandler : MixinExtrasInjectorAnnotationHandler() {
return getPsiParameters(insn, targetClass, annotation)?.size ?: 0
}
- override fun expectedMethodSignature(
+ override fun expectedMethodSignatureImpl(
annotation: PsiAnnotation,
targetClass: ClassNode,
targetMethod: MethodNode,
target: TargetInsn
- ): Pair? {
+ ): GeneralSignatures? {
val params = getPsiParameters(target.insn, targetClass, annotation) ?: return null
- return ParameterGroup(params) to PsiTypes.booleanType()
+ return GeneralSignatures(
+ params,
+ PsiTypes.booleanType(),
+ trailingParams = collectTargetMethodParameters(annotation.project, targetClass, targetMethod),
+ )
+ }
+
+ override fun suggestedMethodSignature(
+ annotation: PsiAnnotation,
+ targets: List
+ ): SuggestedSignature? {
+ return SuggestedSignature.general(
+ annotation,
+ expectedMethodSignatures(annotation, targets).collectSignatures() ?: return null
+ )
}
override val mixinExtrasExpressionContextType = ExpressionContext.Type.WRAP_WITH_CONDITION
diff --git a/src/main/kotlin/platform/mixin/inspection/injector/CancellableBeforeSuperCallInspection.kt b/src/main/kotlin/platform/mixin/inspection/injector/CancellableBeforeSuperCallInspection.kt
index e6c060cf9..c44e6b42f 100644
--- a/src/main/kotlin/platform/mixin/inspection/injector/CancellableBeforeSuperCallInspection.kt
+++ b/src/main/kotlin/platform/mixin/inspection/injector/CancellableBeforeSuperCallInspection.kt
@@ -20,7 +20,7 @@
package com.demonwav.mcdev.platform.mixin.inspection.injector
-import com.demonwav.mcdev.platform.mixin.handlers.InjectorAnnotationHandler
+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.util.MethodTargetMember
@@ -61,7 +61,7 @@ class CancellableBeforeSuperCallInspection : MixinInspection() {
object Util {
fun doesInjectBeforeSuperConstructorCall(annotation: PsiAnnotation): Boolean {
val handler = MixinAnnotationHandler.forMixinAnnotation(MixinConstants.Annotations.INJECT)!!
- as InjectorAnnotationHandler
+ as InsnInjectorAnnotationHandler
for (target in MixinAnnotationHandler.resolveTarget(annotation)) {
if (target !is MethodTargetMember) {
diff --git a/src/main/kotlin/platform/mixin/inspection/injector/DisallowedTargetInsnInspection.kt b/src/main/kotlin/platform/mixin/inspection/injector/DisallowedTargetInsnInspection.kt
index b0e235a31..1153bda68 100644
--- a/src/main/kotlin/platform/mixin/inspection/injector/DisallowedTargetInsnInspection.kt
+++ b/src/main/kotlin/platform/mixin/inspection/injector/DisallowedTargetInsnInspection.kt
@@ -20,11 +20,10 @@
package com.demonwav.mcdev.platform.mixin.inspection.injector
-import com.demonwav.mcdev.platform.mixin.handlers.InjectorAnnotationHandler
+import com.demonwav.mcdev.platform.mixin.handlers.InsnInjectorAnnotationHandler
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.util.MethodTargetMember
import com.demonwav.mcdev.platform.mixin.util.MixinConstants
import com.demonwav.mcdev.platform.mixin.util.mixinTargets
import com.demonwav.mcdev.util.findContainingClass
@@ -37,20 +36,18 @@ class DisallowedTargetInsnInspection : MixinInspection() {
override fun buildVisitor(holder: ProblemsHolder) = object : JavaElementVisitor() {
override fun visitAnnotation(annotation: PsiAnnotation) {
- if (!annotation.hasQualifiedName(MixinConstants.Annotations.AT)) {
+ if (!annotation.hasQualifiedName(MixinConstants.Annotations.AT) &&
+ !annotation.hasQualifiedName(MixinConstants.Annotations.CONSTANT)
+ ) {
return
}
val injectorAnnotation = AtResolver.findInjectorAnnotation(annotation, skipThroughSlice = false) ?: return
val injector = MixinAnnotationHandler.forMixinAnnotation(injectorAnnotation, annotation.project)
- as? InjectorAnnotationHandler ?: return
+ as? InsnInjectorAnnotationHandler ?: return
val containingClass = injectorAnnotation.findContainingClass() ?: return
val hasInvalidInstructions = containingClass.mixinTargets.any { targetClass ->
injector.resolveTarget(injectorAnnotation, targetClass).any { targetMember ->
- if (targetMember !is MethodTargetMember) {
- return@any false
- }
-
AtResolver(annotation, targetMember.classAndMethod.clazz, targetMember.classAndMethod.method)
.resolveInstructions()
.any {
diff --git a/src/main/kotlin/platform/mixin/inspection/injector/DiscouragedShiftInspection.kt b/src/main/kotlin/platform/mixin/inspection/injector/DiscouragedShiftInspection.kt
index 2deb712f0..16958fa6c 100644
--- a/src/main/kotlin/platform/mixin/inspection/injector/DiscouragedShiftInspection.kt
+++ b/src/main/kotlin/platform/mixin/inspection/injector/DiscouragedShiftInspection.kt
@@ -20,7 +20,7 @@
package com.demonwav.mcdev.platform.mixin.inspection.injector
-import com.demonwav.mcdev.platform.mixin.handlers.InjectorAnnotationHandler
+import com.demonwav.mcdev.platform.mixin.handlers.InsnInjectorAnnotationHandler
import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler
import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.AtResolver
import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.InjectionPoint
@@ -41,7 +41,7 @@ class DiscouragedShiftInspection : MixinInspection() {
return
}
val injectorAnnotation = AtResolver.findInjectorAnnotation(annotation) ?: return
- val injector = MixinAnnotationHandler.forMixinAnnotation(injectorAnnotation, holder.project) as? InjectorAnnotationHandler
+ val injector = MixinAnnotationHandler.forMixinAnnotation(injectorAnnotation, holder.project) as? InsnInjectorAnnotationHandler
?: return
val atValue = annotation.findDeclaredAttributeValue("value") ?: return
val atCode = atValue.constantStringValue ?: return
@@ -53,7 +53,7 @@ class DiscouragedShiftInspection : MixinInspection() {
}
}
- private fun isShiftDiscouraged(shift: Int, at: PsiAnnotation, injector: InjectorAnnotationHandler, atCode: String): Boolean {
+ private fun isShiftDiscouraged(shift: Int, at: PsiAnnotation, injector: InsnInjectorAnnotationHandler, atCode: String): Boolean {
if (injector.isShiftAlwaysDiscouraged) {
return shift != 0
}
diff --git a/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt b/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt
index f0acaed4e..4f55bfc33 100644
--- a/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt
+++ b/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt
@@ -21,20 +21,22 @@
package com.demonwav.mcdev.platform.mixin.inspection.injector
import com.demonwav.mcdev.platform.mixin.handlers.InjectorAnnotationHandler
+import com.demonwav.mcdev.platform.mixin.handlers.InsnInjectorAnnotationHandler
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.reference.MethodReference
import com.demonwav.mcdev.platform.mixin.util.MixinConstants
import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Annotations.COERCE
+import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Classes.CALLBACK_INFO
import com.demonwav.mcdev.platform.mixin.util.findDelegateConstructorCall
import com.demonwav.mcdev.platform.mixin.util.hasAccess
-import com.demonwav.mcdev.platform.mixin.util.isAssignable
import com.demonwav.mcdev.platform.mixin.util.isConstructor
import com.demonwav.mcdev.platform.mixin.util.isMixinExtrasSugar
-import com.demonwav.mcdev.util.Parameter
+import com.demonwav.mcdev.platform.mixin.util.mixinTargets
+import com.demonwav.mcdev.util.SequencedSet
+import com.demonwav.mcdev.util.findContainingClass
import com.demonwav.mcdev.util.findKeyword
import com.demonwav.mcdev.util.fullQualifiedName
-import com.demonwav.mcdev.util.invokeLater
import com.demonwav.mcdev.util.synchronize
import com.intellij.codeInsight.FileModificationService
import com.intellij.codeInsight.intention.FileModifier.SafeFieldForPreview
@@ -53,30 +55,26 @@ import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.openapi.application.runWriteAction
-import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import com.intellij.psi.JavaElementVisitor
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.PsiClassType
+import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiElementVisitor
-import com.intellij.psi.PsiEllipsisType
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiMethod
import com.intellij.psi.PsiModifier
import com.intellij.psi.PsiNameHelper
import com.intellij.psi.PsiParameterList
-import com.intellij.psi.PsiPrimitiveType
import com.intellij.psi.PsiType
-import com.intellij.psi.codeStyle.JavaCodeStyleManager
import com.intellij.psi.codeStyle.VariableKind
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.psi.util.PsiUtil
-import com.intellij.psi.util.TypeConversionUtil
-import com.intellij.psi.util.parentOfType
import com.intellij.psi.util.startOffset
+import com.siyeh.ig.psiutils.VariableNameGenerator
import org.objectweb.asm.Opcodes
class InvalidInjectorMethodSignatureInspection : MixinInspection() {
@@ -90,239 +88,137 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() {
override fun visitMethod(method: PsiMethod) {
val identifier = method.nameIdentifier ?: return
val modifiers = method.modifierList
-
- var reportedStatic = false
- var reportedSignature = false
-
- for (annotation in modifiers.annotations) {
- val handler = MixinAnnotationHandler.forMixinAnnotation(annotation, annotation.project)
- as? InjectorAnnotationHandler ?: continue
- val methodAttribute = annotation.findDeclaredAttributeValue("method") ?: continue
- val targetMethods = MethodReference.resolve(methodAttribute) ?: continue
-
- val matchesByMethod = targetMethods.associateWith { classAndMethod ->
- handler.resolveInstructions(
- annotation,
- classAndMethod.clazz,
- classAndMethod.method
- )
- }
- val hasDisallowedInsns = matchesByMethod.values.asSequence()
- .flatten().any { !handler.isInsnAllowed(it.insn, it.decorations) }
- if (hasDisallowedInsns) {
- continue
+ val parameters = method.parameterList
+
+ val (annotation, handler) = modifiers.annotations.firstNotNullOfOrNull { annotation ->
+ (MixinAnnotationHandler.forMixinAnnotation(annotation, annotation.project)
+ as? InjectorAnnotationHandler)?.let { annotation to it }
+ } ?: return
+
+ val targetMethods = annotation.findContainingClass()?.mixinTargets?.flatMap { targetClass ->
+ handler.resolveTarget(annotation, targetClass).map { it.classAndMethod }
+ } ?: return
+
+ val matchesByMethod = targetMethods.asSequence()
+ .mapNotNull { classAndMethod ->
+ if (handler is InsnInjectorAnnotationHandler) {
+ handler.resolveInstructions(annotation, classAndMethod.clazz, classAndMethod.method)
+ .takeUnless { it.isEmpty() }
+ ?.let { classAndMethod to it }
+ } else {
+ classAndMethod to emptyList()
+ }
}
+ .toMap()
+ .ifEmpty { return }
- for ((targetMethod, matches) in matchesByMethod) {
- if (matches.isEmpty()) {
- // We will never inject
- continue
- }
- if (!reportedStatic) {
- var shouldBeStatic = targetMethod.method.hasAccess(Opcodes.ACC_STATIC)
-
- if (!shouldBeStatic && targetMethod.method.isConstructor) {
- // before the superclass constructor call, everything must be static
- val methodInsns = targetMethod.method.instructions
- val delegateCtorCall = targetMethod.method.findDelegateConstructorCall()
- if (methodInsns != null && delegateCtorCall != null) {
- shouldBeStatic = matches.any {
- methodInsns.indexOf(it.insn) <= methodInsns.indexOf(delegateCtorCall)
- }
- }
- }
+ val hasDisallowedInsns = handler is InsnInjectorAnnotationHandler && matchesByMethod.values.asSequence()
+ .flatten().any { !handler.isInsnAllowed(it.insn, it.decorations) }
+ if (hasDisallowedInsns) {
+ return
+ }
- if (shouldBeStatic && !modifiers.hasModifierProperty(PsiModifier.STATIC)) {
- reportedStatic = true
- holder.registerProblem(
- identifier,
- "Method must be static",
- QuickFixFactory.getInstance().createModifierListFix(
- modifiers,
- PsiModifier.STATIC,
- true,
- false,
- ),
- )
- } else if (!shouldBeStatic && modifiers.hasModifierProperty(PsiModifier.STATIC)) {
- if (!handler.canAlwaysBeStatic(method)) {
- reportedStatic = true
- holder.registerProblem(
- modifiers.findKeyword(PsiModifier.STATIC) ?: identifier,
- "Method must not be static",
- QuickFixFactory.getInstance().createModifierListFix(
- modifiers,
- PsiModifier.STATIC,
- false,
- false,
- ),
- )
+ val requiredStaticness = matchesByMethod.asSequence()
+ .mapNotNull { (targetMethod, matches) ->
+ var shouldBeStatic = targetMethod.method.hasAccess(Opcodes.ACC_STATIC)
+
+ if (!shouldBeStatic && targetMethod.method.isConstructor) {
+ // before the superclass constructor call, everything must be static
+ val methodInsns = targetMethod.method.instructions
+ val delegateCtorCall = targetMethod.method.findDelegateConstructorCall()
+ if (methodInsns != null && delegateCtorCall != null) {
+ shouldBeStatic = matches.any {
+ methodInsns.indexOf(it.insn) <= methodInsns.indexOf(delegateCtorCall)
}
}
}
- if (!reportedSignature) {
- // Check method parameters
- val parameters = method.parameterList
- val possibleSignatures = handler.expectedMethodSignature(
- annotation,
- targetMethod.clazz,
- targetMethod.method,
- ) ?: continue
-
- val annotationName = annotation.nameReferenceElement?.referenceName
-
- if (possibleSignatures.isEmpty()) {
- reportedSignature = true
- if (handler.isUnresolved(annotation) != null) {
- holder.registerProblem(
- parameters,
- "There are no possible signatures for this injector",
- )
- }
- continue
- }
+ when {
+ shouldBeStatic -> true
+ handler.canAlwaysBeStatic(method) -> null
+ else -> false
+ }
+ }.toSet()
- var isValid = false
- for ((expectedParameters, expectedReturnType) in possibleSignatures) {
- val paramsMatch =
- Util.checkParameters(parameters, expectedParameters, handler.allowCoerce) == CheckResult.OK
- if (paramsMatch) {
- val methodReturnType = method.returnType
- if (methodReturnType != null &&
- checkReturnType(expectedReturnType, methodReturnType, method, handler.allowCoerce)
- ) {
- isValid = true
- break
- }
- }
- }
+ when {
+ requiredStaticness.size == 2 -> holder.registerProblem(
+ identifier,
+ "Impossible combination of targets: some require a static handler and others a non-static handler",
+ )
- if (!isValid) {
- val (expectedParameters, expectedReturnType, intLikeTypePositions) = possibleSignatures[0]
- val normalizedReturnType = when (expectedReturnType) {
- is PsiEllipsisType -> expectedReturnType.toArrayType()
- else -> expectedReturnType
- }
+ true in requiredStaticness -> if (!method.hasModifierProperty(PsiModifier.STATIC)) {
+ holder.registerProblem(
+ identifier,
+ "Method must be static",
+ QuickFixFactory.getInstance().createModifierListFix(
+ modifiers,
+ PsiModifier.STATIC,
+ true,
+ false,
+ ),
+ )
+ }
- val paramsCheck = Util.checkParameters(parameters, expectedParameters, handler.allowCoerce)
- val isWarning = paramsCheck == CheckResult.WARNING
- val methodReturnType = method.returnType
- val returnTypeOk = methodReturnType != null &&
- checkReturnType(normalizedReturnType, methodReturnType, method, handler.allowCoerce)
- val isError = paramsCheck == CheckResult.ERROR || !returnTypeOk
- if (isWarning || isError) {
- reportedSignature = true
-
- val description =
- "Method signature does not match expected signature for $annotationName"
- val quickFix = SignatureQuickFix(
- method,
- expectedParameters.takeUnless { paramsCheck == CheckResult.OK },
- normalizedReturnType.takeUnless { returnTypeOk },
- intLikeTypePositions
- )
- val highlightType =
- if (isError)
- ProblemHighlightType.GENERIC_ERROR_OR_WARNING
- else
- ProblemHighlightType.WARNING
- val declarationStart = (method.returnTypeElement ?: identifier).startOffsetInParent
- val declarationEnd = method.parameterList.textRangeInParent.endOffset
- holder.registerProblem(
- method,
- description,
- highlightType,
- TextRange.create(declarationStart, declarationEnd),
- quickFix
- )
- }
- }
- }
+ false in requiredStaticness -> if (method.hasModifierProperty(PsiModifier.STATIC)) {
+ holder.registerProblem(
+ modifiers.findKeyword(PsiModifier.STATIC) ?: identifier,
+ "Method must not be static",
+ QuickFixFactory.getInstance().createModifierListFix(
+ modifiers,
+ PsiModifier.STATIC,
+ false,
+ false,
+ ),
+ )
}
}
- }
- private fun checkReturnType(
- expectedReturnType: PsiType,
- methodReturnType: PsiType,
- method: PsiMethod,
- allowCoerce: Boolean,
- ): Boolean {
- val expectedErasure = TypeConversionUtil.erasure(expectedReturnType)
- val returnErasure = TypeConversionUtil.erasure(methodReturnType)
- if (expectedErasure == returnErasure) {
- return true
- }
- if (!allowCoerce || !method.hasAnnotation(COERCE)) {
- return false
- }
- if (expectedReturnType is PsiPrimitiveType || methodReturnType is PsiPrimitiveType) {
- return false
- }
- return isAssignable(methodReturnType, expectedReturnType)
- }
- }
+ val isAlreadyValid = handler.expectedMethodSignatures(
+ annotation,
+ targetMethods,
+ CollectVisitor.Mode.RESOLUTION,
+ ).all { it.matches(method) }
- object Util {
- fun checkParameters(
- parameterList: PsiParameterList,
- expected: List,
- allowCoerce: Boolean,
- ): CheckResult {
- val parameters = parameterList.parameters
- val parametersWithoutSugar = parameters.dropLastWhile { it.isMixinExtrasSugar }.toTypedArray()
- var pos = 0
-
- for (group in expected) {
- // Check if parameter group matches
- if (group.match(parametersWithoutSugar, pos, allowCoerce)) {
- pos += group.size
- } else if (group.required != ParameterGroup.RequiredLevel.OPTIONAL) {
- return if (group.required == ParameterGroup.RequiredLevel.ERROR_IF_ABSENT) {
- CheckResult.ERROR
- } else {
- CheckResult.WARNING
- }
- }
+ if (isAlreadyValid) {
+ return
}
- // Sugars are valid on any injector and should be ignored, as long as they're at the end.
- while (pos < parameters.size) {
- if (parameters[pos].isMixinExtrasSugar) {
- pos++
- } else {
- break
- }
- }
+ val signatureOptions =
+ handler.expectedMethodSignatures(annotation, targetMethods).collectSignatures()
+ val signatureSuggestion = signatureOptions?.let { SuggestedReturnType.forParams(parameters, it) }
+ ?: handler.suggestedMethodSignature(annotation, targetMethods)
- // check we have consumed all the parameters
- if (pos < parameters.size) {
- return if (
- expected.lastOrNull()?.isVarargs == true &&
- expected.last().required == ParameterGroup.RequiredLevel.WARN_IF_ABSENT
- ) {
- CheckResult.WARNING
- } else {
- CheckResult.ERROR
- }
- }
+ val declarationStart = (method.returnTypeElement ?: identifier).startOffsetInParent
+ val declarationEnd = method.parameterList.textRangeInParent.endOffset
- return CheckResult.OK
+ if (signatureSuggestion == null) {
+ holder.registerProblem(
+ method,
+ "There are no possible signatures for this injector",
+ ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
+ TextRange.create(declarationStart, declarationEnd),
+ )
+ } else {
+ val annotationName = annotation.nameReferenceElement?.referenceName
+ val description =
+ "Method signature does not match expected signature for $annotationName"
+ val quickFix = SignatureQuickFix(method, signatureSuggestion)
+
+ holder.registerProblem(
+ method,
+ description,
+ ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
+ TextRange.create(declarationStart, declarationEnd),
+ quickFix,
+ )
+ }
}
}
- enum class CheckResult {
- OK, WARNING, ERROR
- }
-
private class SignatureQuickFix(
method: PsiMethod,
@SafeFieldForPreview
- private val expectedParams: List?,
- @SafeFieldForPreview
- private val expectedReturnType: PsiType?,
- private val intLikeTypePositions: List
+ private val signatureSuggestion: SignatureSuggestion,
) : LocalQuickFixAndIntentionActionOnPsiElement(method) {
private val fixName = "Fix method signature"
@@ -346,7 +242,8 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() {
val method = startElement as PsiMethod
fixParameters(project, method.parameterList, false)
fixReturnType(method, editor ?: return, file, false)
- fixIntLikeTypes(method, editor, false)
+ fixCoerce(project, method, false)
+ fixIntLikeTypes(project, method, editor, false)
}
override fun generatePreview(project: Project, editor: Editor, file: PsiFile): IntentionPreviewInfo {
@@ -355,14 +252,13 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() {
fixParameters(project, method.parameterList, true)
// Pass the original startElement because the underlying fix gets the preview element itself
fixReturnType(startElement as PsiMethod, editor, file, true)
- fixIntLikeTypes(method, editor, true)
+ fixCoerce(project, method, true)
+ fixIntLikeTypes(project, method, editor, true)
return IntentionPreviewInfo.DIFF
}
private fun fixParameters(project: Project, parameters: PsiParameterList, preview: Boolean) {
- if (expectedParams == null) {
- return
- }
+ val suggestedParams = signatureSuggestion.params ?: return
// We want to preserve captured locals
val locals = parameters.parameters.dropWhile {
val fqname = (it.type as? PsiClassType)?.fullQualifiedName ?: return@dropWhile true
@@ -374,21 +270,23 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() {
// We want to preserve sugars, and while we're at it, we might as well move them all to the end
val sugars = parameters.parameters.filter { it.isMixinExtrasSugar }
- val newParams = expectedParams.flatMapTo(mutableListOf()) {
- if (it.default) {
- val nameHelper = PsiNameHelper.getInstance(project)
- val languageLevel = PsiUtil.getLanguageLevel(parameters)
- it.parameters.mapIndexed { i: Int, p: Parameter ->
- val paramName = p.name?.takeIf { name -> nameHelper.isIdentifier(name, languageLevel) }
- ?: JavaCodeStyleManager.getInstance(project)
- .suggestVariableName(VariableKind.PARAMETER, null, null, p.type).names
- .firstOrNull()
- ?: "var$i"
- JavaPsiFacade.getElementFactory(project).createParameter(paramName, p.type)
- }
- } else {
- emptyList()
+ val nameHelper = PsiNameHelper.getInstance(project)
+ val languageLevel = PsiUtil.getLanguageLevel(parameters)
+
+ val usedNames = mutableSetOf()
+ val newParams = suggestedParams.mapTo(mutableListOf()) { p ->
+ val paramName = p.name?.takeIf { name -> nameHelper.isIdentifier(name, languageLevel) }
+ ?: suggestedParamNames(p.type).firstOrNull { it !in usedNames }
+ ?: VariableNameGenerator(parameters, VariableKind.PARAMETER)
+ .byType(p.type)
+ .skipNames(usedNames)
+ .generate(false)
+ usedNames.add(paramName)
+ val newParam = JavaPsiFacade.getElementFactory(project).createParameter(paramName, p.type)
+ if (p.coerce) {
+ newParam.modifierList!!.addAnnotation(COERCE)
}
+ newParam
}
// Restore the captured locals and sugars before applying the fix
newParams.addAll(locals)
@@ -403,10 +301,7 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() {
}
private fun fixReturnType(method: PsiMethod, editor: Editor, file: PsiFile, preview: Boolean) {
- if (expectedReturnType == null) {
- return
- }
- val fix = QuickFixFactory.getInstance().createMethodReturnFix(method, expectedReturnType, false)
+ val fix = QuickFixFactory.getInstance().createMethodReturnFix(method, signatureSuggestion.returnType, false)
if (preview) {
fix.generatePreview(file.project, editor, file)
} else {
@@ -414,37 +309,51 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() {
}
}
- private fun fixIntLikeTypes(method: PsiMethod, editor: Editor, preview: Boolean) {
- if (intLikeTypePositions.isEmpty()) {
+ private fun fixCoerce(project: Project, method: PsiMethod, preview: Boolean) {
+ val existingCoerce = method.modifierList.findAnnotation(COERCE)
+ val needsCoerce = signatureSuggestion.coerceReturnType
+ val returnTypeElement = method.returnTypeElement!!
+
+ val fixCoerce: () -> Unit = when {
+ existingCoerce != null && !needsCoerce -> {
+ { existingCoerce.delete() }
+ }
+
+ existingCoerce == null && needsCoerce -> {
+ val annotation = JavaPsiFacade.getElementFactory(project)
+ .createAnnotationFromText("@$COERCE", returnTypeElement);
+ { method.modifierList.add(annotation) }
+ }
+
+ else -> return
+ }
+
+ if (preview) {
+ fixCoerce()
+ } else {
+ runWriteAction(fixCoerce)
+ }
+ }
+
+ private fun fixIntLikeTypes(project: Project, method: PsiMethod, editor: Editor, preview: Boolean) {
+ if (preview || signatureSuggestion.intLikeTypes.isEmpty()) {
return
}
- val runnable = {
- val template = makeIntLikeTypeTemplate(method, intLikeTypePositions)
+ runWriteAction {
+ PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(editor.document)
+
+ val template = makeIntLikeTypeTemplate(method, signatureSuggestion.intLikeTypes)
if (template != null) {
editor.caretModel.moveToOffset(method.startOffset)
TemplateManager.getInstance(method.project)
.startTemplate(editor, template)
}
}
-
- if (preview) {
- runnable()
- } else {
- invokeLater {
- WriteCommandAction.runWriteCommandAction(
- method.project,
- "Choose Int-Like Type",
- null,
- runnable,
- method.parentOfType()!!
- )
- }
- }
}
private fun makeIntLikeTypeTemplate(
method: PsiMethod,
- positions: List
+ positions: SequencedSet
): Template? {
val builder = TemplateBuilderImpl(method)
builder.replaceElement(
@@ -462,6 +371,13 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() {
}
return builder.buildInlineTemplate()
}
+
+ private companion object {
+ fun suggestedParamNames(type: PsiType): List = when {
+ type.equalsToText(CALLBACK_INFO) -> listOf("ci")
+ else -> emptyList()
+ }
+ }
}
}
diff --git a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt
index 804cb3fa0..04c2fdd62 100644
--- a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt
+++ b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt
@@ -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
@@ -20,24 +20,178 @@
package com.demonwav.mcdev.platform.mixin.inspection.injector
+import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Annotations.COERCE
+import com.demonwav.mcdev.platform.mixin.util.checkCoerce
+import com.demonwav.mcdev.platform.mixin.util.isMixinExtrasSugar
+import com.demonwav.mcdev.util.Parameter
+import com.demonwav.mcdev.util.SequencedSet
+import com.demonwav.mcdev.util.allEqual
+import com.demonwav.mcdev.util.emptySequencedSet
import com.intellij.psi.PsiMethod
+import com.intellij.psi.PsiParameter
+import com.intellij.psi.PsiParameterList
import com.intellij.psi.PsiType
import com.intellij.psi.PsiTypeElement
+import com.intellij.psi.PsiTypes
data class MethodSignature(
- val parameters: List,
+ val requiredParams: List,
val returnType: PsiType,
- val intLikeTypes: List = emptyList()
+ val allowCoerceRequired: Boolean,
+ val trailingParams: List = emptyList(),
+ val intLikePositions: SequencedSet = emptySequencedSet(),
) {
sealed interface TypePosition {
fun getElement(method: PsiMethod): PsiTypeElement?
+ fun getType(signature: MethodSignature): PsiType
+
+ fun getParam(params: Array): PsiParameter?
+
data object Return : TypePosition {
override fun getElement(method: PsiMethod) = method.returnTypeElement
+
+ override fun getType(signature: MethodSignature) = signature.returnType
+
+ override fun getParam(params: Array) = null
}
data class Param(val index: Int) : TypePosition {
override fun getElement(method: PsiMethod) = method.parameterList.parameters[index].typeElement
+
+ override fun getType(signature: MethodSignature) = signature.requiredParams[index].type
+
+ override fun getParam(params: Array) = params[index]
}
}
+
+ fun allPositions(numParams: Int): Sequence {
+ require(numParams >= requiredParams.size)
+ return sequenceOf(TypePosition.Return) + (0.. matches(
+ params: List,
+ returnType: PsiType,
+ returnCoerce: Boolean,
+ paramType: (ParamT) -> PsiType,
+ paramCoerce: (ParamT) -> Boolean,
+ ): Boolean {
+ val intLikeAssignment = when (val anchor = intLikePositions.firstOrNull()) {
+ null -> null
+ is TypePosition.Param -> paramType(params.getOrNull(anchor.index) ?: return false)
+ TypePosition.Return -> returnType
+ }
+ val transformedReturnType = if (TypePosition.Return in intLikePositions) intLikeAssignment!! else returnType
+ return matchesReturnType(transformedReturnType, returnCoerce)
+ && matchesParams(
+ params,
+ paramType,
+ paramCoerce,
+ knownIntLikeAssignment = intLikeAssignment,
+ )
+ }
+
+ private fun matchesParams(
+ params: List,
+ paramType: (ParamT) -> PsiType,
+ paramCoerce: (ParamT) -> Boolean,
+ knownIntLikeAssignment: PsiType?,
+ ): Boolean {
+ if (params.size !in requiredParams.size..requiredParams.size + trailingParams.size) {
+ return false
+ }
+
+ val intLikeAssignment = knownIntLikeAssignment
+ ?: (intLikePositions.firstOrNull() as? TypePosition.Param)?.let { paramType(params[it.index]) }
+
+ if (intLikeAssignment == null && intLikePositions.isNotEmpty()) {
+ // We don't know the return type, but we should make sure the combination is feasible for some return type
+ val intLikeIndices = intLikePositions.mapNotNull { (it as? TypePosition.Param)?.index }
+ val isFeasible = intLikeIndices.asSequence().map { paramType(params[it]) }.allEqual()
+ || intLikeIndices.all { index ->
+ val param = params[index]
+ checkCoerce(
+ PsiTypes.intType(),
+ paramType(param),
+ coerce = paramCoerce(param),
+ expectedIntLike = false,
+ )
+ }
+ if (!isFeasible) {
+ return false
+ }
+ }
+
+ fun matchParams(expected: List, allowCoerce: Boolean, startIndex: Int): Boolean {
+ return expected.asSequence()
+ .zip(params.asSequence().withIndex().drop(startIndex))
+ .all { (expected, indexAndActual) ->
+ val (index, actual) = indexAndActual
+ matchType(
+ expected.type,
+ paramType(actual),
+ isIntLike = intLikeAssignment == null && TypePosition.Param(index) in intLikePositions,
+ coerce = allowCoerce && paramCoerce(actual),
+ )
+ }
+ }
+
+ val transformedRequiredParams = if (intLikeAssignment == null) {
+ requiredParams
+ } else {
+ requiredParams.mapIndexed { i, param ->
+ if (TypePosition.Param(i) in intLikePositions) {
+ param.copy(type = intLikeAssignment)
+ } else {
+ param
+ }
+ }
+ }
+
+ return matchParams(transformedRequiredParams, allowCoerceRequired, 0)
+ && matchParams(trailingParams, true, requiredParams.size)
+ }
+
+ private companion object {
+ private fun matchType(expected: PsiType, actual: PsiType, isIntLike: Boolean, coerce: Boolean) =
+ checkCoerce(expected, actual, coerce, isIntLike)
+ }
}
diff --git a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt
new file mode 100644
index 000000000..86c9acab8
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt
@@ -0,0 +1,205 @@
+/*
+ * 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.util.TypeKind
+import com.demonwav.mcdev.platform.mixin.util.callbackInfoReturnableType
+import com.demonwav.mcdev.platform.mixin.util.callbackInfoType
+import com.demonwav.mcdev.platform.mixin.util.mixinExtrasOperationType
+import com.demonwav.mcdev.util.Parameter
+import com.demonwav.mcdev.util.SequencedMap
+import com.demonwav.mcdev.util.SequencedSet
+import com.demonwav.mcdev.util.emptySequencedSet
+import com.demonwav.mcdev.util.minus
+import com.demonwav.mcdev.util.sequencedMapOf
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiMethod
+import com.intellij.psi.PsiType
+import com.intellij.psi.PsiTypes
+import org.objectweb.asm.Type
+
+sealed interface ExpectedSignatures {
+ fun matches(method: PsiMethod): Boolean
+
+ data object Unknown : ExpectedSignatures {
+ override fun matches(method: PsiMethod) = true
+ }
+
+ data object Invalid : ExpectedSignatures {
+ override fun matches(method: PsiMethod) = false
+ }
+
+ data class Valid(val expected: T) : ExpectedSignatures {
+ override fun matches(method: PsiMethod) = expected.options.any { it.matches(method) }
+ }
+}
+
+inline fun List>.collectSignatures(): List? =
+ mapNotNull {
+ when (it) {
+ ExpectedSignatures.Unknown -> null
+ ExpectedSignatures.Invalid -> return null
+ is ExpectedSignatures.Valid -> it.expected as R
+ }
+ }
+
+interface MethodSignatures {
+ val options: List
+}
+
+class ModifierSignatures(
+ val paramOptions: SequencedMap,
+ val fullParams: List? = null,
+ val trailingParams: List = emptyList(),
+) : MethodSignatures {
+ override val options = paramOptions.values.flatMap { param ->
+ listOfNotNull(
+ MethodSignature(
+ listOf(param),
+ param.type,
+ allowCoerceRequired = false,
+ trailingParams = trailingParams,
+ ),
+ fullParams?.let {
+ MethodSignature(
+ it,
+ param.type,
+ allowCoerceRequired = false,
+ trailingParams = trailingParams,
+ )
+ },
+ )
+ }
+}
+
+class BasicSignatures(override val options: List) : MethodSignatures {
+ constructor(vararg options: MethodSignature) : this(options.asList())
+}
+
+class OperationWrapperSignatures private constructor(
+ val params: List,
+ val returnType: PsiType,
+ val intLikePositions: SequencedSet,
+ val trailingParams: List,
+ operationType: PsiType,
+) : MethodSignatures {
+ val signature = MethodSignature(
+ params + Parameter("original", operationType),
+ returnType,
+ allowCoerceRequired = true,
+ trailingParams = trailingParams,
+ intLikePositions = intLikePositions,
+ )
+
+ override val options = listOf(signature)
+
+ companion object {
+ fun create(
+ context: PsiElement,
+ params: List,
+ returnType: PsiType,
+ intLikePositions: SequencedSet = emptySequencedSet(),
+ trailingParams: List = emptyList(),
+ ) = mixinExtrasOperationType(context, returnType)?.let {
+ OperationWrapperSignatures(params, returnType, intLikePositions, trailingParams, it)
+ }
+ }
+}
+
+data class GeneralSignatures(
+ val params: List,
+ val returnTypeOptions: SequencedMap,
+ val allowCoerce: Boolean,
+ val trailingParams: List,
+ val intLikePositions: SequencedSet = emptySequencedSet(),
+) : MethodSignatures {
+ constructor(
+ params: List,
+ returnType: PsiType,
+ trailingParams: List,
+ allowCoerce: Boolean = true,
+ intLikePositions: SequencedSet = emptySequencedSet(),
+ ) : this(
+ params,
+ sequencedMapOf(TypeKind.of(returnType) to returnType),
+ allowCoerce,
+ trailingParams,
+ intLikePositions,
+ )
+
+ override val options = returnTypeOptions.keys.map(::specificSignature)
+
+ fun specificSignature(returnKind: TypeKind) = MethodSignature(
+ params,
+ returnTypeOptions.getValue(returnKind),
+ allowCoerceRequired = allowCoerce,
+ trailingParams = trailingParams,
+ intLikePositions = if (returnKind == TypeKind.INT_LIKE) {
+ intLikePositions
+ } else {
+ intLikePositions - MethodSignature.TypePosition.Return
+ },
+ )
+}
+
+class InjectSignatures private constructor(
+ val params: List,
+ val locals: List,
+ ciParam: Parameter,
+) : MethodSignatures {
+ val shortSignature = if (params.isNotEmpty()) {
+ MethodSignature(
+ listOf(ciParam),
+ PsiTypes.voidType(),
+ allowCoerceRequired = true,
+ )
+ } else {
+ null
+ }
+
+ val longSignature = MethodSignature(
+ params + ciParam,
+ PsiTypes.voidType(),
+ allowCoerceRequired = true,
+ trailingParams = locals,
+ )
+
+ override val options = listOfNotNull(shortSignature, longSignature)
+
+ companion object {
+ fun create(
+ context: PsiElement,
+ params: List,
+ returnType: PsiType,
+ locals: List
+ ): InjectSignatures? {
+ val ciParam = if (returnType == PsiTypes.voidType()) {
+ Parameter("ci", callbackInfoType(context.project))
+ } else {
+ Parameter(
+ "cir",
+ callbackInfoReturnableType(context.project, context, returnType) ?: return null,
+ )
+ }
+ return InjectSignatures(params, locals, ciParam)
+ }
+ }
+}
diff --git a/src/main/kotlin/platform/mixin/inspection/injector/MixinParameterNameInspection.kt b/src/main/kotlin/platform/mixin/inspection/injector/MixinParameterNameInspection.kt
index e74dc5b32..7b9537d3b 100644
--- a/src/main/kotlin/platform/mixin/inspection/injector/MixinParameterNameInspection.kt
+++ b/src/main/kotlin/platform/mixin/inspection/injector/MixinParameterNameInspection.kt
@@ -21,6 +21,7 @@
package com.demonwav.mcdev.platform.mixin.inspection.injector
import com.demonwav.mcdev.platform.mixin.handlers.InjectorAnnotationHandler
+import com.demonwav.mcdev.platform.mixin.handlers.InsnInjectorAnnotationHandler
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
@@ -30,7 +31,9 @@ import com.demonwav.mcdev.platform.mixin.util.MixinConstants
import com.demonwav.mcdev.platform.mixin.util.MixinTargetMember
import com.demonwav.mcdev.platform.mixin.util.hasNamedLocalVariables
import com.demonwav.mcdev.platform.mixin.util.isMixinExtrasSugar
+import com.demonwav.mcdev.util.Parameter
import com.demonwav.mcdev.util.findModule
+import com.demonwav.mcdev.util.ifNullOrEmpty
import com.intellij.codeInsight.intention.LowPriorityAction
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
@@ -63,7 +66,7 @@ class MixinParameterNameInspection : MixinInspection() {
override fun visitMethod(method: PsiMethod) {
val module = method.findModule() ?: return
val parameters = method.parameterList.parameters
- val parametersWithoutSugar = parameters.dropLastWhile { it.isMixinExtrasSugar }.toTypedArray()
+ val parametersWithoutSugar = parameters.dropLastWhile { it.isMixinExtrasSugar }
val validNames = arrayOfNulls>(parameters.size)
@@ -78,10 +81,7 @@ class MixinParameterNameInspection : MixinInspection() {
}
for (index in validNames.indices) {
- val names = validNames[index] ?: continue
- if (names.isEmpty()) {
- continue
- }
+ val names = validNames[index].ifNullOrEmpty { continue }
if (parameters[index].name !in names) {
val fixes = mutableListOf()
@@ -110,7 +110,7 @@ class MixinParameterNameInspection : MixinInspection() {
validNames: Array?>,
module: Module, method: PsiMethod,
parameters: Array,
- parametersWithoutSugar: Array,
+ parametersWithoutSugar: List,
annotation: PsiAnnotation,
handler: InjectorAnnotationHandler,
target: MixinTargetMember
@@ -123,43 +123,51 @@ class MixinParameterNameInspection : MixinInspection() {
return true
}
- val validNamesForThisTarget = arrayOfNulls>(parameters.size)
-
if (reportForMainSignature) {
- val expectedSignatures =
- handler.expectedMethodSignature(annotation, target.classAndMethod.clazz, target.classAndMethod.method)
- ?: return false
- var anyValidSignatures = false
-
- for ((expectedParams, _) in expectedSignatures) {
- if (InvalidInjectorMethodSignatureInspection.Util.checkParameters(
- method.parameterList,
- expectedParams,
- handler.allowCoerce
- ) != InvalidInjectorMethodSignatureInspection.CheckResult.OK
- ) {
- continue
+ val expectedSignatures = handler.expectedMethodSignatures(annotation, listOf(target.classAndMethod))
+
+ for (expected in expectedSignatures) {
+ val signatures = when (expected) {
+ ExpectedSignatures.Unknown -> continue
+ ExpectedSignatures.Invalid -> return false
+ is ExpectedSignatures.Valid -> expected.expected
}
+ val validNamesForThisTarget = Array?>(parameters.size) { mutableSetOf() }
+ var anyValidSignatures = false
- anyValidSignatures = true
+ for (expectedSignature in signatures.options) {
+ if (!expectedSignature.matches(method)) {
+ continue
+ }
+ anyValidSignatures = true
- checkExpectedSignatureForKnownNames(
- validNamesForThisTarget,
- handler,
- expectedParams,
- parametersWithoutSugar
- )
- }
+ checkExpectedSignatureForKnownNames(
+ validNamesForThisTarget,
+ expectedSignature.requiredParams + expectedSignature.trailingParams,
+ parametersWithoutSugar,
+ )
+ }
- if (!anyValidSignatures) {
- return false
+ if (!anyValidSignatures) {
+ return false
+ }
+
+ for ((pos, names) in validNamesForThisTarget.withIndex()) {
+ val suggestions = names ?: mutableSetOf()
+ val existingNames = validNames[pos]
+ if (existingNames == null) {
+ validNames[pos] = suggestions
+ } else {
+ existingNames.retainAll(suggestions)
+ }
+ }
}
}
if (reportForLocal) {
for (pos in parametersWithoutSugar.size until parameters.size) {
if (!checkSugarForKnownNames(
- validNamesForThisTarget,
+ validNames,
module,
handler,
annotation,
@@ -173,51 +181,30 @@ class MixinParameterNameInspection : MixinInspection() {
}
}
- for (index in validNamesForThisTarget.indices) {
- val names = validNamesForThisTarget[index] ?: continue
- if (validNames[index] == null) {
- validNames[index] = names
- } else {
- validNames[index]!!.retainAll(names)
- }
- }
-
return true
}
private fun checkExpectedSignatureForKnownNames(
validNamesForThisTarget: Array?>,
- handler: InjectorAnnotationHandler,
- expectedParams: List,
- parametersWithoutSugar: Array
+ expectedParams: List,
+ parametersWithoutSugar: List
) {
if (parametersWithoutSugar.isEmpty()) {
return
}
- var pos = 0
- for (group in expectedParams) {
- if (!group.match(parametersWithoutSugar, pos, handler.allowCoerce)) {
- continue
- }
- for (expectedParam in group.parameters) {
- if (expectedParam.knownName && expectedParam.name != null) {
- if (validNamesForThisTarget[pos] == null) {
- validNamesForThisTarget[pos] = mutableSetOf(expectedParam.name)
- } else {
- validNamesForThisTarget[pos]!!.add(expectedParam.name)
- }
- }
- pos++
- if (pos >= parametersWithoutSugar.size) {
- return
- }
+ for (pos in parametersWithoutSugar.indices) {
+ val expectedParam = expectedParams[pos]
+ if (!expectedParam.knownName || expectedParam.name == null) {
+ validNamesForThisTarget[pos] = null
+ } else {
+ validNamesForThisTarget[pos]?.add(expectedParam.name)
}
}
}
private fun checkSugarForKnownNames(
- validNamesForThisTarget: Array?>,
+ validNames: Array?>,
module: Module,
handler: InjectorAnnotationHandler,
annotation: PsiAnnotation,
@@ -225,6 +212,10 @@ class MixinParameterNameInspection : MixinInspection() {
parameter: PsiParameter,
pos: Int
): Boolean {
+ if (handler !is InsnInjectorAnnotationHandler) {
+ // Can't have locals
+ return true
+ }
val localAnnotation = parameter.getAnnotation(MixinConstants.MixinExtras.LOCAL) ?: return true
val localInfo = LocalInfo.fromAnnotation(parameter.type, localAnnotation)
@@ -239,12 +230,13 @@ class MixinParameterNameInspection : MixinInspection() {
target.classAndMethod.method,
insn.insn, CollectVisitor.Mode.RESOLUTION
)?.singleOrNull() ?: return false
- if (matchedLocal.isNamed) {
- if (validNamesForThisTarget[pos] == null) {
- validNamesForThisTarget[pos] = mutableSetOf(matchedLocal.name)
- } else {
- validNamesForThisTarget[pos]!!.add(matchedLocal.name)
- }
+ val suggestions = if (matchedLocal.isNamed) mutableSetOf(matchedLocal.name) else mutableSetOf()
+
+ val existingNames = validNames[pos]
+ if (existingNames == null) {
+ validNames[pos] = suggestions
+ } else {
+ existingNames.retainAll(suggestions)
}
}
diff --git a/src/main/kotlin/platform/mixin/inspection/injector/ModifyVariableArgsOnlyInspection.kt b/src/main/kotlin/platform/mixin/inspection/injector/ModifyVariableArgsOnlyInspection.kt
index 7d522e885..7903d8479 100644
--- a/src/main/kotlin/platform/mixin/inspection/injector/ModifyVariableArgsOnlyInspection.kt
+++ b/src/main/kotlin/platform/mixin/inspection/injector/ModifyVariableArgsOnlyInspection.kt
@@ -20,7 +20,7 @@
package com.demonwav.mcdev.platform.mixin.inspection.injector
-import com.demonwav.mcdev.platform.mixin.handlers.InjectorAnnotationHandler
+import com.demonwav.mcdev.platform.mixin.handlers.InsnInjectorAnnotationHandler
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
@@ -51,7 +51,7 @@ class ModifyVariableArgsOnlyInspection : MixinInspection() {
val problemElement = modifyVariable.nameReferenceElement ?: return
val injector =
- MixinAnnotationHandler.forMixinAnnotation(MODIFY_VARIABLE) as? InjectorAnnotationHandler ?: return
+ MixinAnnotationHandler.forMixinAnnotation(MODIFY_VARIABLE) as? InsnInjectorAnnotationHandler ?: return
val localInfo = LocalInfo.fromAnnotation(localType, modifyVariable)
if (Util.shouldReport(localInfo, injector, modifyVariable)) {
@@ -69,7 +69,7 @@ class ModifyVariableArgsOnlyInspection : MixinInspection() {
object Util {
fun shouldReport(
localInfo: LocalInfo,
- injector: InjectorAnnotationHandler,
+ injector: InsnInjectorAnnotationHandler,
injectorAnnotation: PsiAnnotation,
): Boolean {
if (localInfo.argsOnly) {
diff --git a/src/main/kotlin/platform/mixin/inspection/injector/ModifyVariableMayUseNameInspection.kt b/src/main/kotlin/platform/mixin/inspection/injector/ModifyVariableMayUseNameInspection.kt
index 40f541bdd..6af04288e 100644
--- a/src/main/kotlin/platform/mixin/inspection/injector/ModifyVariableMayUseNameInspection.kt
+++ b/src/main/kotlin/platform/mixin/inspection/injector/ModifyVariableMayUseNameInspection.kt
@@ -20,13 +20,12 @@
package com.demonwav.mcdev.platform.mixin.inspection.injector
-import com.demonwav.mcdev.platform.mixin.handlers.InjectorAnnotationHandler
+import com.demonwav.mcdev.platform.mixin.handlers.InsnInjectorAnnotationHandler
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
@@ -62,7 +61,7 @@ class ModifyVariableMayUseNameInspection : MixinInspection() {
val problemElement = modifyVariable.nameReferenceElement ?: return
val injector =
- MixinAnnotationHandler.forMixinAnnotation(MODIFY_VARIABLE) as? InjectorAnnotationHandler ?: return
+ MixinAnnotationHandler.forMixinAnnotation(MODIFY_VARIABLE) as? InsnInjectorAnnotationHandler ?: return
val localInfo = LocalInfo.fromAnnotation(localType, modifyVariable)
if (ignoreForImplicitLocals && localInfo.isImplicit) {
@@ -106,7 +105,7 @@ class ModifyVariableMayUseNameInspection : MixinInspection() {
object Util {
fun getVariableNameToIntroduce(
localInfo: LocalInfo,
- injector: InjectorAnnotationHandler,
+ injector: InsnInjectorAnnotationHandler,
injectorAnnotation: PsiAnnotation,
): String? {
if (localInfo.index == null && localInfo.ordinal == null && localInfo.names.isNotEmpty()) {
@@ -124,7 +123,7 @@ class ModifyVariableMayUseNameInspection : MixinInspection() {
}
for (target in injector.resolveTarget(injectorAnnotation, targetClass)) {
- val (clazz, method) = (target as? MethodTargetMember)?.classAndMethod ?: continue
+ val (clazz, method) = target.classAndMethod
for (insn in injector.resolveInstructions(injectorAnnotation, clazz, method)) {
val matchedLocals = localInfo.matchLocals(
module,
diff --git a/src/main/kotlin/platform/mixin/inspection/injector/ParameterGroup.kt b/src/main/kotlin/platform/mixin/inspection/injector/ParameterGroup.kt
deleted file mode 100644
index ddce382d3..000000000
--- a/src/main/kotlin/platform/mixin/inspection/injector/ParameterGroup.kt
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * 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.util.MixinConstants.Annotations.COERCE
-import com.demonwav.mcdev.platform.mixin.util.isAssignable
-import com.demonwav.mcdev.util.Parameter
-import com.demonwav.mcdev.util.normalize
-import com.intellij.psi.PsiParameter
-import com.intellij.psi.PsiPrimitiveType
-import com.intellij.psi.PsiType
-import com.intellij.psi.PsiTypes
-
-data class ParameterGroup(
- val parameters: List,
- val required: RequiredLevel = RequiredLevel.ERROR_IF_ABSENT,
- val default: Boolean = required != RequiredLevel.OPTIONAL,
- val isVarargs: Boolean = false,
-) {
-
- val size
- get() = this.parameters.size
-
- fun match(parameters: Array, currentPosition: Int, allowCoerce: Boolean): Boolean {
- // Check if remaining parameter count is enough
- if (!isVarargs && currentPosition + size > parameters.size) {
- return false
- }
-
- var pos = currentPosition
-
- // Check parameter types
- for ((_, expectedType) in this.parameters) {
- if (isVarargs && pos == parameters.size) {
- break
- }
- val parameter = parameters[pos++]
- if (!matchParameter(expectedType, parameter, allowCoerce)) {
- return false
- }
- }
-
- return !isVarargs || pos == parameters.size
- }
-
- enum class RequiredLevel {
- OPTIONAL, WARN_IF_ABSENT, ERROR_IF_ABSENT
- }
-
- companion object {
- private val INT_TYPES = setOf(
- PsiTypes.intType(),
- PsiTypes.shortType(),
- PsiTypes.charType(),
- PsiTypes.byteType(),
- PsiTypes.booleanType()
- )
-
- private fun matchParameter(expectedType: PsiType, parameter: PsiParameter, allowCoerce: Boolean): Boolean {
- val normalizedExpected = expectedType.normalize()
- val normalizedParameter = parameter.type.normalize()
- if (normalizedExpected == normalizedParameter) {
- return true
- }
- if (!allowCoerce || !parameter.hasAnnotation(COERCE)) {
- return false
- }
-
- if (normalizedExpected is PsiPrimitiveType) {
- if (normalizedParameter !is PsiPrimitiveType) {
- return false
- }
- return normalizedExpected in INT_TYPES && normalizedParameter in INT_TYPES
- }
- return isAssignable(normalizedParameter, normalizedExpected)
- }
- }
-}
diff --git a/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt
new file mode 100644
index 000000000..94bfc0c91
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt
@@ -0,0 +1,419 @@
+/*
+ * 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.util.IntLikeAnchorSolver
+import com.demonwav.mcdev.platform.mixin.util.ReturnTypeSolver
+import com.demonwav.mcdev.platform.mixin.util.TypeKind
+import com.demonwav.mcdev.util.SequencedSet
+import com.demonwav.mcdev.util.descriptor
+import com.demonwav.mcdev.util.emptySequencedSet
+import com.demonwav.mcdev.util.normalize
+import com.demonwav.mcdev.util.reduceFallible
+import com.demonwav.mcdev.util.sequencedSetOf
+import com.demonwav.mcdev.util.singleDistinct
+import com.demonwav.mcdev.util.singleDistinctOrNull
+import com.intellij.psi.GenericsUtil
+import com.intellij.psi.PsiAnnotation
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiManager
+import com.intellij.psi.PsiMethod
+import com.intellij.psi.PsiParameterList
+import com.intellij.psi.PsiType
+import com.intellij.psi.PsiTypes
+import com.intellij.psi.util.parentOfType
+import org.objectweb.asm.Type
+
+sealed interface SignatureSuggestion {
+ val params: List?
+ val returnType: PsiType
+ val intLikeTypes: SequencedSet
+ val coerceReturnType: Boolean
+
+ data class Param(val name: String?, val type: PsiType, val coerce: Boolean = false)
+}
+
+data class SuggestedReturnType(
+ override val returnType: PsiType,
+ val returnTypeIsIntLike: Boolean = false,
+ override val coerceReturnType: Boolean = false,
+) : SignatureSuggestion {
+ override val intLikeTypes =
+ if (returnTypeIsIntLike) sequencedSetOf(MethodSignature.TypePosition.Return) else emptySequencedSet()
+ override val params get() = null
+
+ companion object {
+ /**
+ * Returns a valid suggested return type iff one is possible while making no changes to the existing parameters.
+ */
+ fun forParams(params: PsiParameterList, signatures: List): SuggestedReturnType? {
+ val validSignatures = signatures.map { expected ->
+ expected.options.filter { it.matchesParams(params) }.ifEmpty { return null }
+ }
+
+ val solver = ReturnTypeSolver(params)
+
+ for (expected in validSignatures) {
+ solver.addExpected(expected)
+ }
+
+ return solver.solve()
+ }
+ }
+}
+
+data class SuggestedSignature(
+ override val params: List,
+ override val returnType: PsiType,
+ override val intLikeTypes: SequencedSet = emptySequencedSet(),
+ override val coerceReturnType: Boolean = false,
+) : SignatureSuggestion {
+ private fun intersectCoerce(other: SuggestedSignature, manager: PsiManager): SuggestedSignature? {
+ val intLikeTypes = linkedSetOf()
+
+ val (returnType, returnTypeIsIntLike, coerceReturnType) = mergeTypes(
+ manager,
+ this.returnType,
+ other.returnType,
+ MethodSignature.TypePosition.Return in this.intLikeTypes,
+ MethodSignature.TypePosition.Return in other.intLikeTypes,
+ ) ?: return null
+
+ if (returnTypeIsIntLike) {
+ intLikeTypes.add(MethodSignature.TypePosition.Return)
+ }
+
+ return SuggestedSignature(
+ params.withIndex().zip(other.params) { (index, a), b ->
+ val pos = MethodSignature.TypePosition.Param(index)
+ val (type, isIntLike, coerce) = mergeTypes(
+ manager,
+ a.type,
+ b.type,
+ pos in this.intLikeTypes,
+ pos in other.intLikeTypes,
+ ) ?: return null
+ if (isIntLike) {
+ intLikeTypes.add(pos)
+ }
+ val name = if (a.name == b.name) a.name else null
+ SignatureSuggestion.Param(name, type, a.coerce || b.coerce || coerce)
+ },
+ returnType,
+ SequencedSet(intLikeTypes),
+ this.coerceReturnType || other.coerceReturnType || coerceReturnType,
+ )
+ }
+
+ companion object {
+ private fun exact(
+ signature: MethodSignature,
+ takeTrailing: Int = 0,
+ intLikeAssignment: PsiType? = null,
+ ): SuggestedSignature {
+ var returnType = signature.returnType
+ val params =
+ (signature.requiredParams.asSequence() + signature.trailingParams.asSequence().take(takeTrailing)).map {
+ SignatureSuggestion.Param(
+ it.name,
+ it.type,
+ )
+ }.toMutableList()
+ if (intLikeAssignment != null) {
+ for (pos in signature.intLikePositions) {
+ when (pos) {
+ MethodSignature.TypePosition.Return -> returnType = intLikeAssignment
+ is MethodSignature.TypePosition.Param -> {
+ params[pos.index] = params[pos.index].copy(type = intLikeAssignment)
+ }
+ }
+ }
+ }
+ return SuggestedSignature(
+ params,
+ returnType,
+ if (intLikeAssignment == null) signature.intLikePositions else emptySequencedSet(),
+ )
+ }
+
+ /**
+ * Returns a suggested signature for the given `@Modify`-style signatures. The resulting signature, if any, will
+ * always have 1 parameter. Preference is given to the existing return type, if any, since this is likely to be
+ * typed before the parameters.
+ */
+ fun modifier(annotation: PsiAnnotation, signatures: List): SuggestedSignature? {
+ val parameterOptions = signatures.map { it.paramOptions }
+ val psiManager = PsiManager.getInstance(annotation.project)
+
+ val optionsByType = parameterOptions.asSequence()
+ .flatMap { it.entries }
+ .groupBy({ it.key }, { it.value })
+
+ val existingReturnType = annotation.parentOfType()?.returnType
+ val forExistingReturnType = existingReturnType?.let { optionsByType[Type.getType(it.descriptor)] }
+
+ val chosenParams = forExistingReturnType?.takeIf { it.size == parameterOptions.size }
+ ?: optionsByType.values.firstOrNull { it.size == parameterOptions.size } ?: return null
+
+ val name = chosenParams.asSequence().map { it.name }.singleDistinctOrNull() ?: "original"
+ val type = chosenParams.asSequence().map { it.type }
+ .reduce { a, b -> GenericsUtil.getLeastUpperBound(a, b, psiManager) ?: a }
+
+ return SuggestedSignature(listOf(SignatureSuggestion.Param(name, type)), type)
+ }
+
+ /**
+ * Returns a suggested signature for the given `Operation`-wrapper signatures. Trailing parameters are never
+ * used, and the Operation type is never `@Coerce`d. Doing so would make it possible to reconcile differing
+ * parameter counts in some cases, but would be confusing and impractical to use.
+ */
+ fun operationWrapper(
+ annotation: PsiAnnotation,
+ signatures: List,
+ ): SuggestedSignature? {
+ return intersect(
+ annotation,
+ signatures.map { it.signature },
+ numParams = { it.requiredParams.size },
+ )
+ }
+
+ /**
+ * Returns a suggested signature for the given `@Inject` signatures. The target method's parameters are captured
+ * if possible, and we take the longest possible prefix of the available locals from each signature. `@Coerce`
+ * is used to rectify any differences in these cases, where possible. The `CallbackInfo(Returnable)` parameter
+ * may be `@Coerce`d to `CallbackInfo` itself, but never to any other type. Doing so would make it possible to
+ * reconcile differing parameter counts in some cases, but would be confusing and impractical to use.
+ */
+ fun inject(annotation: PsiAnnotation, signatures: List): SuggestedSignature? {
+ val localsToUse =
+ coerciblePrefixLength(signatures.asSequence().map { sig -> sig.locals.asSequence().map { it.type } })
+
+ val longForm = intersect(
+ annotation,
+ signatures.map { it.longSignature },
+ numParams = { it.requiredParams.size + localsToUse },
+ )
+
+ return longForm ?: intersect(
+ annotation,
+ signatures.map { it.shortSignature ?: it.longSignature },
+ numParams = { it.requiredParams.size },
+ )
+ }
+
+ /**
+ * Returns a suggested signature for the given signatures. The signature shapes are relatively flexible with
+ * constraints as per the structure of [GeneralSignatures]. The resulting signature, if any, will have as many
+ * parameters as the longest input signature mandates, with trailing parameters being taken from any shorter
+ * signatures to fill the gaps. `@Coerce` is used to reconcile differences in parameter and return types, where
+ * the input signatures allow it.
+ */
+ fun general(annotation: PsiAnnotation, signatures: List): SuggestedSignature? {
+ val numParams = signatures.maxOf { it.params.size }
+
+ val candidatesByReturnKind = signatures.asSequence()
+ .flatMap { sig ->
+ sig.returnTypeOptions.keys.asSequence().map { it to sig.specificSignature(it) }
+ }
+ .groupBy({ it.first }, { it.second })
+ .values
+
+ for (candidates in candidatesByReturnKind) {
+ if (candidates.size < signatures.size) {
+ // Not viable
+ continue
+ }
+
+ val intersected = intersect(
+ annotation,
+ candidates,
+ numParams = { numParams },
+ ) ?: continue
+
+ return intersected
+ }
+
+ return null
+ }
+
+ /**
+ * Returns the most specific possible intersection of the given signatures, iff any exists. The returned
+ * signature, if any, is guaranteed to satisfy all the given signatures, including when some parts do not permit
+ * coercion. Parameters are taken from each signature as per [numParams].
+ */
+ private fun intersect(
+ context: PsiElement,
+ signatures: List,
+ numParams: (MethodSignature) -> Int,
+ ): SuggestedSignature? {
+ val paramsToUse = signatures
+ .map { sig ->
+ numParams(sig).also {
+ if (it !in sig.requiredParams.size..sig.requiredParams.size + sig.trailingParams.size) {
+ return null
+ }
+ }
+ }
+ .asSequence()
+ .singleDistinctOrNull()
+ ?: return null
+
+ val intLikeAnchor = run {
+ signatures.mapNotNull { it.intLikePositions.firstOrNull() }.toList()
+ .ifEmpty { return@run null }
+ .asSequence()
+ // With the currently available signature shapes, there can only ever be 1 anchor.
+ // This logic will need revisiting if that changes.
+ .singleDistinct()
+ }
+ val intLikeAssignment = intLikeAnchor?.let {
+ val solver = IntLikeAnchorSolver()
+
+ val allIntLikePositions = signatures.asSequence()
+ .flatMap { it.intLikePositions }
+ .toHashSet()
+ for (signature in signatures) {
+ for (pos in signature.allPositions(paramsToUse)) {
+ if (pos in signature.intLikePositions || pos !in allIntLikePositions) {
+ continue
+ }
+ val isTrailingParam = pos is MethodSignature.TypePosition.Param &&
+ pos.index > signature.requiredParams.lastIndex
+ val allowCoerce = isTrailingParam || signature.allowCoerceRequired
+ val isValid = solver.constrain(
+ pos.getType(signature),
+ isAnchor = pos == intLikeAnchor,
+ isHard = !allowCoerce,
+ )
+ if (!isValid) {
+ return null
+ }
+ }
+ }
+
+ solver.solve()
+ }
+
+ val suggestedSignatures = signatures.map {
+ exact(it, takeTrailing = paramsToUse - it.requiredParams.size, intLikeAssignment = intLikeAssignment)
+ }
+
+ if (suggestedSignatures.asSequence().zipWithNext { a, b -> !kindsMatch(a, b) }.any { it }) {
+ return null
+ }
+
+ val manager = PsiManager.getInstance(context.project)
+ val intersected = suggestedSignatures.asSequence()
+ .reduceFallible { acc, it -> acc.intersectCoerce(it, manager) } ?: return null
+
+ // It is fine simply to intersect and then check hard constraints, because if an input signature contains a
+ // subtype Y of X at a position where X is a hard constraint, then the intersection will coerce Y upwards
+ // to X. If it contains instead a *supertype* Z of X, then reconciliation is not possible and the following
+ // check will fail:
+ return intersected.takeIf {
+ signatures.all {
+ it.allowCoerceRequired || it.matches(intersected)
+ }
+ }
+ }
+ }
+}
+
+private fun kindsMatch(a: SuggestedSignature, b: SuggestedSignature) =
+ TypeKind.of(a.returnType) == TypeKind.of(b.returnType)
+ && a.params.size == b.params.size
+ && a.params.indices.all { TypeKind.of(a.params[it].type) == TypeKind.of(b.params[it].type) }
+
+/**
+ * Returns the largest N such that the sequences' first N types can be merged element-wise.
+ */
+private fun coerciblePrefixLength(types: Sequence>): Int {
+ val iterators = types.map { it.iterator() }.toList()
+ var i = 0
+
+ while (true) {
+ var kind: TypeKind? = null
+ var forcedIntType: PsiType? = null
+
+ for (iterator in iterators) {
+ val candidate = if (iterator.hasNext()) iterator.next() else return i
+ val candidateKind = TypeKind.of(candidate)
+
+ when (kind) {
+ null -> kind = candidateKind
+ candidateKind -> {}
+ else -> return i
+ }
+
+ if (candidateKind == TypeKind.INT_LIKE && candidate != PsiTypes.intType()) {
+ when (forcedIntType) {
+ null -> forcedIntType = candidate
+ candidate -> {}
+ else -> return i
+ }
+ }
+ }
+
+ i++
+ }
+}
+
+/**
+ * Merges the two types with regards to `@Coerce` behavior. Returns the merged type, whether the merged type is a
+ * free int-like type, and whether the merge **newly** requires `@Coerce`.
+ *
+ * **Preconditions:** The types must be of the same kind, and if either type is a free int-like type, the other must be
+ * too. (Fully solving int-like types is left to the caller.)
+ */
+private fun mergeTypes(
+ manager: PsiManager,
+ a: PsiType,
+ b: PsiType,
+ aIsIntLike: Boolean,
+ bIsIntLike: Boolean,
+): TypeMergeResult? = when (TypeKind.of(a)) {
+ TypeKind.OBJECT -> {
+ TypeMergeResult(
+ GenericsUtil.getLeastUpperBound(a, b, manager)!!,
+ isIntLike = false,
+ coerce = a.normalize() != b.normalize(),
+ )
+ }
+
+ TypeKind.INT_LIKE -> {
+ when {
+ aIsIntLike && bIsIntLike -> TypeMergeResult(a, isIntLike = true, coerce = false)
+ aIsIntLike || bIsIntLike -> error("Int-like type should have been forced")
+ a == b -> TypeMergeResult(a, isIntLike = false, coerce = false)
+ a == PsiTypes.intType() -> TypeMergeResult(b, isIntLike = false, coerce = true)
+ b == PsiTypes.intType() -> TypeMergeResult(a, isIntLike = false, coerce = true)
+ else -> null
+ }
+ }
+
+ else -> {
+ require(a == b)
+ TypeMergeResult(a, isIntLike = false, coerce = false)
+ }
+}
+
+private data class TypeMergeResult(val type: PsiType, val isIntLike: Boolean, val coerce: Boolean)
diff --git a/src/main/kotlin/platform/mixin/inspection/mixinextras/InjectLocalCaptureReplaceWithLocalInspection.kt b/src/main/kotlin/platform/mixin/inspection/mixinextras/InjectLocalCaptureReplaceWithLocalInspection.kt
index 9fbb97c99..bafd1b048 100644
--- a/src/main/kotlin/platform/mixin/inspection/mixinextras/InjectLocalCaptureReplaceWithLocalInspection.kt
+++ b/src/main/kotlin/platform/mixin/inspection/mixinextras/InjectLocalCaptureReplaceWithLocalInspection.kt
@@ -20,7 +20,7 @@
package com.demonwav.mcdev.platform.mixin.inspection.mixinextras
-import com.demonwav.mcdev.platform.mixin.handlers.InjectorAnnotationHandler
+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.fix.AnnotationAttributeFix
@@ -101,7 +101,7 @@ class InjectLocalCaptureReplaceWithLocalInspection : MixinInspection() {
// resolve the local variables at the targets
val handler = MixinAnnotationHandler.forMixinAnnotation(MixinConstants.Annotations.INJECT)
- as InjectorAnnotationHandler
+ as InsnInjectorAnnotationHandler
val module = annotation.findModule() ?: return
val localsAndParamCountsAtTargets = handler.resolveInstructions(annotation).map { result ->
val locals = LocalVariables.getLocals(
diff --git a/src/main/kotlin/platform/mixin/inspection/mixinextras/LocalArgsOnlyInspection.kt b/src/main/kotlin/platform/mixin/inspection/mixinextras/LocalArgsOnlyInspection.kt
index c3ad5c3fb..218fce8c3 100644
--- a/src/main/kotlin/platform/mixin/inspection/mixinextras/LocalArgsOnlyInspection.kt
+++ b/src/main/kotlin/platform/mixin/inspection/mixinextras/LocalArgsOnlyInspection.kt
@@ -20,7 +20,7 @@
package com.demonwav.mcdev.platform.mixin.inspection.mixinextras
-import com.demonwav.mcdev.platform.mixin.handlers.InjectorAnnotationHandler
+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.fix.AnnotationAttributeFix
@@ -54,7 +54,7 @@ class LocalArgsOnlyInspection : MixinInspection() {
val method = parameter.declarationScope as? PsiMethod ?: return
val (injector, injectorAnnotation) = method.annotations.mapFirstNotNull { annotation ->
- (MixinAnnotationHandler.forMixinAnnotation(annotation, holder.project) as? InjectorAnnotationHandler)?.let { it to annotation }
+ (MixinAnnotationHandler.forMixinAnnotation(annotation, holder.project) as? InsnInjectorAnnotationHandler)?.let { it to annotation }
} ?: return
val localType = parameter.type.unwrapLocalRef()
diff --git a/src/main/kotlin/platform/mixin/inspection/mixinextras/LocalMayUseNameInspection.kt b/src/main/kotlin/platform/mixin/inspection/mixinextras/LocalMayUseNameInspection.kt
index d21ae290d..7b5e52b2c 100644
--- a/src/main/kotlin/platform/mixin/inspection/mixinextras/LocalMayUseNameInspection.kt
+++ b/src/main/kotlin/platform/mixin/inspection/mixinextras/LocalMayUseNameInspection.kt
@@ -20,11 +20,11 @@
package com.demonwav.mcdev.platform.mixin.inspection.mixinextras
-import com.demonwav.mcdev.platform.mixin.handlers.InjectorAnnotationHandler
+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.ModifyVariableMayUseNameInspection.Util.getVariableNameToIntroduce
import com.demonwav.mcdev.platform.mixin.inspection.injector.ModifyVariableMayUseNameInspection.ReplaceWithNameFix
+import com.demonwav.mcdev.platform.mixin.inspection.injector.ModifyVariableMayUseNameInspection.Util.getVariableNameToIntroduce
import com.demonwav.mcdev.platform.mixin.util.LocalInfo
import com.demonwav.mcdev.platform.mixin.util.MixinConstants
import com.demonwav.mcdev.platform.mixin.util.unwrapLocalRef
@@ -72,7 +72,7 @@ class LocalMayUseNameInspection : MixinInspection() {
}
val (injector, injectorAnnotation) = method.annotations.mapFirstNotNull { annotation ->
- (MixinAnnotationHandler.forMixinAnnotation(annotation, holder.project) as? InjectorAnnotationHandler)?.let { it to annotation }
+ (MixinAnnotationHandler.forMixinAnnotation(annotation, holder.project) as? InsnInjectorAnnotationHandler)?.let { it to annotation }
} ?: return
val variableName = getVariableNameToIntroduce(localInfo, injector, injectorAnnotation) ?: return
diff --git a/src/main/kotlin/platform/mixin/inspection/mixinextras/UnnecessaryMutableLocalInspection.kt b/src/main/kotlin/platform/mixin/inspection/mixinextras/UnnecessaryMutableLocalInspection.kt
index 1127e6cc4..128141666 100644
--- a/src/main/kotlin/platform/mixin/inspection/mixinextras/UnnecessaryMutableLocalInspection.kt
+++ b/src/main/kotlin/platform/mixin/inspection/mixinextras/UnnecessaryMutableLocalInspection.kt
@@ -20,7 +20,7 @@
package com.demonwav.mcdev.platform.mixin.inspection.mixinextras
-import com.demonwav.mcdev.platform.mixin.handlers.InjectorAnnotationHandler
+import com.demonwav.mcdev.platform.mixin.handlers.InsnInjectorAnnotationHandler
import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler
import com.demonwav.mcdev.platform.mixin.handlers.mixinextras.WrapOperationHandler
import com.demonwav.mcdev.platform.mixin.inspection.MixinInspection
@@ -58,7 +58,7 @@ class UnnecessaryMutableLocalInspection : MixinInspection() {
val hasValidMixinAnnotation = method.annotations.any { ann ->
MixinAnnotationHandler.forMixinAnnotation(ann, project)
// Mutable Local references do have different semantics inside a WrapOperation.
- ?.let { it is InjectorAnnotationHandler && it !is WrapOperationHandler } == true
+ ?.let { it is InsnInjectorAnnotationHandler && it !is WrapOperationHandler } == true
}
if (!hasValidMixinAnnotation) {
return
diff --git a/src/main/kotlin/platform/mixin/inspection/mixinextras/UnresolvedLocalCaptureInspection.kt b/src/main/kotlin/platform/mixin/inspection/mixinextras/UnresolvedLocalCaptureInspection.kt
index c22ad43c2..39705133a 100644
--- a/src/main/kotlin/platform/mixin/inspection/mixinextras/UnresolvedLocalCaptureInspection.kt
+++ b/src/main/kotlin/platform/mixin/inspection/mixinextras/UnresolvedLocalCaptureInspection.kt
@@ -20,7 +20,7 @@
package com.demonwav.mcdev.platform.mixin.inspection.mixinextras
-import com.demonwav.mcdev.platform.mixin.handlers.InjectorAnnotationHandler
+import com.demonwav.mcdev.platform.mixin.handlers.InsnInjectorAnnotationHandler
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
@@ -50,7 +50,7 @@ class UnresolvedLocalCaptureInspection : MixinInspection() {
val method = parameter.findContainingMethod() ?: return
val targets = method.annotations.mapFirstNotNull { annotation ->
val handler =
- MixinAnnotationHandler.forMixinAnnotation(annotation, holder.project) as? InjectorAnnotationHandler
+ MixinAnnotationHandler.forMixinAnnotation(annotation, holder.project) as? InsnInjectorAnnotationHandler
?: return@mapFirstNotNull null
handler.resolveInstructions(annotation)
} ?: return
diff --git a/src/main/kotlin/platform/mixin/util/AsmUtil.kt b/src/main/kotlin/platform/mixin/util/AsmUtil.kt
index 80e768b67..88b5b2802 100644
--- a/src/main/kotlin/platform/mixin/util/AsmUtil.kt
+++ b/src/main/kotlin/platform/mixin/util/AsmUtil.kt
@@ -75,7 +75,6 @@ import com.intellij.psi.PsiMethodReferenceExpression
import com.intellij.psi.PsiModifier
import com.intellij.psi.PsiModifierList
import com.intellij.psi.PsiParameter
-import com.intellij.psi.PsiParameterList
import com.intellij.psi.PsiParameterListOwner
import com.intellij.psi.PsiType
import com.intellij.psi.PsiTypes
@@ -830,8 +829,21 @@ private fun MethodNode.getOffset(clazz: ClassNode?): Int {
}
}
-fun MethodNode.getParameter(clazz: ClassNode, index: Int, parameterList: PsiParameterList): PsiParameter? {
- return parameterList.parameters.getOrNull(index - getOffset(clazz))
+private fun PsiMethod.getOffset(): Int {
+ val clazz = containingClass ?: return 0
+ return if (this.isConstructor) {
+ when {
+ clazz.isEnum -> 2
+ clazz.containingClass != null && !clazz.hasModifierProperty(PsiModifier.STATIC) -> 1
+ else -> 0
+ }
+ } else {
+ 0
+ }
+}
+
+fun PsiMethod.getBytecodeParameter(index: Int): PsiParameter? {
+ return parameterList.parameters.getOrNull(index - getOffset())
}
/**
diff --git a/src/main/kotlin/platform/mixin/util/IntLikeAnchorSolver.kt b/src/main/kotlin/platform/mixin/util/IntLikeAnchorSolver.kt
new file mode 100644
index 000000000..814bdde71
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/util/IntLikeAnchorSolver.kt
@@ -0,0 +1,97 @@
+/*
+ * 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.util
+
+import com.intellij.psi.PsiType
+import com.intellij.psi.PsiTypes
+import java.util.EnumSet
+
+/**
+ * Represents a constraint solver for the anchor type of a signature containing free int-like types.
+ */
+class IntLikeAnchorSolver {
+ private val candidates = EnumSet.allOf(IntType::class.java)
+ private var hasIntPreference = false
+ private var hasLeafPreference = false
+
+ /**
+ * Adds the given constraint to the solver and returns whether a solution is still possible.
+ */
+ fun constrain(desiredType: PsiType, isAnchor: Boolean, isHard: Boolean): Boolean {
+ val type = IntType.of(desiredType) ?: return false
+ when {
+ isHard && (isAnchor || type == IntType.INT) -> candidates.retainAll(EnumSet.of(type))
+ type == IntType.INT -> hasIntPreference = true
+ isAnchor -> candidates.retainAll(EnumSet.of(type))
+ else -> {
+ candidates.retainAll(EnumSet.of(type, IntType.INT))
+ hasLeafPreference = true
+ }
+ }
+ return candidates.isNotEmpty()
+ }
+
+ /**
+ * Returns either a chosen anchor type or `null` if any anchor type will suffice.
+ *
+ * **Precondition:** A solution is possible.
+ */
+ fun solve(): PsiType? {
+ require(candidates.isNotEmpty())
+
+ candidates.singleOrNull()?.let { return it.type }
+
+ return when {
+ // int preference is the strongest, since choosing a leaf type instead would require a @Coerce *and* the
+ // use of an undesired leaf type
+ hasIntPreference -> {
+ // Cannot have 2 leaves without int
+ check(IntType.INT in candidates)
+ PsiTypes.intType()
+ }
+ // Leaf preference is the next strongest, since choosing int instead would require a @Coerce
+ // NB leaf preferences can only come from non-anchor positions
+ hasLeafPreference -> {
+ // Cannot have 2 leaves and a leaf preference
+ candidates.single { it != IntType.INT }.type
+ }
+ else -> {
+ // We have multiple options and no preference at all
+ check(candidates.size == IntType.entries.size)
+ null
+ }
+ }
+ }
+}
+
+private enum class IntType(val type: PsiType) {
+ INT(PsiTypes.intType()),
+ BYTE(PsiTypes.byteType()),
+ SHORT(PsiTypes.shortType()),
+ CHAR(PsiTypes.charType()),
+ BOOLEAN(PsiTypes.booleanType());
+
+ companion object {
+ private val lookup = entries.associateBy { it.type }
+
+ fun of(type: PsiType) = lookup[type]
+ }
+}
diff --git a/src/main/kotlin/platform/mixin/util/LocalInfo.kt b/src/main/kotlin/platform/mixin/util/LocalInfo.kt
index 0b0d9565e..9ceb78575 100644
--- a/src/main/kotlin/platform/mixin/util/LocalInfo.kt
+++ b/src/main/kotlin/platform/mixin/util/LocalInfo.kt
@@ -88,15 +88,13 @@ class LocalInfo(
): List? {
val locals = getLocals(module, targetClass, methodNode, insn)
?.drop(if (methodNode.hasAccess(Opcodes.ACC_STATIC)) 0 else 1)
+ ?.filterNotNull()
?: return null
val typeDesc = type?.descriptor
if (ordinal != null) {
val ordinals = mutableMapOf()
val result = mutableListOf()
for (local in locals) {
- if (local == null) {
- continue
- }
val ordinal = ordinals[local.desc] ?: 0
ordinals[local.desc!!] = ordinal + 1
if (ordinal == this.ordinal && (!matchType || typeDesc == null || local.desc == typeDesc)) {
@@ -107,7 +105,7 @@ class LocalInfo(
}
if (index != null) {
- val local = locals.firstOrNull { it?.index == index }
+ val local = locals.firstOrNull { it.index == index }
if (local != null) {
if (!matchType || typeDesc == null || local.desc == typeDesc) {
return listOf(local)
@@ -119,9 +117,6 @@ class LocalInfo(
if (names.isNotEmpty()) {
val result = mutableListOf()
for (local in locals) {
- if (local == null) {
- continue
- }
if (names.contains(local.name)) {
if (!matchType || typeDesc == null || local.desc == typeDesc) {
result += local
@@ -132,15 +127,13 @@ class LocalInfo(
}
// implicit mode
- if (mode == CollectVisitor.Mode.COMPLETION) {
- return locals.asSequence()
- .filterNotNull()
- .filter { local -> locals.count { it?.desc == local.desc } == 1 }
- .toList()
+ if (!mode.assumeCorrectSignature) {
+ val byType = locals.asSequence().filter { it.desc != null }.groupBy { it.desc!! }
+ return byType.values.mapNotNull { it.singleOrNull() }
}
return if (matchType && typeDesc != null) {
- locals.singleOrNull { it?.desc == typeDesc }?.let { listOf(it) } ?: emptyList()
+ locals.singleOrNull { it.desc == typeDesc }?.let { listOf(it) } ?: emptyList()
} else {
emptyList()
}
diff --git a/src/main/kotlin/platform/mixin/util/Mixin.kt b/src/main/kotlin/platform/mixin/util/Mixin.kt
index 2181d55b4..f12a8b1ea 100644
--- a/src/main/kotlin/platform/mixin/util/Mixin.kt
+++ b/src/main/kotlin/platform/mixin/util/Mixin.kt
@@ -33,6 +33,7 @@ import com.demonwav.mcdev.util.cached
import com.demonwav.mcdev.util.computeStringArray
import com.demonwav.mcdev.util.constantValue
import com.demonwav.mcdev.util.findModule
+import com.demonwav.mcdev.util.normalize
import com.demonwav.mcdev.util.resolveClassArray
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.Project
@@ -329,3 +330,19 @@ fun PsiModifierListOwner.shouldDoMixinAccessChecks(): Boolean {
return true
}
+
+private val INT_TYPES = setOf(
+ PsiTypes.intType(),
+ PsiTypes.shortType(),
+ PsiTypes.charType(),
+ PsiTypes.byteType(),
+ PsiTypes.booleanType(),
+)
+
+fun checkCoerce(expected: PsiType, actual: PsiType, coerce: Boolean, expectedIntLike: Boolean): Boolean = when {
+ expectedIntLike -> actual in INT_TYPES
+ actual.normalize() == expected.normalize() -> true
+ !coerce -> false
+ actual in INT_TYPES -> expected == PsiTypes.intType()
+ else -> isAssignable(actual.normalize(), expected.normalize())
+}
diff --git a/src/main/kotlin/platform/mixin/util/ReturnTypeSolver.kt b/src/main/kotlin/platform/mixin/util/ReturnTypeSolver.kt
new file mode 100644
index 000000000..0f8674ffe
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/util/ReturnTypeSolver.kt
@@ -0,0 +1,261 @@
+/*
+ * 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.util
+
+import com.demonwav.mcdev.platform.mixin.inspection.injector.MethodSignature
+import com.demonwav.mcdev.platform.mixin.inspection.injector.SuggestedReturnType
+import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Annotations.COERCE
+import com.demonwav.mcdev.util.MutableSequencedMap
+import com.demonwav.mcdev.util.PrioritySet
+import com.demonwav.mcdev.util.normalize
+import com.demonwav.mcdev.util.singleDistinct
+import com.intellij.psi.CommonClassNames
+import com.intellij.psi.GenericsUtil
+import com.intellij.psi.PsiArrayType
+import com.intellij.psi.PsiManager
+import com.intellij.psi.PsiParameterList
+import com.intellij.psi.PsiPrimitiveType
+import com.intellij.psi.PsiType
+import com.intellij.psi.PsiTypes
+
+/**
+ * Represents a constraint solver for the return type of a signature given a fixed list of parameters.
+ */
+class ReturnTypeSolver(private val parameterList: PsiParameterList) {
+ private val manager = PsiManager.getInstance(parameterList.project)
+ private val objectReturnOptionsCache = hashMapOf>>()
+ private val suggestionsByType = hashMapOf>()
+ private val typeOptions = PrioritySet()
+ private var numExpected = 0
+
+ /**
+ * Adds a new constraint that the chosen return type must match at least one of the given signatures.
+ */
+ fun addExpected(signatures: List) {
+ val expectedIndex = numExpected++
+ for (signature in signatures) {
+ for ((option, priority) in signature.returnTypeOptions()) {
+ val key = TypeKey.of(option)
+ typeOptions.add(key, priority)
+ suggestionsByType.getOrPut(key, ::linkedMapOf).putIfAbsent(expectedIndex, option)
+ }
+ }
+ }
+
+ /**
+ * Returns a suggested return type, or null if it is not possible to satisfy all the constraints.
+ */
+ fun solve(): SuggestedReturnType? {
+ val suggestions = typeOptions.asSequence()
+ .map { suggestionsByType.getValue(it) }.firstOrNull { it.size == numExpected }?.values
+ return suggestions?.let { intersect(it) }
+ }
+
+ /**
+ * Returns the possible return types that this signature can support, along with their priority.
+ *
+ * **Precondition:** The parameter list is valid according to the signature.
+ */
+ private fun MethodSignature.returnTypeOptions(): List> {
+ if (MethodSignature.TypePosition.Return in intLikePositions) {
+ val params = parameterList.parameters
+ val anchor = (intLikePositions.first() as? MethodSignature.TypePosition.Param)
+ ?.let { params[it.index].type }
+ return when (anchor) {
+ PsiTypes.intType() -> {
+ intReturnOptions(allowCoerceRequired)
+ }
+ null -> {
+ // The return type itself is the anchor
+ val intLikeParams = intLikePositions.asSequence()
+ .mapNotNull { it.getParam(params) }
+ .groupBy { it.type }
+ when {
+ intLikeParams.isEmpty() -> {
+ // Only the return type is int-like, free choice
+ val results = mutableListOf(
+ SuggestedReturnType(PsiTypes.intType(), returnTypeIsIntLike = true) withPriority 0
+ )
+ for (specific in intLikeTypes) {
+ // We don't know what the return type should be, so we avoid influencing the preference
+ // order.
+ results.add(SuggestedReturnType(specific) withPriority Int.MAX_VALUE)
+ }
+ results
+ }
+ PsiTypes.intType() in intLikeParams -> {
+ // Only int can be coerced to int (can also be coerced to anything else we found)
+ listOf(SuggestedReturnType(PsiTypes.intType()) withPriority 0)
+ }
+ intLikeParams.size == 1 -> {
+ // Take the specific leaf type we found, plus int iff all the params have @Coerce.
+ // int is less preferable since it doesn't match exactly.
+ val leaf = intLikeParams.keys.single()
+ val results = mutableListOf(
+ SuggestedReturnType(leaf) withPriority 0
+ )
+ if (allowCoerceRequired && intLikeParams.values.single().all { it.hasAnnotation(COERCE) }) {
+ results.add(
+ SuggestedReturnType(PsiTypes.intType()) withPriority 1
+ )
+ }
+ results
+ }
+ else -> {
+ // Only int can be coerced to multiple types
+ check(allowCoerceRequired)
+ for (params in intLikeParams.values) {
+ // We double-check that the parameters are valid as the caller promised.
+ // NB We know there are no int params, so all must be coerced.
+ check(params.all { it.hasAnnotation(COERCE) })
+ }
+ listOf(SuggestedReturnType(PsiTypes.intType()) withPriority 0)
+ }
+ }
+ }
+ else -> {
+ // Anchor is a leaf type which can only be coerced to itself
+ listOf(SuggestedReturnType(anchor) withPriority 0)
+ }
+ }
+ }
+ return when {
+ returnType == PsiTypes.intType() -> intReturnOptions(allowCoerceRequired)
+ TypeKind.of(returnType) == TypeKind.OBJECT -> objectReturnOptions(returnType, allowCoerceRequired)
+ else -> listOf(SuggestedReturnType(returnType) withPriority 0)
+ }
+ }
+
+ /**
+ * Returns a suggested return type which is the LUB of all the given types.
+ *
+ * **Precondition:** [suggestions] must be non-empty and every suggestion in [suggestions] must have the same type
+ * as considered by [TypeKey].
+ */
+ private fun intersect(suggestions: Iterable): SuggestedReturnType {
+ val types = suggestions.asSequence().map { it.returnType }
+ val kind = types.map { TypeKind.of(it) }.singleDistinct()
+ val mergedType = when (kind) {
+ TypeKind.OBJECT -> types.reduce { acc, it -> GenericsUtil.getLeastUpperBound(acc, it, manager)!! }
+ else -> types.singleDistinct()
+ }
+ return SuggestedReturnType(
+ mergedType,
+ returnTypeIsIntLike = suggestions.asSequence().map { it.returnTypeIsIntLike }.singleDistinct(),
+ coerceReturnType = suggestions.any { it.coerceReturnType },
+ )
+ }
+
+ /**
+ * Yields all valid supertypes (inclusive) of the given type, at most once per raw type, with priority equal to the
+ * number of traversal steps required to reach the supertype.
+ */
+ private fun objectReturnOptions(type: PsiType, allowCoerce: Boolean): List> =
+ if (!allowCoerce) {
+ listOf(SuggestedReturnType(type) withPriority 0)
+ } else {
+ objectReturnOptionsCache.getOrPut(type) {
+ val result = mutableListOf>()
+
+ val queue = ArrayDeque(listOf(SuggestedReturnType(type) withPriority 0))
+ val visited = hashSetOf(type.normalize())
+
+ while (queue.isNotEmpty()) {
+ val (next, priority) = queue.removeFirst().also { result.add(it) }
+ for (directSuper in next.returnType.directSupertypes()) {
+ if (visited.add(directSuper.normalize())) {
+ queue.addLast(
+ SuggestedReturnType(
+ directSuper,
+ coerceReturnType = true,
+ ) withPriority priority + 1
+ )
+ }
+ }
+ }
+
+ result
+ }
+ }
+
+ private fun PsiType.directSupertypes(): List = when (this) {
+ is PsiArrayType -> {
+ if (componentType is PsiPrimitiveType || componentType.equalsToText(CommonClassNames.JAVA_LANG_OBJECT)) {
+ arraySuperTypes.map { PsiType.getTypeByName(it, manager.project, parameterList.resolveScope) }
+ } else {
+ componentType.directSupertypes().map { it.createArrayType() }
+ }
+ }
+ else -> {
+ val superTypeArray = superTypes
+ if (superTypeArray.size <= 1) {
+ superTypeArray.toList()
+ } else {
+ // Delay java.lang.Object since it is necessarily less specific than any other type
+ superTypeArray.filterNot { it.equalsToText(CommonClassNames.JAVA_LANG_OBJECT) }
+ }
+ }
+ }
+
+ private companion object {
+ private val intLikeTypes = listOf(
+ PsiTypes.intType(),
+ PsiTypes.booleanType(),
+ PsiTypes.charType(),
+ PsiTypes.byteType(),
+ PsiTypes.shortType(),
+ )
+
+ private val intReturnOptions = intLikeTypes.map {
+ val needsCoerce = it != PsiTypes.intType()
+ SuggestedReturnType(
+ it,
+ coerceReturnType = needsCoerce,
+ ) withPriority (if (needsCoerce) 1 else 0)
+ }
+
+ private val arraySuperTypes = listOf(
+ CommonClassNames.JAVA_IO_SERIALIZABLE,
+ CommonClassNames.JAVA_LANG_CLONEABLE,
+ )
+
+ private fun intReturnOptions(allowCoerce: Boolean): List> =
+ if (allowCoerce) intReturnOptions else listOf(SuggestedReturnType(PsiTypes.intType()) withPriority 0)
+ }
+}
+
+private data class Prioritized(val element: T, val priority: Int)
+
+private infix fun T.withPriority(priority: Int) = Prioritized(this, priority)
+
+private sealed interface TypeKey {
+ data class Type(val type: PsiType) : TypeKey
+
+ data object IntLike : TypeKey
+
+ companion object {
+ fun of(suggestion: SuggestedReturnType) = if (suggestion.returnTypeIsIntLike) {
+ IntLike
+ } else {
+ Type(suggestion.returnType.normalize())
+ }
+ }
+}
diff --git a/src/main/kotlin/platform/mixin/util/TypeKind.kt b/src/main/kotlin/platform/mixin/util/TypeKind.kt
new file mode 100644
index 000000000..7eb6ac301
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/util/TypeKind.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.util
+
+import com.intellij.psi.PsiType
+import com.intellij.psi.PsiTypes
+
+enum class TypeKind {
+ OBJECT,
+ INT_LIKE,
+ FLOAT,
+ DOUBLE,
+ LONG,
+ VOID;
+
+ companion object {
+ private val typeMap = mapOf(
+ PsiTypes.byteType() to INT_LIKE,
+ PsiTypes.charType() to INT_LIKE,
+ PsiTypes.intType() to INT_LIKE,
+ PsiTypes.shortType() to INT_LIKE,
+ PsiTypes.booleanType() to INT_LIKE,
+ PsiTypes.doubleType() to DOUBLE,
+ PsiTypes.floatType() to FLOAT,
+ PsiTypes.longType() to LONG,
+ PsiTypes.voidType() to VOID,
+ )
+
+ fun of(type: PsiType) = typeMap[type] ?: OBJECT
+ }
+}
diff --git a/src/main/kotlin/util/PrioritySet.kt b/src/main/kotlin/util/PrioritySet.kt
new file mode 100644
index 000000000..17c61642b
--- /dev/null
+++ b/src/main/kotlin/util/PrioritySet.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.util
+
+/**
+ * Represents a prioritised set of elements. Elements can only be present once and assume the lowest priority value with
+ * which they were added. Lower priority values indicate more important elements. Iteration yields the elements in
+ * priority order, with ties broken by insertion order.
+ */
+class PrioritySet : Iterable {
+ private val buckets = sortedMapOf>()
+ private val priorities = hashMapOf()
+
+ fun add(element: T, priority: Int) {
+ val existing = priorities[element]
+ when {
+ existing == null -> {}
+ priority < existing -> buckets.getValue(existing).remove(element)
+ priority >= existing -> return
+ }
+ priorities[element] = priority
+ buckets.getOrPut(priority, ::linkedSetOf).add(element)
+ }
+
+ override fun iterator(): Iterator =
+ buckets.values.asSequence().flatten().iterator()
+}
diff --git a/src/main/kotlin/util/sequenced-collections.kt b/src/main/kotlin/util/sequenced-collections.kt
new file mode 100644
index 000000000..bc30ff97f
--- /dev/null
+++ b/src/main/kotlin/util/sequenced-collections.kt
@@ -0,0 +1,59 @@
+/*
+ * 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