User: llamalad7
Date: 09 Oct 26 11:29
Revision: 8539df1c74008cade4a10470748e1794e7dd6277
Summary:
Feature/method signature intersection (#2647)
* Refactor: Rework expected MethodSignatures.
- ParameterGroups are gone, with every signature now effectively containing a required leading group and an optional vararg trailing group. The prior flexibility made working them easy to get wrong, for example in the matching logic which would have been incorrect for leading vararg groups.
- Inject previously contained 2 optional groups (the target's params and the captured locals) but this was in fact incorrect, since the captured params are required if we want to capture any locals, so it is now (better) represented as 2 separate signature options.
- The distinction between `WARN_IF_ABSENT` and `ERROR_IF_ABSENT` is entirely removed. It was effectively unused since it only applied to captured Inject locals, which are varargs and therefore never "absent" (a separate inspection handles unused LocalCapture).
- ModifyArgs is in fact all-or-nothing wrt capturing the target parameters, and this is now reflected.
* Fix: Disallow non-int int-like types in @ModifyConstant.
Not sure why these are there, Mixin doesn't allow them.
* Fix: Disallow `nullValue` in `@ModifyConstant`.
It does not work and cannot ever work.
* stash
* stash
* finish general intersection
* cleanup
* stash
* stash
* fix general intersection
* stash
* fix tests
* return type only suggestions
* prioritise existing return type in modifier suggestions
* restore BeforeConstant's behaviour of ensuring only 1 discriminator is specified, and supporting wildcard matches
* Fix: Support `.class` constants in `@ModifyConstant`.
* Switch `@ModifyConstant` to a general signature type.
* int coercion kinda fixed ish
* fix int coercion properly
* cleanup
* proper int-like logic
* cleanup
* some docs and fixes
* fix return type solving
* Cache walked supertypes and improve handling of Object as a supertype.
* Cleanup
* Cleanup
* Small fixes
* Small fixes
* Fix: Suggest "ci" for coerced callba...
TeamCity URL: http://ci.mcdev.io:80/viewModification.html?tab=vcsModificationFiles&modId=10643&personal=false
Index: gradle/libs.versions.toml
===================================================================
--- gradle/libs.versions.toml (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
+++ gradle/libs.versions.toml (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -68,7 +68,7 @@
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"
Index: mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInComplex.java
===================================================================
--- mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInComplex.java (revision 8539df1c74008cade4a10470748e1794e7dd6277)
+++ mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInComplex.java (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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) {
+ }
+}
Index: mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInModifyArg.java
===================================================================
--- mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInModifyArg.java (revision 8539df1c74008cade4a10470748e1794e7dd6277)
+++ mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInModifyArg.java (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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) {
+ }
+}
Index: mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInModifyConstant.java
===================================================================
--- mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInModifyConstant.java (revision 8539df1c74008cade4a10470748e1794e7dd6277)
+++ mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInModifyConstant.java (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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);
+ }
+}
Index: mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInModifyExpressionValue.java
===================================================================
--- mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInModifyExpressionValue.java (revision 8539df1c74008cade4a10470748e1794e7dd6277)
+++ mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInModifyExpressionValue.java (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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;
+ }
+}
Index: mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInModifyReceiver.java
===================================================================
--- mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInModifyReceiver.java (revision 8539df1c74008cade4a10470748e1794e7dd6277)
+++ mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInModifyReceiver.java (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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) {
+ }
+}
Index: mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInModifyReturnValue.java
===================================================================
--- mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInModifyReturnValue.java (revision 8539df1c74008cade4a10470748e1794e7dd6277)
+++ mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInModifyReturnValue.java (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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;
+ }
+}
Index: mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInModifyVariable.java
===================================================================
--- mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInModifyVariable.java (revision 8539df1c74008cade4a10470748e1794e7dd6277)
+++ mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInModifyVariable.java (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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;
+ }
+}
Index: mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInRedirect.java
===================================================================
--- mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInRedirect.java (revision 8539df1c74008cade4a10470748e1794e7dd6277)
+++ mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInRedirect.java (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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;
+ }
+}
Index: mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInWrapMethod.java
===================================================================
--- mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInWrapMethod.java (revision 8539df1c74008cade4a10470748e1794e7dd6277)
+++ mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInWrapMethod.java (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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) {
+ }
+}
Index: mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInWrapOperation.java
===================================================================
--- mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInWrapOperation.java (revision 8539df1c74008cade4a10470748e1794e7dd6277)
+++ mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInWrapOperation.java (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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) {
+ }
+}
Index: mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInWrapWithCondition.java
===================================================================
--- mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInWrapWithCondition.java (revision 8539df1c74008cade4a10470748e1794e7dd6277)
+++ mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInWrapWithCondition.java (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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) {
+ }
+}
Index: src/main/kotlin/platform/mixin/expression/MEExpressionMatchUtil.kt
===================================================================
--- src/main/kotlin/platform/mixin/expression/MEExpressionMatchUtil.kt (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
+++ src/main/kotlin/platform/mixin/expression/MEExpressionMatchUtil.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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 @@
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
}
Index: src/main/kotlin/platform/mixin/expression/gui/MEShowFlowAction.kt
===================================================================
--- src/main/kotlin/platform/mixin/expression/gui/MEShowFlowAction.kt (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
+++ src/main/kotlin/platform/mixin/expression/gui/MEShowFlowAction.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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.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 @@
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)
Index: src/main/kotlin/platform/mixin/handlers/InjectAnnotationHandler.kt
===================================================================
--- src/main/kotlin/platform/mixin/handlers/InjectAnnotationHandler.kt (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
+++ src/main/kotlin/platform/mixin/handlers/InjectAnnotationHandler.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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.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
+ val targetParams = collectTargetMethodParameters(annotation.project, targetClass, targetMethod)
- // 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)!!,
- )
- },
- ),
- ),
- )
-
// 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)
+ 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 }
+ ?.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()
- }
- }
- }
+ ?: return@let
- if (commonLocalsPrefix != null) {
- val elementFactory = JavaPsiFacade.getElementFactory(annotation.project)
+ val elementFactory = JavaPsiFacade.getElementFactory(annotation.project)
- val localParams = commonLocalsPrefix.map { local ->
+ capturedLocals = locals.map { local ->
- val type =
- Type.getType(local.desc).toPsiType(elementFactory, annotation.parentOfType())
- sanitizedParameter(type, local.name)
- }
+ 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,
- ),
+ }
+
+ return ExpectedSignatures.Valid(
+ InjectSignatures.create(
+ annotation,
+ targetParams,
+ returnType,
+ capturedLocals,
+ ) ?: return ExpectedSignatures.Invalid
- )
- }
+ )
+ }
- }
- }
- return listOf(MethodSignature(result, PsiTypes.voidType()))
+ 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
Index: src/main/kotlin/platform/mixin/handlers/InjectorAnnotationHandler.kt
===================================================================
--- src/main/kotlin/platform/mixin/handlers/InjectorAnnotationHandler.kt (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
+++ src/main/kotlin/platform/mixin/handlers/InjectorAnnotationHandler.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -24,23 +24,20 @@
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.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 @@
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?
+ targets: List,
+ ): SuggestedSignature?
- open fun isInsnAllowed(insn: AbstractInsnNode, decorations: Map): Boolean {
- return true
- }
-
- open val allowedInsnDescription = "all instructions"
-
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,13 +158,19 @@
}
}
-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
override val mixinExtrasExpressionContextType = ExpressionContext.Type.CUSTOM
Index: src/main/kotlin/platform/mixin/handlers/InsnInjectorAnnotationHandler.kt
===================================================================
--- src/main/kotlin/platform/mixin/handlers/InsnInjectorAnnotationHandler.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
+++ src/main/kotlin/platform/mixin/handlers/InsnInjectorAnnotationHandler.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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<*>)
+}
Index: src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt
===================================================================
--- src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
+++ src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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.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,96 +52,65 @@
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")
+ val argTypes = Type.getArgumentTypes(insn.desc)
- 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()
- }
-
- if (validSingleArgTypes.isEmpty()) {
- validSingleArgTypes.addAll(validArgTypes)
+ val validTypes = if (index == null) {
+ argTypes.groupingBy { it }.eachCount().asSequence().filter { it.value == 1 }.map { it.key }.toList()
- } else {
+ } else {
- validSingleArgTypes.retainAll(validArgTypes.toSet())
- if (validSingleArgTypes.isEmpty()) {
- return listOf()
+ listOfNotNull(argTypes.getOrNull(index))
- }
+ }
- }
- } 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()
- }
- }
- }
- 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) }
+ 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(
+ sanitizedParameter(
- psiParam?.type ?: argType.toPsiType(elementFactory),
- psiParam?.name,
+ param?.type ?: argType.toPsiType(elementFactory),
+ param?.name,
- )
+ )
- },
+ }
+ } else null
+ return ExpectedSignatures.Valid(
+ ModifierSignatures(
+ SequencedMap(paramOptions),
+ fullParams,
- )
+ )
- listOf(
- singleSignature,
- MethodSignature(listOf(fullParamGroup), psiType),
- )
+ )
- } else {
- listOf(singleSignature)
- }
+ }
+
+ 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
}
Index: src/main/kotlin/platform/mixin/handlers/ModifyArgsHandler.kt
===================================================================
--- src/main/kotlin/platform/mixin/handlers/ModifyArgsHandler.kt (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
+++ src/main/kotlin/platform/mixin/handlers/ModifyArgsHandler.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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.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,23 +49,38 @@
annotation: PsiAnnotation,
targetClass: ClassNode,
targetMethod: MethodNode,
- ): List {
+ targetInsn: TargetInsn,
+ ): ExpectedSignatures {
val argsType = JavaPsiFacade.getElementFactory(annotation.project)
.createTypeByFQClassName(ARGS, annotation.resolveScope)
- return listOf(
+ val shortParams = listOf(Parameter("args", argsType))
+ return ExpectedSignatures.Valid(
+ BasicSignatures(
- MethodSignature(
+ MethodSignature(
- listOf(
- ParameterGroup(listOf(Parameter("args", argsType))),
- ParameterGroup(
- collectTargetMethodParameters(annotation.project, targetClass, targetMethod),
- required = ParameterGroup.RequiredLevel.OPTIONAL,
- isVarargs = true,
+ shortParams,
+ PsiTypes.voidType(),
+ allowCoerceRequired = false,
- ),
+ ),
- ),
+ MethodSignature(
+ shortParams + collectTargetMethodParameters(annotation.project, targetClass, targetMethod),
- PsiTypes.voidType(),
+ 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(),
+ )
+ }
+
override val mixinExtrasExpressionContextType = ExpressionContext.Type.MODIFY_ARGS
}
Index: src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt
===================================================================
--- src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
+++ src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -22,27 +22,34 @@
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 : InjectorAnnotationHandler() {
+class ModifyConstantHandler : InsnInjectorAnnotationHandler() {
+ private val constantInjectionPoint by lazy { InjectionPoint.byAtCode("CONSTANT") as ConstantInjectionPoint }
+
private val allowedOpcodes = setOf(
+ Opcodes.ACONST_NULL,
Opcodes.ICONST_M1,
Opcodes.ICONST_0,
Opcodes.ICONST_1,
@@ -64,129 +71,89 @@
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 constantInfos.asSequence()
- .distinctBy { it.constant?.javaClass }
- .flatMap {
- when (it.constant) {
- null -> sequenceOf(
- makeMethodSignature(annotation.project, targetClass, targetMethod, PsiType.getJavaLangObject(psiManager, annotation.resolveScope))
+
+ 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,
- )
+ )
- 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()),
+ } else {
+ makeSignatures(
+ getConstantType(annotation, cst)
+ ?: throw IllegalStateException("Unknown constant type: ${cst.javaClass.name}"),
+ trailingParams,
- )
+ )
- is Long -> sequenceOf(
- makeMethodSignature(annotation.project, targetClass, targetMethod, PsiTypes.longType())
+ }
+ }
+
+ 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 Float -> sequenceOf(
- makeMethodSignature(annotation.project, targetClass, targetMethod, PsiTypes.floatType())
+ is Type -> getClassType(
+ PsiManager.getInstance(context.project),
+ context,
- )
+ )
- 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}")
+
+ else -> null
- }
+ }
- }
- .toList()
- }
- 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(
+ private fun makeTypeCheckParams(psiManager: PsiManager, context: PsiElement): List {
+ return listOf(
- sanitizedParameter(PsiType.getJavaLangObject(psiManager, context.resolveScope), "instance"),
- sanitizedParameter(getClassType(psiManager, context), "type"),
- )
+ 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 getClassType(psiManager: PsiManager, context: PsiElement): PsiType {
Index: src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt
===================================================================
--- src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
+++ src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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.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 result = linkedMapOf()
- val matchedLocals = info.matchLocals(
+ val matchedLocals = info.matchLocals(
- module, targetClass, targetMethod, insn.insn,
- CollectVisitor.Mode.COMPLETION, matchType = false
- ) ?: continue
+ module, targetClass, targetMethod, targetInsn.insn,
+ CollectVisitor.Mode.SUGGESTION, matchType = false
+ ).orEmpty()
- for (local in matchedLocals) {
+ 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 type = Type.getType(local.desc ?: continue)
+ result.computeIfAbsent(type) {
+ sanitizedParameter(type.toPsiType(elementFactory), local.name, local.isNamed)
- }
- }
+ }
+ }
+
+ return ExpectedSignatures.Valid(
+ ModifierSignatures(
+ SequencedMap(result),
+ trailingParams = targetParams,
+ )
+ )
- }
+ }
- return result
+ override fun suggestedMethodSignature(
+ annotation: PsiAnnotation,
+ targets: List
+ ): SuggestedSignature? {
+ return SuggestedSignature.modifier(
+ annotation,
+ expectedMethodSignatures(annotation, targets).collectSignatures() ?: return null,
+ )
}
override val isShiftAlwaysDiscouraged = false
Index: src/main/kotlin/platform/mixin/handlers/RedirectInjectorHandler.kt
===================================================================
--- src/main/kotlin/platform/mixin/handlers/RedirectInjectorHandler.kt (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
+++ src/main/kotlin/platform/mixin/handlers/RedirectInjectorHandler.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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.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 @@
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,
+ targetInsn.insn,
+ extraParams,
+ )?.let(ExpectedSignatures<*>::Valid) ?: ExpectedSignatures.Invalid
+ }
+
+ override fun suggestedMethodSignature(
+ annotation: PsiAnnotation,
+ targets: List
+ ): SuggestedSignature? {
+ return SuggestedSignature.general(
+ annotation,
+ expectedMethodSignatures(annotation, targets).collectSignatures() ?: return null
- )
+ )
- MethodSignature(paramGroups + extraGroup, returnType)
- }
+ }
- }
- override val allowCoerce = true
-
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 @@
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)),
+ return GeneralSignatures(
+ parameters,
- fieldType,
+ fieldType,
- ),
+ trailingParams,
)
}
}
@@ -178,32 +181,23 @@
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)),
+ return GeneralSignatures(
+ parameters,
- PsiTypes.voidType(),
+ PsiTypes.voidType(),
- ),
+ trailingParams,
)
}
}
@@ -217,25 +211,15 @@
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 @@
)
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 @@
.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 @@
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 @@
val elementFactory = JavaPsiFacade.getElementFactory(annotation.project)
- return listOf(
- MethodSignature(
- listOf(
- ParameterGroup(
- listOf(
- Parameter("array", arrayType.toPsiType(elementFactory)),
- ),
- ),
- ),
+ return GeneralSignatures(
+ listOf(Parameter("array", arrayType.toPsiType(elementFactory))),
- PsiTypes.intType(),
+ PsiTypes.intType(),
- ),
+ trailingParams,
)
}
}
@@ -310,13 +288,10 @@
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 @@
val elementFactory = JavaPsiFacade.getElementFactory(annotation.project)
val psiArrayType = arrayType.toPsiType(elementFactory) as PsiArrayType
- return listOf(
- MethodSignature(
+ return GeneralSignatures(
- listOf(
+ listOf(
- ParameterGroup(
- listOf(
- Parameter("array", psiArrayType),
- Parameter("index", PsiTypes.intType()),
- ),
+ Parameter("array", psiArrayType),
+ Parameter("index", PsiTypes.intType()),
+ ),
- ),
- ),
- psiArrayType.componentType,
+ psiArrayType.componentType,
- ),
+ trailingParams,
)
}
}
@@ -346,13 +316,10 @@
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 @@
val elementFactory = JavaPsiFacade.getElementFactory(annotation.project)
val psiArrayType = arrayType.toPsiType(elementFactory) as PsiArrayType
- return listOf(
- MethodSignature(
+ return GeneralSignatures(
- listOf(
+ listOf(
- ParameterGroup(
- listOf(
- Parameter("array", psiArrayType),
- Parameter("index", PsiTypes.intType()),
- Parameter("value", psiArrayType.componentType),
- ),
+ Parameter("array", psiArrayType),
+ Parameter("index", PsiTypes.intType()),
+ Parameter("value", psiArrayType.componentType),
+ ),
- ),
- ),
- PsiTypes.voidType(),
+ PsiTypes.voidType(),
- ),
+ trailingParams,
)
}
}
@@ -387,59 +349,57 @@
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(
+ val parameters = listOf(
- Parameter("instance", objectType),
- Parameter("type", classType),
+ Parameter("instance", objectType),
+ Parameter("type", classType),
+ )
+ return GeneralSignatures(
+ parameters,
+ sequencedMapOf(
+ TypeKind.INT_LIKE to PsiTypes.booleanType(),
+ TypeKind.OBJECT to classType,
),
+ allowCoerce = false,
+ trailingParams,
)
- return listOf(
- MethodSignature(listOf(parameters), PsiTypes.booleanType()),
- MethodSignature(listOf(parameters), classType),
- )
}
}
}
Index: src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt
===================================================================
--- src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
+++ src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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 @@
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 {
Index: src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantInjectionPoint.kt
===================================================================
--- src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantInjectionPoint.kt (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
+++ src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantInjectionPoint.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -50,6 +50,7 @@
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 @@
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) {
Index: src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantStringMethodInjectionPoint.kt
===================================================================
--- src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantStringMethodInjectionPoint.kt (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
+++ src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantStringMethodInjectionPoint.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -148,7 +148,7 @@
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 @@
// 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
Index: src/main/kotlin/platform/mixin/handlers/injectionPoint/FieldInjectionPoint.kt
===================================================================
--- src/main/kotlin/platform/mixin/handlers/injectionPoint/FieldInjectionPoint.kt (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
+++ src/main/kotlin/platform/mixin/handlers/injectionPoint/FieldInjectionPoint.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -97,7 +97,7 @@
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 @@
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
}
Index: src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.kt
===================================================================
--- src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.kt (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
+++ src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -161,7 +161,7 @@
// 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,8 +458,12 @@
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(
insn: MethodInsnNode,
@@ -467,7 +471,7 @@
selector: MixinSelector,
project: Project,
): PsiMethod? {
- if (mode != CollectVisitor.Mode.COMPLETION) {
+ if (mode.assumeCorrectAt) {
if (!selector.matchMethod(insn.owner, insn.name, insn.desc)) {
return null
}
Index: src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeAssignInjectionPoint.kt
===================================================================
--- src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeAssignInjectionPoint.kt (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
+++ src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeAssignInjectionPoint.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -105,7 +105,7 @@
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) }
Index: src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeInjectionPoint.kt
===================================================================
--- src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeInjectionPoint.kt (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
+++ src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeInjectionPoint.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -64,7 +64,7 @@
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) }
Index: src/main/kotlin/platform/mixin/handlers/injectionPoint/LoadInjectionPoint.kt
===================================================================
--- src/main/kotlin/platform/mixin/handlers/injectionPoint/LoadInjectionPoint.kt (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
+++ src/main/kotlin/platform/mixin/handlers/injectionPoint/LoadInjectionPoint.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -53,16 +53,17 @@
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 @@
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)
}
Index: src/main/kotlin/platform/mixin/handlers/injectionPoint/NewInsnInjectionPoint.kt
===================================================================
--- src/main/kotlin/platform/mixin/handlers/injectionPoint/NewInsnInjectionPoint.kt (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
+++ src/main/kotlin/platform/mixin/handlers/injectionPoint/NewInsnInjectionPoint.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -27,9 +27,7 @@
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 @@
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) }
Index: src/main/kotlin/platform/mixin/handlers/mixinextras/MixinExtrasInjectorAnnotationHandler.kt
===================================================================
--- src/main/kotlin/platform/mixin/handlers/mixinextras/MixinExtrasInjectorAnnotationHandler.kt (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
+++ src/main/kotlin/platform/mixin/handlers/mixinextras/MixinExtrasInjectorAnnotationHandler.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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.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 @@
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(
+ final 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(
- 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)
+ targetInsn: TargetInsn
+ ): ExpectedSignatures {
+ return ExpectedSignatures.Valid(
+ expectedMethodSignatureImpl(annotation, targetClass, targetMethod, targetInsn)
+ ?: return ExpectedSignatures.Invalid
- )
- }
+ )
+ }
- 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
- )
- )
- }
- }
- }
- 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 val intLikePsiTypes = listOf(
- PsiTypes.intType(), PsiTypes.booleanType(), PsiTypes.charType(), PsiTypes.byteType(), PsiTypes.shortType()
-)
Index: src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyExpressionValueHandler.kt
===================================================================
--- src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyExpressionValueHandler.kt (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
+++ src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyExpressionValueHandler.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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 @@
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(
Index: src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReceiverHandler.kt
===================================================================
--- src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReceiverHandler.kt (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
+++ src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReceiverHandler.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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,15 +45,29 @@
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
}
Index: src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReturnValueHandler.kt
===================================================================
--- src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReturnValueHandler.kt (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
+++ src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReturnValueHandler.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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,15 +36,29 @@
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
}
Index: src/main/kotlin/platform/mixin/handlers/mixinextras/ShareUtil.kt
===================================================================
--- src/main/kotlin/platform/mixin/handlers/mixinextras/ShareUtil.kt (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
+++ src/main/kotlin/platform/mixin/handlers/mixinextras/ShareUtil.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -22,7 +22,6 @@
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 @@
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,
Index: src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt
===================================================================
--- src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
+++ src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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,44 +21,49 @@
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 {
+ targets: List,
+ mode: CollectVisitor.Mode,
+ ): List> {
+ return targets.map { (targetClass, targetMethod) ->
- val returnType = targetMethod.getGenericReturnType(targetClass, annotation.project)
+ val returnType = targetMethod.getGenericReturnType(targetClass, annotation.project)
- return listOf(
- MethodSignature(
- listOf(
- ParameterGroup(
- collectTargetMethodParameters(annotation.project, targetClass, targetMethod) +
- Parameter(
- "original",
- mixinExtrasOperationType(annotation, returnType) ?: return emptyList()
- ),
+ ExpectedSignatures.Valid(
+ OperationWrapperSignatures.create(
+ annotation,
+ collectTargetMethodParameters(annotation.project, targetClass, targetMethod),
+ returnType,
+ ) ?: return@map ExpectedSignatures.Invalid
- )
+ )
- ),
- returnType
+ }
+ }
+
+ override fun suggestedMethodSignature(
+ annotation: PsiAnnotation,
+ targets: List
+ ): SuggestedSignature? {
+ return SuggestedSignature.operationWrapper(
+ annotation,
+ expectedMethodSignatures(annotation, targets).collectSignatures() ?: return null
- )
+ )
- )
}
override fun isUnresolved(
@@ -84,5 +89,5 @@
)?.let(::listOf).orEmpty()
}
- override val mixinExtrasExpressionContextType = ExpressionContext.Type.CUSTOM
+ override fun canAlwaysBeStatic(method: PsiMethod) = false
}
Index: src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt
===================================================================
--- src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
+++ src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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,33 +50,46 @@
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 {
- if (
- target.getDecoration(ExpressionDecorations.SIMPLE_OPERATION_RETURN_TYPE)
- == ExpressionASMUtils.INTLIKE_TYPE
- ) {
- add(MethodSignature.TypePosition.Return)
+ 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)
- }
+ }
+ }
private fun getParameterTypes(
target: TargetInsn,
Index: src/main/kotlin/platform/mixin/handlers/mixinextras/WrapWithConditionHandler.kt
===================================================================
--- src/main/kotlin/platform/mixin/handlers/mixinextras/WrapWithConditionHandler.kt (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
+++ src/main/kotlin/platform/mixin/handlers/mixinextras/WrapWithConditionHandler.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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,15 +68,29 @@
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
}
Index: src/main/kotlin/platform/mixin/inspection/injector/CancellableBeforeSuperCallInspection.kt
===================================================================
--- src/main/kotlin/platform/mixin/inspection/injector/CancellableBeforeSuperCallInspection.kt (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
+++ src/main/kotlin/platform/mixin/inspection/injector/CancellableBeforeSuperCallInspection.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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 @@
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) {
Index: src/main/kotlin/platform/mixin/inspection/injector/DisallowedTargetInsnInspection.kt
===================================================================
--- src/main/kotlin/platform/mixin/inspection/injector/DisallowedTargetInsnInspection.kt (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
+++ src/main/kotlin/platform/mixin/inspection/injector/DisallowedTargetInsnInspection.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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 @@
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 {
Index: src/main/kotlin/platform/mixin/inspection/injector/DiscouragedShiftInspection.kt
===================================================================
--- src/main/kotlin/platform/mixin/inspection/injector/DiscouragedShiftInspection.kt (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
+++ src/main/kotlin/platform/mixin/inspection/injector/DiscouragedShiftInspection.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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 @@
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 @@
}
}
- 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
}
Index: src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt
===================================================================
--- src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
+++ src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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.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 @@
override fun visitMethod(method: PsiMethod) {
val identifier = method.nameIdentifier ?: return
val modifiers = method.modifierList
+ val parameters = method.parameterList
- var reportedStatic = false
- var reportedSignature = false
+ val (annotation, handler) = modifiers.annotations.firstNotNullOfOrNull { annotation ->
+ (MixinAnnotationHandler.forMixinAnnotation(annotation, annotation.project)
+ as? InjectorAnnotationHandler)?.let { annotation to it }
+ } ?: return
- 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 targetMethods = annotation.findContainingClass()?.mixinTargets?.flatMap { targetClass ->
+ handler.resolveTarget(annotation, targetClass).map { it.classAndMethod }
+ } ?: return
- val matchesByMethod = targetMethods.associateWith { classAndMethod ->
- handler.resolveInstructions(
- annotation,
- classAndMethod.clazz,
- classAndMethod.method
- )
+ 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()
- }
+ }
- val hasDisallowedInsns = matchesByMethod.values.asSequence()
+ }
+ .toMap()
+ .ifEmpty { return }
+
+ val hasDisallowedInsns = handler is InsnInjectorAnnotationHandler && matchesByMethod.values.asSequence()
- .flatten().any { !handler.isInsnAllowed(it.insn, it.decorations) }
- if (hasDisallowedInsns) {
+ .flatten().any { !handler.isInsnAllowed(it.insn, it.decorations) }
+ if (hasDisallowedInsns) {
- continue
+ return
- }
+ }
- for ((targetMethod, matches) in matchesByMethod) {
- if (matches.isEmpty()) {
- // We will never inject
- continue
- }
- if (!reportedStatic) {
+ val requiredStaticness = matchesByMethod.asSequence()
+ .mapNotNull { (targetMethod, matches) ->
- var shouldBeStatic = targetMethod.method.hasAccess(Opcodes.ACC_STATIC)
+ 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 (!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 (shouldBeStatic && !modifiers.hasModifierProperty(PsiModifier.STATIC)) {
- reportedStatic = true
+ when {
+ shouldBeStatic -> true
+ handler.canAlwaysBeStatic(method) -> null
+ else -> false
+ }
+ }.toSet()
+
+ when {
+ requiredStaticness.size == 2 -> holder.registerProblem(
+ identifier,
+ "Impossible combination of targets: some require a static handler and others a non-static handler",
+ )
+
+ true in requiredStaticness -> if (!method.hasModifierProperty(PsiModifier.STATIC)) {
- holder.registerProblem(
- identifier,
- "Method must be static",
- QuickFixFactory.getInstance().createModifierListFix(
- modifiers,
- PsiModifier.STATIC,
- true,
- false,
- ),
- )
+ 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
+ }
+
+ 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,
- ),
- )
- }
- }
+ holder.registerProblem(
+ modifiers.findKeyword(PsiModifier.STATIC) ?: identifier,
+ "Method must not be static",
+ QuickFixFactory.getInstance().createModifierListFix(
+ modifiers,
+ PsiModifier.STATIC,
+ false,
+ false,
+ ),
+ )
+ }
+ }
- }
- if (!reportedSignature) {
- // Check method parameters
- val parameters = method.parameterList
- val possibleSignatures = handler.expectedMethodSignature(
+ val isAlreadyValid = handler.expectedMethodSignatures(
- annotation,
+ annotation,
- targetMethod.clazz,
- targetMethod.method,
- ) ?: continue
+ targetMethods,
+ CollectVisitor.Mode.RESOLUTION,
+ ).all { it.matches(method) }
- 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",
- )
+ if (isAlreadyValid) {
+ return
- }
+ }
- continue
- }
- 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
- }
- }
- }
+ val signatureOptions =
+ handler.expectedMethodSignatures(annotation, targetMethods).collectSignatures()
+ val signatureSuggestion = signatureOptions?.let { SuggestedReturnType.forParams(parameters, it) }
+ ?: handler.suggestedMethodSignature(annotation, targetMethods)
- if (!isValid) {
- val (expectedParameters, expectedReturnType, intLikeTypePositions) = possibleSignatures[0]
- val normalizedReturnType = when (expectedReturnType) {
- is PsiEllipsisType -> expectedReturnType.toArrayType()
- else -> expectedReturnType
- }
+ val declarationStart = (method.returnTypeElement ?: identifier).startOffsetInParent
+ val declarationEnd = method.parameterList.textRangeInParent.endOffset
- 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(
+ if (signatureSuggestion == null) {
+ holder.registerProblem(
- method,
+ method,
- expectedParameters.takeUnless { paramsCheck == CheckResult.OK },
- normalizedReturnType.takeUnless { returnTypeOk },
- intLikeTypePositions
+ "There are no possible signatures for this injector",
+ ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
+ TextRange.create(declarationStart, declarationEnd),
- )
+ )
- val highlightType =
- if (isError)
- ProblemHighlightType.GENERIC_ERROR_OR_WARNING
- else
- ProblemHighlightType.WARNING
- val declarationStart = (method.returnTypeElement ?: identifier).startOffsetInParent
- val declarationEnd = method.parameterList.textRangeInParent.endOffset
+ } 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,
+ holder.registerProblem(
+ method,
+ description,
- highlightType,
+ ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
- TextRange.create(declarationStart, declarationEnd),
+ TextRange.create(declarationStart, declarationEnd),
- quickFix
+ quickFix,
- )
- }
- }
- }
+ )
+ }
+ }
+ }
- }
- }
- }
- 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)
- }
- }
-
- 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
- }
- }
- }
-
- // 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
- }
- }
-
- // 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
- }
- }
-
- return CheckResult.OK
- }
- }
-
- 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 @@
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 @@
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,22 +270,24 @@
// 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)
+ val nameHelper = PsiNameHelper.getInstance(project)
+ val languageLevel = PsiUtil.getLanguageLevel(parameters)
- it.parameters.mapIndexed { i: Int, p: Parameter ->
+
+ val usedNames = mutableSetOf()
+ val newParams = suggestedParams.mapTo(mutableListOf()) { p ->
- val paramName = p.name?.takeIf { name -> nameHelper.isIdentifier(name, languageLevel) }
+ 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)
+ ?: 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)
- }
+ }
- } else {
- emptyList()
+ newParam
- }
+ }
- }
// Restore the captured locals and sugars before applying the fix
newParams.addAll(locals)
newParams.addAll(sugars)
@@ -403,10 +301,7 @@
}
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 @@
}
}
- private fun fixIntLikeTypes(method: PsiMethod, editor: Editor, preview: Boolean) {
- if (intLikeTypePositions.isEmpty()) {
- return
+ 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() }
- }
+ }
- val runnable = {
- val template = makeIntLikeTypeTemplate(method, intLikeTypePositions)
- if (template != null) {
- editor.caretModel.moveToOffset(method.startOffset)
- TemplateManager.getInstance(method.project)
- .startTemplate(editor, template)
+
+ existingCoerce == null && needsCoerce -> {
+ val annotation = JavaPsiFacade.getElementFactory(project)
+ .createAnnotationFromText("@$COERCE", returnTypeElement);
+ { method.modifierList.add(annotation) }
}
+
+ else -> return
}
if (preview) {
- runnable()
+ fixCoerce()
} else {
- invokeLater {
- WriteCommandAction.runWriteCommandAction(
- method.project,
- "Choose Int-Like Type",
- null,
- runnable,
- method.parentOfType()!!
- )
+ runWriteAction(fixCoerce)
- }
- }
+ }
+ }
+
+ private fun fixIntLikeTypes(project: Project, method: PsiMethod, editor: Editor, preview: Boolean) {
+ if (preview || signatureSuggestion.intLikeTypes.isEmpty()) {
+ return
- }
+ }
+ 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)
+ }
+ }
+ }
+
private fun makeIntLikeTypeTemplate(
method: PsiMethod,
- positions: List
+ positions: SequencedSet
): Template? {
val builder = TemplateBuilderImpl(method)
builder.replaceElement(
@@ -462,8 +371,15 @@
}
return builder.buildInlineTemplate()
}
+
+ private companion object {
+ fun suggestedParamNames(type: PsiType): List = when {
+ type.equalsToText(CALLBACK_INFO) -> listOf("ci")
+ else -> emptyList()
- }
-}
+ }
+ }
+ }
+}
private class ChooseIntLikeTypeExpression : Expression() {
private val lookupItems: Array = intLikeTypes.map(LookupElementBuilder::create).toTypedArray()
Index: src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt
===================================================================
--- src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
+++ src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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)
+ }
+}
Index: src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt
===================================================================
--- src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
+++ src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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)
+ }
+ }
+}
Index: src/main/kotlin/platform/mixin/inspection/injector/MixinParameterNameInspection.kt
===================================================================
--- src/main/kotlin/platform/mixin/inspection/injector/MixinParameterNameInspection.kt (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
+++ src/main/kotlin/platform/mixin/inspection/injector/MixinParameterNameInspection.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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.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 @@
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 @@
}
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 @@
validNames: Array?>,
module: Module, method: PsiMethod,
parameters: Array,
- parametersWithoutSugar: Array,
+ parametersWithoutSugar: List,
annotation: PsiAnnotation,
handler: InjectorAnnotationHandler,
target: MixinTargetMember
@@ -123,43 +123,51 @@
return true
}
- val validNamesForThisTarget = arrayOfNulls>(parameters.size)
-
if (reportForMainSignature) {
- val expectedSignatures =
- handler.expectedMethodSignature(annotation, target.classAndMethod.clazz, target.classAndMethod.method)
- ?: return false
+ 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
+ var anyValidSignatures = false
- for ((expectedParams, _) in expectedSignatures) {
- if (InvalidInjectorMethodSignatureInspection.Util.checkParameters(
- method.parameterList,
- expectedParams,
- handler.allowCoerce
- ) != InvalidInjectorMethodSignatureInspection.CheckResult.OK
- ) {
+ for (expectedSignature in signatures.options) {
+ if (!expectedSignature.matches(method)) {
- continue
- }
+ continue
+ }
-
- anyValidSignatures = true
+ anyValidSignatures = true
- checkExpectedSignatureForKnownNames(
- validNamesForThisTarget,
+ checkExpectedSignatureForKnownNames(
+ validNamesForThisTarget,
- handler,
- expectedParams,
- parametersWithoutSugar
+ 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 @@
}
}
- 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)
+ for (pos in parametersWithoutSugar.indices) {
+ val expectedParam = expectedParams[pos]
+ if (!expectedParam.knownName || expectedParam.name == null) {
+ validNamesForThisTarget[pos] = null
- } else {
+ } else {
- validNamesForThisTarget[pos]!!.add(expectedParam.name)
+ validNamesForThisTarget[pos]?.add(expectedParam.name)
- }
- }
+ }
+ }
- pos++
- if (pos >= parametersWithoutSugar.size) {
- return
- }
+ }
- }
- }
- }
private fun checkSugarForKnownNames(
- validNamesForThisTarget: Array?>,
+ validNames: Array?>,
module: Module,
handler: InjectorAnnotationHandler,
annotation: PsiAnnotation,
@@ -225,6 +212,10 @@
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,14 +230,15 @@
target.classAndMethod.method,
insn.insn, CollectVisitor.Mode.RESOLUTION
)?.singleOrNull() ?: return false
- if (matchedLocal.isNamed) {
- if (validNamesForThisTarget[pos] == null) {
- validNamesForThisTarget[pos] = mutableSetOf(matchedLocal.name)
+ val suggestions = if (matchedLocal.isNamed) mutableSetOf(matchedLocal.name) else mutableSetOf()
+
+ val existingNames = validNames[pos]
+ if (existingNames == null) {
+ validNames[pos] = suggestions
- } else {
+ } else {
- validNamesForThisTarget[pos]!!.add(matchedLocal.name)
+ existingNames.retainAll(suggestions)
- }
- }
+ }
+ }
- }
return true
}
Index: src/main/kotlin/platform/mixin/inspection/injector/ModifyVariableArgsOnlyInspection.kt
===================================================================
--- src/main/kotlin/platform/mixin/inspection/injector/ModifyVariableArgsOnlyInspection.kt (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
+++ src/main/kotlin/platform/mixin/inspection/injector/ModifyVariableArgsOnlyInspection.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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 @@
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 @@
object Util {
fun shouldReport(
localInfo: LocalInfo,
- injector: InjectorAnnotationHandler,
+ injector: InsnInjectorAnnotationHandler,
injectorAnnotation: PsiAnnotation,
): Boolean {
if (localInfo.argsOnly) {
Index: src/main/kotlin/platform/mixin/inspection/injector/ModifyVariableMayUseNameInspection.kt
===================================================================
--- src/main/kotlin/platform/mixin/inspection/injector/ModifyVariableMayUseNameInspection.kt (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
+++ src/main/kotlin/platform/mixin/inspection/injector/ModifyVariableMayUseNameInspection.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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 @@
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 @@
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 @@
}
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,
Index: src/main/kotlin/platform/mixin/inspection/injector/ParameterGroup.kt
===================================================================
--- src/main/kotlin/platform/mixin/inspection/injector/ParameterGroup.kt (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
+++ src/main/kotlin/platform/mixin/inspection/injector/ParameterGroup.kt (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
@@ -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)
- }
- }
-}
Index: src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt
===================================================================
--- src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
+++ src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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)
Index: src/main/kotlin/platform/mixin/inspection/mixinextras/InjectLocalCaptureReplaceWithLocalInspection.kt
===================================================================
--- src/main/kotlin/platform/mixin/inspection/mixinextras/InjectLocalCaptureReplaceWithLocalInspection.kt (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
+++ src/main/kotlin/platform/mixin/inspection/mixinextras/InjectLocalCaptureReplaceWithLocalInspection.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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 @@
// 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(
Index: src/main/kotlin/platform/mixin/inspection/mixinextras/LocalArgsOnlyInspection.kt
===================================================================
--- src/main/kotlin/platform/mixin/inspection/mixinextras/LocalArgsOnlyInspection.kt (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
+++ src/main/kotlin/platform/mixin/inspection/mixinextras/LocalArgsOnlyInspection.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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 @@
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()
Index: src/main/kotlin/platform/mixin/inspection/mixinextras/LocalMayUseNameInspection.kt
===================================================================
--- src/main/kotlin/platform/mixin/inspection/mixinextras/LocalMayUseNameInspection.kt (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
+++ src/main/kotlin/platform/mixin/inspection/mixinextras/LocalMayUseNameInspection.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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 @@
}
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
Index: src/main/kotlin/platform/mixin/inspection/mixinextras/UnnecessaryMutableLocalInspection.kt
===================================================================
--- src/main/kotlin/platform/mixin/inspection/mixinextras/UnnecessaryMutableLocalInspection.kt (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
+++ src/main/kotlin/platform/mixin/inspection/mixinextras/UnnecessaryMutableLocalInspection.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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 @@
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
Index: src/main/kotlin/platform/mixin/inspection/mixinextras/UnresolvedLocalCaptureInspection.kt
===================================================================
--- src/main/kotlin/platform/mixin/inspection/mixinextras/UnresolvedLocalCaptureInspection.kt (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
+++ src/main/kotlin/platform/mixin/inspection/mixinextras/UnresolvedLocalCaptureInspection.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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 @@
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
Index: src/main/kotlin/platform/mixin/util/AsmUtil.kt
===================================================================
--- src/main/kotlin/platform/mixin/util/AsmUtil.kt (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
+++ src/main/kotlin/platform/mixin/util/AsmUtil.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -75,7 +75,6 @@
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,10 +829,23 @@
}
}
-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())
+}
+
/**
* Attempts to find the source method using [findSourceElement]. If this fails, or if the result is not a `PsiMethod`,
* then a new source method is constructed, possibly copying the body of the found source element.
Index: src/main/kotlin/platform/mixin/util/IntLikeAnchorSolver.kt
===================================================================
--- src/main/kotlin/platform/mixin/util/IntLikeAnchorSolver.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
+++ src/main/kotlin/platform/mixin/util/IntLikeAnchorSolver.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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]
+ }
+}
Index: src/main/kotlin/platform/mixin/util/LocalInfo.kt
===================================================================
--- src/main/kotlin/platform/mixin/util/LocalInfo.kt (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
+++ src/main/kotlin/platform/mixin/util/LocalInfo.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -88,15 +88,13 @@
): 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 @@
}
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 @@
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 @@
}
// 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()
}
Index: src/main/kotlin/platform/mixin/util/Mixin.kt
===================================================================
--- src/main/kotlin/platform/mixin/util/Mixin.kt (revision 739fc17f869706c3e5da886f5258c0e52f9f2d0d)
+++ src/main/kotlin/platform/mixin/util/Mixin.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -33,6 +33,7 @@
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 @@
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())
+}
Index: src/main/kotlin/platform/mixin/util/ReturnTypeSolver.kt
===================================================================
--- src/main/kotlin/platform/mixin/util/ReturnTypeSolver.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
+++ src/main/kotlin/platform/mixin/util/ReturnTypeSolver.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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())
+ }
+ }
+}
Index: src/main/kotlin/platform/mixin/util/TypeKind.kt
===================================================================
--- src/main/kotlin/platform/mixin/util/TypeKind.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
+++ src/main/kotlin/platform/mixin/util/TypeKind.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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
+ }
+}
Index: src/main/kotlin/util/PrioritySet.kt
===================================================================
--- src/main/kotlin/util/PrioritySet.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
+++ src/main/kotlin/util/PrioritySet.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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()
+}
Index: src/main/kotlin/util/sequenced-collections.kt
===================================================================
--- src/main/kotlin/util/sequenced-collections.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
+++ src/main/kotlin/util/sequenced-collections.kt (revision 8539df1c74008cade4a10470748e1794e7dd6277)
@@ -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 .
+ */
+
+package com.demonwav.mcdev.util
+
+@Suppress("JavaDefaultMethodsNotOverriddenByDelegation")
+@JvmInline
+value class SequencedSet(private val set: MutableSequencedSet) : Set by set
+
+typealias MutableSequencedSet = java.util.SequencedSet
+
+private val EMPTY_SEQUENCED_SET: SequencedSet