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