From 6a6c1ebe68e20db10bdc35c697f185cc3792abd8 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Wed, 16 Sep 2026 17:31:03 +0100 Subject: [PATCH 01/51] Refactor: Rework expected MethodSignatures. - ParameterGroups are gone, with every signature now effectively containing a required leading group and an optional vararg trailing group. The prior flexibility made working them easy to get wrong, for example in the matching logic which would have been incorrect for leading vararg groups. - Inject previously contained 2 optional groups (the target's params and the captured locals) but this was in fact incorrect, since the captured params are required if we want to capture any locals, so it is now (better) represented as 2 separate signature options. - The distinction between `WARN_IF_ABSENT` and `ERROR_IF_ABSENT` is entirely removed. It was effectively unused since it only applied to captured Inject locals, which are varargs and therefore never "absent" (a separate inspection handles unused LocalCapture). - ModifyArgs is in fact all-or-nothing wrt capturing the target parameters, and this is now reflected. --- .../mixin/handlers/InjectAnnotationHandler.kt | 75 ++++++------- .../mixin/handlers/ModifyArgHandler.kt | 16 +-- .../mixin/handlers/ModifyArgsHandler.kt | 17 ++- .../mixin/handlers/ModifyConstantHandler.kt | 35 ++---- .../mixin/handlers/ModifyVariableHandler.kt | 13 +-- .../mixin/handlers/RedirectInjectorHandler.kt | 105 ++++++------------ .../MixinExtrasInjectorAnnotationHandler.kt | 23 ++-- .../ModifyExpressionValueHandler.kt | 7 +- .../mixinextras/ModifyReceiverHandler.kt | 8 +- .../mixinextras/ModifyReturnValueHandler.kt | 7 +- .../handlers/mixinextras/WrapMethodHandler.kt | 17 +-- .../mixinextras/WrapOperationHandler.kt | 7 +- .../mixinextras/WrapWithConditionHandler.kt | 8 +- ...nvalidInjectorMethodSignatureInspection.kt | 102 +++++------------ .../inspection/injector/MethodSignature.kt | 58 +++++++++- .../injector/MixinParameterNameInspection.kt | 44 +++----- .../inspection/injector/ParameterGroup.kt | 96 ---------------- 17 files changed, 221 insertions(+), 417 deletions(-) delete mode 100644 src/main/kotlin/platform/mixin/inspection/injector/ParameterGroup.kt diff --git a/src/main/kotlin/platform/mixin/handlers/InjectAnnotationHandler.kt b/src/main/kotlin/platform/mixin/handlers/InjectAnnotationHandler.kt index 0c56329d3..b640f9480 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 @@ -21,7 +21,6 @@ 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.LocalVariables import com.demonwav.mcdev.platform.mixin.util.callbackInfoReturnableType import com.demonwav.mcdev.platform.mixin.util.callbackInfoType @@ -52,34 +51,23 @@ class InjectAnnotationHandler : InjectorAnnotationHandler() { ): List { val returnType = targetMethod.getGenericReturnType(targetClass, annotation.project) - val result = ArrayList() + val result = mutableListOf() - // Parameters from injected method (optional) - result.add( - ParameterGroup( - collectTargetMethodParameters(annotation.project, targetClass, targetMethod), - required = ParameterGroup.RequiredLevel.OPTIONAL, - default = true, - ), - ) + val ciParam = if (returnType == PsiTypes.voidType()) { + Parameter("ci", callbackInfoType(annotation.project)) + } else { + Parameter( + "cir", + callbackInfoReturnableType(annotation.project, annotation, returnType)!!, + ) + } - // 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") { @@ -107,29 +95,34 @@ class InjectAnnotationHandler : InjectorAnnotationHandler() { if (commonLocalsPrefix != null) { val elementFactory = JavaPsiFacade.getElementFactory(annotation.project) - val localParams = commonLocalsPrefix.map { local -> + capturedLocals = 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, - ), - ) } } } - return listOf(MethodSignature(result, PsiTypes.voidType())) + // Long form + result.add( + MethodSignature( + targetParams + ciParam, + PsiTypes.voidType(), + capturedLocals, + trailingByDefault = true, + ) + ) + + // Short form + result.add( + MethodSignature( + listOf(ciParam), + PsiTypes.voidType(), + ) + ) + + return result } override fun canAlwaysBeStatic(method: PsiMethod): Boolean { diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt index 6fb196501..e340e3406 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 @@ -21,7 +21,6 @@ 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.util.toPsiType @@ -111,27 +110,22 @@ class ModifyArgHandler : InjectorAnnotationHandler() { val psiType = psiParameter?.type ?: Type.getType(type).toPsiType(elementFactory, null) val singleSignature = MethodSignature( listOf( - ParameterGroup( - listOf( - sanitizedParameter(psiType, psiParameter?.name), - ), - ), + sanitizedParameter(psiType, psiParameter?.name), ), psiType, ) if (validFullSignature != null) { - val fullParamGroup = ParameterGroup( + val fullParams = 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), + MethodSignature(fullParams, psiType), ) } else { listOf(singleSignature) diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyArgsHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyArgsHandler.kt index 3ec63ae73..97f057503 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 @@ -21,7 +21,6 @@ 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.MixinConstants.Classes.ARGS import com.demonwav.mcdev.util.Parameter import com.intellij.psi.JavaPsiFacade @@ -47,16 +46,14 @@ class ModifyArgsHandler : InjectorAnnotationHandler() { ): List { val argsType = JavaPsiFacade.getElementFactory(annotation.project) .createTypeByFQClassName(ARGS, annotation.resolveScope) + val shortParams = listOf(Parameter("args", argsType)) return listOf( MethodSignature( - listOf( - ParameterGroup(listOf(Parameter("args", argsType))), - ParameterGroup( - collectTargetMethodParameters(annotation.project, targetClass, targetMethod), - required = ParameterGroup.RequiredLevel.OPTIONAL, - isVarargs = true, - ), - ), + shortParams, + PsiTypes.voidType(), + ), + MethodSignature( + shortParams + collectTargetMethodParameters(annotation.project, targetClass, targetMethod), PsiTypes.voidType(), ), ) diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt index bc15541f0..6f19fe38b 100644 --- a/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt @@ -23,7 +23,6 @@ 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.intellij.psi.JavaPsiFacade @@ -94,15 +93,9 @@ class ModifyConstantHandler : InjectorAnnotationHandler() { 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, - ), - ), + listOf(sanitizedParameter(returnType, constantParamName)), returnType, + collectTargetMethodParameters(annotation.project, targetClass, targetMethod), ) ) } @@ -151,15 +144,9 @@ class ModifyConstantHandler : InjectorAnnotationHandler() { type: PsiType, ): MethodSignature { return MethodSignature( - listOf( - ParameterGroup(listOf(sanitizedParameter(type, "constant"))), - ParameterGroup( - collectTargetMethodParameters(project, targetClass, targetMethod), - isVarargs = true, - required = ParameterGroup.RequiredLevel.OPTIONAL, - ), - ), + listOf(sanitizedParameter(type, "constant")), type, + collectTargetMethodParameters(project, targetClass, targetMethod), ) } @@ -173,19 +160,11 @@ class ModifyConstantHandler : InjectorAnnotationHandler() { ): 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, - ), + sanitizedParameter(PsiType.getJavaLangObject(psiManager, context.resolveScope), "instance"), + sanitizedParameter(getClassType(psiManager, context), "type"), ), returnType, + collectTargetMethodParameters(project, targetClass, targetMethod), ) } diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt index 11284ecfc..aff8da459 100644 --- a/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt @@ -24,7 +24,6 @@ import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.AbstractLoadInj 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.util.LocalInfo import com.demonwav.mcdev.platform.mixin.util.toPsiType import com.demonwav.mcdev.util.constantStringValue @@ -51,11 +50,7 @@ class ModifyVariableHandler : InjectorAnnotationHandler() { 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 targetParams = collectTargetMethodParameters(annotation.project, targetClass, targetMethod) val method = annotation.findContainingMethod() ?: return null val localType = method.parameterList.getParameter(0)?.type @@ -73,11 +68,9 @@ class ModifyVariableHandler : InjectorAnnotationHandler() { 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, - ), + listOf(sanitizedParameter(localType, local.name, local.isNamed)), localType, + targetParams, ) } } diff --git a/src/main/kotlin/platform/mixin/handlers/RedirectInjectorHandler.kt b/src/main/kotlin/platform/mixin/handlers/RedirectInjectorHandler.kt index 5536a39dc..a8d74774b 100644 --- a/src/main/kotlin/platform/mixin/handlers/RedirectInjectorHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/RedirectInjectorHandler.kt @@ -22,7 +22,6 @@ 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.util.AsmDfaUtil import com.demonwav.mcdev.platform.mixin.util.FieldTargetMember import com.demonwav.mcdev.platform.mixin.util.MethodTargetMember @@ -90,19 +89,14 @@ class RedirectInjectorHandler : InjectorAnnotationHandler() { targetMethod: MethodNode, ): List? { val insns = resolveInstructions(annotation, targetClass, targetMethod).ifEmpty { return emptyList() } + val extraParams = collectTargetMethodParameters(annotation.project, targetClass, targetMethod) return getRedirectType(insns[0].insn)?.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) + )?.map { (params, returnType) -> + MethodSignature(params, returnType, extraParams) } } @@ -118,7 +112,7 @@ class RedirectInjectorHandler : InjectorAnnotationHandler() { targetClass: ClassNode, targetMethod: MethodNode, insns: List, - ): List? + ): List, PsiType>>? } private abstract class FieldAccess : RedirectType { @@ -145,7 +139,7 @@ class RedirectInjectorHandler : InjectorAnnotationHandler() { targetClass: ClassNode, targetMethod: MethodNode, insns: List, - ): List { + ): List, PsiType>> { val firstMatch = insns.first() as FieldInsnNode val isValid = insns.all { val insn = it as? FieldInsnNode ?: return@all false @@ -164,12 +158,7 @@ class RedirectInjectorHandler : InjectorAnnotationHandler() { parameters += Parameter("instance", Type.getObjectType(firstMatch.owner).toPsiType(elementFactory)) } - return listOf( - MethodSignature( - listOf(ParameterGroup(parameters)), - fieldType, - ), - ) + return listOf(parameters to fieldType) } } @@ -179,7 +168,7 @@ class RedirectInjectorHandler : InjectorAnnotationHandler() { targetClass: ClassNode, targetMethod: MethodNode, insns: List, - ): List { + ): List, PsiType>> { val firstMatch = insns.first() as FieldInsnNode val isValid = insns.all { val insn = it as? FieldInsnNode ?: return@all false @@ -199,12 +188,7 @@ class RedirectInjectorHandler : InjectorAnnotationHandler() { } parameters += Parameter("value", fieldType) - return listOf( - MethodSignature( - listOf(ParameterGroup(parameters)), - PsiTypes.voidType(), - ), - ) + return listOf(parameters to PsiTypes.voidType()) } } @@ -218,7 +202,7 @@ class RedirectInjectorHandler : InjectorAnnotationHandler() { targetClass: ClassNode, targetMethod: MethodNode, insns: List, - ): List { + ): List, PsiType>> { val firstMatch = insns.first() as MethodInsnNode val isValid = insns.all { val insn = it as? MethodInsnNode ?: return@all false @@ -267,7 +251,7 @@ class RedirectInjectorHandler : InjectorAnnotationHandler() { } val returnType = signature?.first ?: Type.getReturnType(firstMatch.desc).toPsiType(elementFactory) - return listOf(MethodSignature(listOf(ParameterGroup(parameters)), returnType)) + return listOf(parameters to returnType) } } @@ -277,7 +261,7 @@ class RedirectInjectorHandler : InjectorAnnotationHandler() { targetClass: ClassNode, targetMethod: MethodNode, insns: List, - ): List? { + ): List, PsiType>>? { val firstMatch = insns.first() val isValid = insns.all { it.opcode == firstMatch.opcode } if (!isValid) return emptyList() @@ -291,16 +275,7 @@ class RedirectInjectorHandler : InjectorAnnotationHandler() { val elementFactory = JavaPsiFacade.getElementFactory(annotation.project) return listOf( - MethodSignature( - listOf( - ParameterGroup( - listOf( - Parameter("array", arrayType.toPsiType(elementFactory)), - ), - ), - ), - PsiTypes.intType(), - ), + listOf(Parameter("array", arrayType.toPsiType(elementFactory))) to PsiTypes.intType(), ) } } @@ -311,7 +286,7 @@ class RedirectInjectorHandler : InjectorAnnotationHandler() { targetClass: ClassNode, targetMethod: MethodNode, insns: List, - ): List? { + ): List, PsiType>>? { val firstMatch = insns.first() val isValid = insns.all { it.opcode == firstMatch.opcode } if (!isValid) return emptyList() @@ -326,17 +301,10 @@ class RedirectInjectorHandler : InjectorAnnotationHandler() { val psiArrayType = arrayType.toPsiType(elementFactory) as PsiArrayType return listOf( - MethodSignature( - listOf( - ParameterGroup( - listOf( - Parameter("array", psiArrayType), - Parameter("index", PsiTypes.intType()), - ), - ), - ), - psiArrayType.componentType, - ), + listOf( + Parameter("array", psiArrayType), + Parameter("index", PsiTypes.intType()), + ) to psiArrayType.componentType, ) } } @@ -347,7 +315,7 @@ class RedirectInjectorHandler : InjectorAnnotationHandler() { targetClass: ClassNode, targetMethod: MethodNode, insns: List, - ): List? { + ): List, PsiType>>? { val firstMatch = insns.first() val isValid = insns.all { it.opcode == firstMatch.opcode } if (!isValid) return emptyList() @@ -362,18 +330,11 @@ class RedirectInjectorHandler : InjectorAnnotationHandler() { 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(), - ), + listOf( + Parameter("array", psiArrayType), + Parameter("index", PsiTypes.intType()), + Parameter("value", psiArrayType.componentType), + ) to PsiTypes.voidType(), ) } } @@ -388,7 +349,7 @@ class RedirectInjectorHandler : InjectorAnnotationHandler() { targetClass: ClassNode, targetMethod: MethodNode, insns: List, - ): List { + ): List, PsiType>> { val firstMatch = insns.first() as TypeInsnNode val isValid = insns.all { val insn = it as? TypeInsnNode ?: return@all false @@ -408,9 +369,9 @@ class RedirectInjectorHandler : InjectorAnnotationHandler() { insns.mapNotNull { NewInsnInjectionPoint.Util.findInitCall(it as TypeInsnNode) }, - ).map { (paramGroups, _) -> + ).map { (params, _) -> // drop the instance parameter, return the constructed type - MethodSignature(listOf(ParameterGroup(paramGroups[0].parameters.drop(1))), constructedType) + params.drop(1) to constructedType } } } @@ -421,7 +382,7 @@ class RedirectInjectorHandler : InjectorAnnotationHandler() { targetClass: ClassNode, targetMethod: MethodNode, insns: List, - ): List { + ): List, PsiType>> { val firstMatch = insns.first() val isValid = insns.all { it.opcode == firstMatch.opcode } if (!isValid) return emptyList() @@ -430,15 +391,13 @@ class RedirectInjectorHandler : InjectorAnnotationHandler() { 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), + parameters to PsiTypes.booleanType(), + parameters to classType, ) } } diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/MixinExtrasInjectorAnnotationHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/MixinExtrasInjectorAnnotationHandler.kt index 1c3592e32..b4fc5dfe6 100644 --- a/src/main/kotlin/platform/mixin/handlers/mixinextras/MixinExtrasInjectorAnnotationHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/MixinExtrasInjectorAnnotationHandler.kt @@ -23,7 +23,6 @@ package com.demonwav.mcdev.platform.mixin.handlers.mixinextras import com.demonwav.mcdev.platform.mixin.handlers.InjectorAnnotationHandler 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.util.FieldTargetMember import com.demonwav.mcdev.platform.mixin.util.MethodTargetMember import com.demonwav.mcdev.platform.mixin.util.getGenericParameterTypes @@ -104,7 +103,7 @@ abstract class MixinExtrasInjectorAnnotationHandler : InjectorAnnotationHandler( targetClass: ClassNode, targetMethod: MethodNode, target: TargetInsn, - ): Pair? + ): Pair, PsiType>? open fun intLikeTypePositions( target: TargetInsn @@ -140,7 +139,7 @@ abstract class MixinExtrasInjectorAnnotationHandler : InjectorAnnotationHandler( annotation: PsiAnnotation, targetClass: ClassNode, targetMethod: MethodNode, - params: ParameterGroup, + params: List, returnType: PsiType, intLikeTypePositions: List ): List { @@ -151,7 +150,7 @@ abstract class MixinExtrasInjectorAnnotationHandler : InjectorAnnotationHandler( } return buildList { for (actualType in intLikePsiTypes) { - val newParams = params.parameters.toMutableList() + val newParams = params.toMutableList() var newReturnType = returnType for (pos in intLikeTypePositions) { when (pos) { @@ -165,7 +164,7 @@ abstract class MixinExtrasInjectorAnnotationHandler : InjectorAnnotationHandler( annotation, targetClass, targetMethod, - ParameterGroup(newParams), + newParams, newReturnType, intLikeTypePositions ) @@ -178,20 +177,14 @@ abstract class MixinExtrasInjectorAnnotationHandler : InjectorAnnotationHandler( annotation: PsiAnnotation, targetClass: ClassNode, targetMethod: MethodNode, - params: ParameterGroup, + params: List, returnType: PsiType, intLikeTypePositions: List ) = MethodSignature( - listOf( - params, - ParameterGroup( - collectTargetMethodParameters(annotation.project, targetClass, targetMethod), - required = ParameterGroup.RequiredLevel.OPTIONAL, - isVarargs = true, - ), - ), + params, returnType, - intLikeTypePositions + collectTargetMethodParameters(annotation.project, targetClass, targetMethod), + intLikeTypes = intLikeTypePositions ) protected fun getInsnReturnType(insn: AbstractInsnNode): Type? { diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyExpressionValueHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyExpressionValueHandler.kt index e37cfcb7c..5b2d6191e 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 @@ -21,7 +21,6 @@ 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.toPsiType import com.demonwav.mcdev.util.Parameter import com.intellij.psi.JavaPsiFacade @@ -61,9 +60,9 @@ class ModifyExpressionValueHandler : MixinExtrasInjectorAnnotationHandler() { targetClass: ClassNode, targetMethod: MethodNode, target: TargetInsn - ): Pair? { + ): Pair, PsiType>? { val psiType = getReturnType(target, annotation) ?: return null - return ParameterGroup(listOf(Parameter("original", psiType))) to psiType + return listOf(Parameter("original", psiType)) to psiType } override fun intLikeTypePositions(target: TargetInsn): List { diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReceiverHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReceiverHandler.kt index ba5eae551..31ca20f6a 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,7 +20,7 @@ package com.demonwav.mcdev.platform.mixin.handlers.mixinextras -import com.demonwav.mcdev.platform.mixin.inspection.injector.ParameterGroup +import com.demonwav.mcdev.util.Parameter import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiType import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext @@ -48,9 +48,9 @@ class ModifyReceiverHandler : MixinExtrasInjectorAnnotationHandler() { targetClass: ClassNode, targetMethod: MethodNode, target: TargetInsn - ): Pair? { + ): Pair, PsiType>? { val params = getPsiParameters(target.insn, targetClass, annotation) ?: return null - return ParameterGroup(params) to params[0].type + return params to params[0].type } 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..2c2dd5902 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,7 +20,6 @@ package com.demonwav.mcdev.platform.mixin.handlers.mixinextras -import com.demonwav.mcdev.platform.mixin.inspection.injector.ParameterGroup import com.demonwav.mcdev.platform.mixin.util.getGenericReturnType import com.demonwav.mcdev.util.Parameter import com.intellij.psi.PsiAnnotation @@ -39,9 +38,9 @@ class ModifyReturnValueHandler : MixinExtrasInjectorAnnotationHandler() { targetClass: ClassNode, targetMethod: MethodNode, target: TargetInsn - ): Pair { + ): Pair, PsiType> { val returnType = targetMethod.getGenericReturnType(targetClass, annotation.project) - return ParameterGroup(listOf(Parameter("original", returnType))) to returnType + return listOf(Parameter("original", returnType)) to returnType } override val mixinExtrasExpressionContextType = ExpressionContext.Type.MODIFY_RETURN_VALUE diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt index 0cae1c436..4d9155cee 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 @@ -23,7 +23,6 @@ package com.demonwav.mcdev.platform.mixin.handlers.mixinextras import com.demonwav.mcdev.platform.mixin.handlers.InjectorAnnotationHandler 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.util.findSourceElement import com.demonwav.mcdev.platform.mixin.util.getGenericReturnType import com.demonwav.mcdev.platform.mixin.util.mixinExtrasOperationType @@ -47,15 +46,11 @@ class WrapMethodHandler : InjectorAnnotationHandler() { return listOf( MethodSignature( - listOf( - ParameterGroup( - collectTargetMethodParameters(annotation.project, targetClass, targetMethod) + - Parameter( - "original", - mixinExtrasOperationType(annotation, returnType) ?: return emptyList() - ), - ) - ), + collectTargetMethodParameters(annotation.project, targetClass, targetMethod) + + Parameter( + "original", + mixinExtrasOperationType(annotation, returnType) ?: return emptyList() + ), returnType ) ) diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt index bedc1822f..719531ff5 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,7 +21,6 @@ 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.util.toPsiType import com.demonwav.mcdev.util.Parameter @@ -52,11 +51,11 @@ class WrapOperationHandler : MixinExtrasInjectorAnnotationHandler() { targetClass: ClassNode, targetMethod: MethodNode, target: TargetInsn - ): Pair? { + ): Pair, PsiType>? { val params = getParameterTypes(target, targetClass, annotation) ?: return null val returnType = getReturnType(target, annotation) ?: return null val operationType = mixinExtrasOperationType(annotation, returnType) ?: return null - return ParameterGroup( + return ( params + Parameter("original", operationType) ) to returnType } diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapWithConditionHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapWithConditionHandler.kt index 4875991c2..2d2ddcd34 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,8 +20,8 @@ package com.demonwav.mcdev.platform.mixin.handlers.mixinextras -import com.demonwav.mcdev.platform.mixin.inspection.injector.ParameterGroup import com.demonwav.mcdev.platform.mixin.util.nextRealInsn +import com.demonwav.mcdev.util.Parameter import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiType import com.intellij.psi.PsiTypes @@ -71,9 +71,9 @@ class WrapWithConditionHandler : MixinExtrasInjectorAnnotationHandler() { targetClass: ClassNode, targetMethod: MethodNode, target: TargetInsn - ): Pair? { + ): Pair, PsiType>? { val params = getPsiParameters(target.insn, targetClass, annotation) ?: return null - return ParameterGroup(params) to PsiTypes.booleanType() + return params to PsiTypes.booleanType() } override val mixinExtrasExpressionContextType = ExpressionContext.Type.WRAP_WITH_CONDITION diff --git a/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt b/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt index f0acaed4e..fa3ccbde9 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt @@ -184,13 +184,13 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() { } var isValid = false - for ((expectedParameters, expectedReturnType) in possibleSignatures) { + for (expectedSignature in possibleSignatures) { val paramsMatch = - Util.checkParameters(parameters, expectedParameters, handler.allowCoerce) == CheckResult.OK + Util.checkParameters(parameters, expectedSignature, handler.allowCoerce) if (paramsMatch) { val methodReturnType = method.returnType if (methodReturnType != null && - checkReturnType(expectedReturnType, methodReturnType, method, handler.allowCoerce) + checkReturnType(expectedSignature.returnType, methodReturnType, method, handler.allowCoerce) ) { isValid = true break @@ -199,18 +199,19 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() { } if (!isValid) { - val (expectedParameters, expectedReturnType, intLikeTypePositions) = possibleSignatures[0] + val expectedSignature = possibleSignatures[0] + val (expectedParams, expectedReturnType, expectedTrailingParams, trailingByDefault, intLikeTypePositions) = expectedSignature val normalizedReturnType = when (expectedReturnType) { is PsiEllipsisType -> expectedReturnType.toArrayType() else -> expectedReturnType } - val paramsCheck = Util.checkParameters(parameters, expectedParameters, handler.allowCoerce) - val isWarning = paramsCheck == CheckResult.WARNING + val paramsMatch = Util.checkParameters(parameters, expectedSignature, handler.allowCoerce) + val isWarning = false val methodReturnType = method.returnType val returnTypeOk = methodReturnType != null && checkReturnType(normalizedReturnType, methodReturnType, method, handler.allowCoerce) - val isError = paramsCheck == CheckResult.ERROR || !returnTypeOk + val isError = true if (isWarning || isError) { reportedSignature = true @@ -218,21 +219,20 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() { "Method signature does not match expected signature for $annotationName" val quickFix = SignatureQuickFix( method, - expectedParameters.takeUnless { paramsCheck == CheckResult.OK }, + when { + paramsMatch -> null + trailingByDefault -> expectedParams + expectedTrailingParams + else -> expectedParams + }, 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, + ProblemHighlightType.GENERIC_ERROR_OR_WARNING, TextRange.create(declarationStart, declarationEnd), quickFix ) @@ -267,59 +267,20 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() { object Util { fun checkParameters( parameterList: PsiParameterList, - expected: List, + expected: MethodSignature, allowCoerce: Boolean, - ): CheckResult { + ): Boolean { 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 - } - } - } + val parametersWithoutSugar = parameters.dropLastWhile { it.isMixinExtrasSugar } - // Sugars are valid on any injector and should be ignored, as long as they're at the end. - while (pos < parameters.size) { - if (parameters[pos].isMixinExtrasSugar) { - pos++ - } else { - break - } - } - - // check we have consumed all the parameters - if (pos < parameters.size) { - return if ( - expected.lastOrNull()?.isVarargs == true && - expected.last().required == ParameterGroup.RequiredLevel.WARN_IF_ABSENT - ) { - CheckResult.WARNING - } else { - CheckResult.ERROR - } - } - - return CheckResult.OK + return expected.matchParams(parametersWithoutSugar, allowCoerce) } } - enum class CheckResult { - OK, WARNING, ERROR - } - private class SignatureQuickFix( method: PsiMethod, @SafeFieldForPreview - private val expectedParams: List?, + private val expectedParams: List?, @SafeFieldForPreview private val expectedReturnType: PsiType?, private val intLikeTypePositions: List @@ -374,21 +335,16 @@ 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 newParams = expectedParams.mapIndexedTo(mutableListOf()) { i, p -> + 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) } // Restore the captured locals and sugars before applying the fix newParams.addAll(locals) diff --git a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt index 804cb3fa0..a5f4c638b 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,15 +20,30 @@ 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.PsiMethod +import com.intellij.psi.PsiParameter +import com.intellij.psi.PsiPrimitiveType 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 trailingParams: List = emptyList(), + val trailingByDefault: Boolean = false, val intLikeTypes: List = emptyList() ) { + fun matchParams(parameters: List, allowCoerce: Boolean): Boolean { + return parameters.size in requiredParams.size..requiredParams.size + trailingParams.size + && matchParams(requiredParams, parameters, allowCoerce) + && matchParams(trailingParams, parameters.subList(requiredParams.size, parameters.size), allowCoerce) + } + sealed interface TypePosition { fun getElement(method: PsiMethod): PsiTypeElement? @@ -40,4 +55,43 @@ data class MethodSignature( override fun getElement(method: PsiMethod) = method.parameterList.parameters[index].typeElement } } + + companion object { + private val INT_TYPES = setOf( + PsiTypes.intType(), + PsiTypes.shortType(), + PsiTypes.charType(), + PsiTypes.byteType(), + PsiTypes.booleanType() + ) + + private fun matchParams( + expectedParams: List, + actualParams: List, + allowCoerce: Boolean + ): Boolean { + return expectedParams.asSequence() + .zip(actualParams.asSequence()) + .all { (expected, actual) -> matchParam(expected.type, actual, allowCoerce) } + } + + private fun matchParam(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/MixinParameterNameInspection.kt b/src/main/kotlin/platform/mixin/inspection/injector/MixinParameterNameInspection.kt index e74dc5b32..3913e2216 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/MixinParameterNameInspection.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/MixinParameterNameInspection.kt @@ -30,6 +30,7 @@ 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.intellij.codeInsight.intention.LowPriorityAction import com.intellij.codeInspection.LocalQuickFix @@ -63,7 +64,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) @@ -110,7 +111,7 @@ class MixinParameterNameInspection : MixinInspection() { validNames: Array?>, module: Module, method: PsiMethod, parameters: Array, - parametersWithoutSugar: Array, + parametersWithoutSugar: List, annotation: PsiAnnotation, handler: InjectorAnnotationHandler, target: MixinTargetMember @@ -131,12 +132,12 @@ class MixinParameterNameInspection : MixinInspection() { ?: return false var anyValidSignatures = false - for ((expectedParams, _) in expectedSignatures) { - if (InvalidInjectorMethodSignatureInspection.Util.checkParameters( + for (expectedSignature in expectedSignatures) { + if (!InvalidInjectorMethodSignatureInspection.Util.checkParameters( method.parameterList, - expectedParams, + expectedSignature, handler.allowCoerce - ) != InvalidInjectorMethodSignatureInspection.CheckResult.OK + ) ) { continue } @@ -145,8 +146,7 @@ class MixinParameterNameInspection : MixinInspection() { checkExpectedSignatureForKnownNames( validNamesForThisTarget, - handler, - expectedParams, + expectedSignature.requiredParams + expectedSignature.trailingParams, parametersWithoutSugar ) } @@ -187,30 +187,20 @@ class MixinParameterNameInspection : MixinInspection() { 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) { + if (validNamesForThisTarget[pos] == null) { + validNamesForThisTarget[pos] = mutableSetOf(expectedParam.name) + } else { + validNamesForThisTarget[pos]!!.add(expectedParam.name) } } } 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) - } - } -} From ceab7de38f6dc7f3b73c9c75ba403c27695f9202 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Fri, 18 Sep 2026 03:11:24 +0100 Subject: [PATCH 02/51] Fix: Disallow non-int int-like types in @ModifyConstant. Not sure why these are there, Mixin doesn't allow them. --- .../kotlin/platform/mixin/handlers/ModifyConstantHandler.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt index 6f19fe38b..cd4007a28 100644 --- a/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt @@ -110,10 +110,6 @@ class ModifyConstantHandler : InjectorAnnotationHandler() { ) 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()) From 97b6c2405aebdc16d8f297c94af1f1e5c5b79a51 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Fri, 18 Sep 2026 00:56:20 +0100 Subject: [PATCH 03/51] Fix: Disallow `nullValue` in `@ModifyConstant`. It does not work and cannot ever work. --- .../mixin/handlers/ModifyConstantHandler.kt | 5 +---- .../injectionPoint/ConstantInjectionPoint.kt | 14 ++++---------- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt index cd4007a28..e260338d4 100644 --- a/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt @@ -102,12 +102,9 @@ class ModifyConstantHandler : InjectorAnnotationHandler() { val psiManager = PsiManager.getInstance(annotation.project) return constantInfos.asSequence() - .distinctBy { it.constant?.javaClass } + .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()), ) diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantInjectionPoint.kt index 516055526..a4bb9cfb4 100644 --- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantInjectionPoint.kt +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantInjectionPoint.kt @@ -131,7 +131,6 @@ class ConstantInjectionPoint : InjectionPoint() { fun getConstantInfo(at: PsiAnnotation): ConstantInfo? { val args = AtResolver.getArgs(at) - val nullValue = args["nullValue"]?.let(java.lang.Boolean::parseBoolean) ?: false val intValue = args["intValue"]?.toIntOrNull() val floatValue = args["floatValue"]?.toFloatOrNull() val longValue = args["longValue"]?.toLongOrNull() @@ -139,22 +138,17 @@ class ConstantInjectionPoint : InjectionPoint() { val stringValue = args["stringValue"] val classValue = args["classValue"]?.ifNotBlank { Type.getObjectType(it.replace('.', '/')) } val count = - nullValue.toInt() + - (intValue != null).toInt() + + (intValue != null).toInt() + (floatValue != null).toInt() + (longValue != null).toInt() + (doubleValue != null).toInt() + (stringValue != null).toInt() + (classValue != null).toInt() - if (count != 1) { + if ("nullValue" in args || count != 1) { return null } - val constant = if (nullValue) { - null - } else { - intValue ?: floatValue ?: longValue ?: doubleValue ?: stringValue ?: classValue!! - } + val constant = intValue ?: floatValue ?: longValue ?: doubleValue ?: stringValue ?: classValue!! return ConstantInfo(constant, parseExpandConditions(args)) } @@ -195,7 +189,7 @@ class ConstantInjectionPoint : InjectionPoint() { return null } - class ConstantInfo(val constant: Any?, val expandConditions: Set) + class ConstantInfo(val constant: Any, val expandConditions: Set) enum class ExpandCondition(vararg val opcodes: Int) { LESS_THAN_ZERO(Opcodes.IFLT, Opcodes.IFGE), From 42b371ef9434a69cc0464fe09dae5b370b8d67e2 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Sat, 19 Sep 2026 15:08:52 +0100 Subject: [PATCH 04/51] stash --- .../mixin/expression/MEExpressionMatchUtil.kt | 4 +- .../mixin/expression/gui/MEShowFlowAction.kt | 7 +- .../mixin/handlers/InjectAnnotationHandler.kt | 53 +-- .../handlers/InjectorAnnotationHandler.kt | 107 ++--- .../handlers/InsnInjectorAnnotationHandler.kt | 141 +++++++ .../mixin/handlers/ModifyArgHandler.kt | 108 +++-- .../mixin/handlers/ModifyArgsHandler.kt | 20 +- .../mixin/handlers/ModifyConstantHandler.kt | 139 +++---- .../mixin/handlers/ModifyVariableHandler.kt | 50 +-- .../mixin/handlers/RedirectInjectorHandler.kt | 139 +++---- .../handlers/injectionPoint/AtResolver.kt | 4 +- .../injectionPoint/ConstantInjectionPoint.kt | 195 ++++----- .../ConstantStringMethodInjectionPoint.kt | 4 +- .../injectionPoint/FieldInjectionPoint.kt | 4 +- .../handlers/injectionPoint/InjectionPoint.kt | 10 +- .../InvokeAssignInjectionPoint.kt | 2 +- .../injectionPoint/InvokeInjectionPoint.kt | 2 +- .../injectionPoint/LoadInjectionPoint.kt | 11 +- .../injectionPoint/NewInsnInjectionPoint.kt | 4 +- .../MixinExtrasInjectorAnnotationHandler.kt | 85 +--- .../ModifyExpressionValueHandler.kt | 8 +- .../mixinextras/ModifyReceiverHandler.kt | 2 +- .../mixinextras/ModifyReturnValueHandler.kt | 2 +- .../mixin/handlers/mixinextras/ShareUtil.kt | 7 +- .../handlers/mixinextras/WrapMethodHandler.kt | 38 +- .../mixinextras/WrapOperationHandler.kt | 13 +- .../mixinextras/WrapWithConditionHandler.kt | 2 +- .../CancellableBeforeSuperCallInspection.kt | 4 +- .../DisallowedTargetInsnInspection.kt | 13 +- .../injector/DiscouragedShiftInspection.kt | 6 +- .../inspection/injector/ExpectedSignatures.kt | 25 ++ ...nvalidInjectorMethodSignatureInspection.kt | 374 ++++++++---------- .../inspection/injector/MethodSignature.kt | 85 ++-- .../injector/MixinParameterNameInspection.kt | 248 ++++++------ .../ModifyVariableArgsOnlyInspection.kt | 6 +- .../ModifyVariableMayUseNameInspection.kt | 9 +- .../inspection/injector/SuggestedSignature.kt | 252 ++++++++++++ ...tLocalCaptureReplaceWithLocalInspection.kt | 4 +- .../mixinextras/LocalArgsOnlyInspection.kt | 4 +- .../mixinextras/LocalMayUseNameInspection.kt | 6 +- .../UnnecessaryMutableLocalInspection.kt | 4 +- .../UnresolvedLocalCaptureInspection.kt | 4 +- .../kotlin/platform/mixin/util/AsmUtil.kt | 18 +- .../kotlin/platform/mixin/util/LocalInfo.kt | 2 +- src/main/kotlin/util/utils.kt | 4 +- 45 files changed, 1216 insertions(+), 1013 deletions(-) create mode 100644 src/main/kotlin/platform/mixin/handlers/InsnInjectorAnnotationHandler.kt create mode 100644 src/main/kotlin/platform/mixin/inspection/injector/ExpectedSignatures.kt create mode 100644 src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt 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 b640f9480..c712859e3 100644 --- a/src/main/kotlin/platform/mixin/handlers/InjectAnnotationHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/InjectAnnotationHandler.kt @@ -20,6 +20,7 @@ package com.demonwav.mcdev.platform.mixin.handlers +import com.demonwav.mcdev.platform.mixin.handlers.mixinextras.TargetInsn import com.demonwav.mcdev.platform.mixin.inspection.injector.MethodSignature import com.demonwav.mcdev.platform.mixin.util.LocalVariables import com.demonwav.mcdev.platform.mixin.util.callbackInfoReturnableType @@ -30,7 +31,6 @@ 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 @@ -43,11 +43,12 @@ 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, + targetInsn: TargetInsn, ): List { val returnType = targetMethod.getGenericReturnType(targetClass, annotation.project) @@ -72,34 +73,20 @@ class InjectAnnotationHandler : InjectorAnnotationHandler() { ?.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) - capturedLocals = commonLocalsPrefix.map { local -> - val type = - Type.getType(local.desc).toPsiType(elementFactory, annotation.parentOfType()) - sanitizedParameter(type, local.name) - } + 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) } } } @@ -109,7 +96,8 @@ class InjectAnnotationHandler : InjectorAnnotationHandler() { MethodSignature( targetParams + ciParam, PsiTypes.voidType(), - capturedLocals, + trailingParams = capturedLocals, + allowCoerceRequired = true, trailingByDefault = true, ) ) @@ -119,6 +107,7 @@ class InjectAnnotationHandler : InjectorAnnotationHandler() { MethodSignature( listOf(ciParam), PsiTypes.voidType(), + allowCoerceRequired = true, ) ) @@ -129,8 +118,6 @@ class InjectAnnotationHandler : InjectorAnnotationHandler() { 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..d6cd07b37 100644 --- a/src/main/kotlin/platform/mixin/handlers/InjectorAnnotationHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/InjectorAnnotationHandler.kt @@ -22,25 +22,21 @@ package com.demonwav.mcdev.platform.mixin.handlers 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.handlers.mixinextras.TargetInsn import com.demonwav.mcdev.platform.mixin.inspection.injector.MethodSignature +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 +45,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 +81,47 @@ 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() } - } - - fun resolveInstructions(annotation: PsiAnnotation) = annotation.cached(PsiModificationTracker.MODIFICATION_COUNT) { - val containingClass = annotation.findContainingClass() ?: return@cached emptyList() - containingClass.mixinTargets.flatMap { resolveInstructions(annotation, it) } - } + ): List - 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, + ): 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( + open 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? = null 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( @@ -218,14 +153,24 @@ abstract class InjectorAnnotationHandler : MixinAnnotationHandler { Parameter(name?.toJavaIdentifier(), type, knownName) } } + + @JvmStatic + protected fun sanitizedReturnType(type: PsiType): PsiType { + return if (type is PsiEllipsisType) { + type.toArrayType() + } else { + type + } + } } } -object DefaultInjectorAnnotationHandler : InjectorAnnotationHandler() { +object DefaultInjectorAnnotationHandler : InsnInjectorAnnotationHandler() { override fun expectedMethodSignature( annotation: PsiAnnotation, targetClass: ClassNode, targetMethod: MethodNode, + targetInsn: TargetInsn, ) = 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..0c33bb0cd --- /dev/null +++ b/src/main/kotlin/platform/mixin/handlers/InsnInjectorAnnotationHandler.kt @@ -0,0 +1,141 @@ +/* + * 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.MethodSignature +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, + ): List> { + return resolveInstructions(annotation, targets).mapNotNull { (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, + ): List? + + 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 e340e3406..e9e8821d1 100644 --- a/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt @@ -20,12 +20,14 @@ package com.demonwav.mcdev.platform.mixin.handlers +import com.demonwav.mcdev.platform.mixin.handlers.mixinextras.TargetInsn import com.demonwav.mcdev.platform.mixin.inspection.injector.MethodSignature -import com.demonwav.mcdev.platform.mixin.util.fakeResolve -import com.demonwav.mcdev.platform.mixin.util.getParameter +import com.demonwav.mcdev.platform.mixin.inspection.injector.SuggestedSignature +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.constantValue -import com.demonwav.mcdev.util.descriptor import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiMethod @@ -36,7 +38,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 } @@ -47,85 +49,56 @@ class ModifyArgHandler : InjectorAnnotationHandler() { annotation: PsiAnnotation, targetClass: ClassNode, targetMethod: MethodNode, + targetInsn: TargetInsn, ): List? { + val insn = targetInsn.insn + if (insn !is MethodInsnNode) { + return null + } + 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 emptyList() } // 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) + return validTypes.flatMap { type -> + val psiParams = argTypes.indices.map { index -> sourceMethod?.getBytecodeParameter(index) } + val targetParam = psiParams[index ?: argTypes.indexOf(type)] + val psiType = targetParam?.type ?: type.toPsiType(elementFactory) val singleSignature = MethodSignature( listOf( - sanitizedParameter(psiType, psiParameter?.name), + sanitizedParameter(psiType, targetParam?.name), ), - psiType, + sanitizedReturnType(psiType), + allowCoerceRequired = false, ) - if (validFullSignature != null) { + if (argTypes.size > 1) { val fullParams = - Type.getArgumentTypes(validFullSignature).withIndex().map { (index, argType) -> - val psiParam = paramList?.let { bytecodeMethod.getParameter(bytecodeClass, index, it) } + psiParams.zip(argTypes) { param, argType -> sanitizedParameter( - psiParam?.type ?: argType.toPsiType(elementFactory), - psiParam?.name, + param?.type ?: argType.toPsiType(elementFactory), + param?.name, ) } listOf( singleSignature, - MethodSignature(fullParams, psiType), + MethodSignature(fullParams, sanitizedReturnType(psiType), allowCoerceRequired = false), ) } else { listOf(singleSignature) @@ -133,5 +106,12 @@ class ModifyArgHandler : InjectorAnnotationHandler() { } } + override fun suggestedMethodSignature( + annotation: PsiAnnotation, + targets: List + ): SuggestedSignature? { + return SuggestedSignature.modifierNoCoerce(annotation, targets, this) + } + 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 97f057503..90e81d28d 100644 --- a/src/main/kotlin/platform/mixin/handlers/ModifyArgsHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/ModifyArgsHandler.kt @@ -20,7 +20,10 @@ package com.demonwav.mcdev.platform.mixin.handlers +import com.demonwav.mcdev.platform.mixin.handlers.mixinextras.TargetInsn import com.demonwav.mcdev.platform.mixin.inspection.injector.MethodSignature +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 @@ -32,7 +35,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 } @@ -43,6 +46,7 @@ class ModifyArgsHandler : InjectorAnnotationHandler() { annotation: PsiAnnotation, targetClass: ClassNode, targetMethod: MethodNode, + targetInsn: TargetInsn, ): List { val argsType = JavaPsiFacade.getElementFactory(annotation.project) .createTypeByFQClassName(ARGS, annotation.resolveScope) @@ -51,13 +55,27 @@ class ModifyArgsHandler : InjectorAnnotationHandler() { MethodSignature( shortParams, PsiTypes.voidType(), + allowCoerceRequired = false, ), 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(SuggestedSignature.Param("args", argsType)), + PsiTypes.voidType(), + ) + } + override val mixinExtrasExpressionContextType = ExpressionContext.Type.MODIFY_ARGS } diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt index e260338d4..31050df2a 100644 --- a/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt @@ -22,17 +22,16 @@ 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.handlers.mixinextras.TargetInsn import com.demonwav.mcdev.platform.mixin.inspection.injector.MethodSignature -import com.demonwav.mcdev.util.findAnnotations -import com.intellij.openapi.project.Project +import com.demonwav.mcdev.platform.mixin.inspection.injector.SuggestedSignature +import com.demonwav.mcdev.platform.mixin.util.ClassAndMethodNode 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 @@ -40,8 +39,11 @@ import org.objectweb.asm.tree.AbstractInsnNode import org.objectweb.asm.tree.ClassNode import org.objectweb.asm.tree.MethodNode -class ModifyConstantHandler : InjectorAnnotationHandler() { +class ModifyConstantHandler : InsnInjectorAnnotationHandler() { + private val constantInjectionPoint by lazy { InjectionPoint.byAtCode("CONSTANT") as ConstantInjectionPoint } + private val allowedOpcodes = setOf( + Opcodes.ACONST_NULL, Opcodes.ICONST_M1, Opcodes.ICONST_0, Opcodes.ICONST_1, @@ -63,92 +65,95 @@ 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(sanitizedParameter(returnType, constantParamName)), - returnType, - collectTargetMethodParameters(annotation.project, targetClass, targetMethod), + targetInsn: TargetInsn, + ): List? { + val targetParams = collectTargetMethodParameters(annotation.project, targetClass, targetMethod) + val cst = constantInjectionPoint.getTargetedConstant(targetInsn.insn) ?: return emptyList() + return basicMethodSignatures(annotation, cst)?.map { it.copy(trailingParams = targetParams) } + } + + override fun suggestedMethodSignature( + annotation: PsiAnnotation, + targets: List + ): SuggestedSignature? { + val isTypeCheck = + resolveInstructions(annotation, targets).asSequence() + .map { it.result.insn.opcode == Opcodes.INSTANCEOF } + .distinct() + .singleOrNull() ?: return null + + return if (isTypeCheck) { + SuggestedSignature.exact( + makeTypeCheckMethodSignature( + PsiManager.getInstance(annotation.project), + annotation, + PsiTypes.booleanType(), ) ) + } else { + SuggestedSignature.modifierNoCoerce(annotation, targets, this) } + } + private fun basicMethodSignatures(annotation: PsiAnnotation, cst: Any): List? { val psiManager = PsiManager.getInstance(annotation.project) - return constantInfos.asSequence() - .distinctBy { it.constant.javaClass } - .flatMap { - when (it.constant) { - is Int -> sequenceOf( - makeMethodSignature(annotation.project, targetClass, targetMethod, PsiTypes.intType()), - ) - 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() + + return if (cst is Type) { + listOf( + makeTypeCheckMethodSignature( + psiManager, + annotation, + PsiTypes.booleanType(), + ), + makeTypeCheckMethodSignature( + psiManager, + annotation, + getClassType(psiManager, annotation), + ), + ) + } else { + listOf( + makeMethodSignature( + getConstantType(annotation, cst) + ?: throw IllegalStateException("Unknown constant type: ${cst.javaClass.name}") + ), + ) + } } - private fun makeMethodSignature( - project: Project, - targetClass: ClassNode, - targetMethod: MethodNode, - type: PsiType, - ): MethodSignature { + 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 + ) + + else -> null + } + + private fun makeMethodSignature(type: PsiType): MethodSignature { return MethodSignature( listOf(sanitizedParameter(type, "constant")), type, - collectTargetMethodParameters(project, targetClass, targetMethod), + allowCoerceRequired = true, ) } private fun makeTypeCheckMethodSignature( - project: Project, psiManager: PsiManager, context: PsiElement, - targetClass: ClassNode, - targetMethod: MethodNode, returnType: PsiType, ): MethodSignature { return MethodSignature( @@ -157,7 +162,7 @@ class ModifyConstantHandler : InjectorAnnotationHandler() { sanitizedParameter(getClassType(psiManager, context), "type"), ), returnType, - collectTargetMethodParameters(project, targetClass, targetMethod), + allowCoerceRequired = false, ) } diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt index aff8da459..9f8873b2c 100644 --- a/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt @@ -20,13 +20,13 @@ 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.handlers.mixinextras.TargetInsn import com.demonwav.mcdev.platform.mixin.inspection.injector.MethodSignature +import com.demonwav.mcdev.platform.mixin.inspection.injector.SuggestedSignature +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.findContainingMethod import com.demonwav.mcdev.util.findModule import com.intellij.psi.JavaPsiFacade @@ -36,20 +36,14 @@ 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, + targetInsn: TargetInsn, ): List? { val module = annotation.findModule() ?: return null - - 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 targetParams = collectTargetMethodParameters(annotation.project, targetClass, targetMethod) val method = annotation.findContainingMethod() ?: return null @@ -59,26 +53,32 @@ class ModifyVariableHandler : InjectorAnnotationHandler() { 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(sanitizedParameter(localType, local.name, local.isNamed)), - localType, - targetParams, - ) - } + val matchedLocals = info.matchLocals( + module, targetClass, targetMethod, targetInsn.insn, + CollectVisitor.Mode.SUGGESTION, matchType = false + ).orEmpty() + for (local in matchedLocals) { + if (seenParams.add(local.desc + local.name)) { + val localType = Type.getType(local.desc).toPsiType(elementFactory) + result += MethodSignature( + listOf(sanitizedParameter(localType, local.name, local.isNamed)), + localType, + trailingParams = targetParams, + allowCoerceRequired = false, + ) } } return result } + override fun suggestedMethodSignature( + annotation: PsiAnnotation, + targets: List + ): SuggestedSignature? { + return SuggestedSignature.modifierNoCoerce(annotation, targets, this) + } + override val isShiftAlwaysDiscouraged = false override val mixinExtrasExpressionContextType = ExpressionContext.Type.MODIFY_VARIABLE diff --git a/src/main/kotlin/platform/mixin/handlers/RedirectInjectorHandler.kt b/src/main/kotlin/platform/mixin/handlers/RedirectInjectorHandler.kt index a8d74774b..e2214ca93 100644 --- a/src/main/kotlin/platform/mixin/handlers/RedirectInjectorHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/RedirectInjectorHandler.kt @@ -21,6 +21,7 @@ package com.demonwav.mcdev.platform.mixin.handlers import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.NewInsnInjectionPoint +import com.demonwav.mcdev.platform.mixin.handlers.mixinextras.TargetInsn import com.demonwav.mcdev.platform.mixin.inspection.injector.MethodSignature import com.demonwav.mcdev.platform.mixin.util.AsmDfaUtil import com.demonwav.mcdev.platform.mixin.util.FieldTargetMember @@ -49,7 +50,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 -> { @@ -87,31 +88,37 @@ class RedirectInjectorHandler : InjectorAnnotationHandler() { annotation: PsiAnnotation, targetClass: ClassNode, targetMethod: MethodNode, + targetInsn: TargetInsn, ): List? { - val insns = resolveInstructions(annotation, targetClass, targetMethod).ifEmpty { return emptyList() } val extraParams = collectTargetMethodParameters(annotation.project, targetClass, targetMethod) - return getRedirectType(insns[0].insn)?.expectedMethodSignature( + val redirectType = getRedirectType(targetInsn.insn) ?: return null + return redirectType.expectedMethodSignature( annotation, targetClass, targetMethod, - insns.map { it.insn }, + targetInsn.insn, )?.map { (params, returnType) -> - MethodSignature(params, returnType, extraParams) + MethodSignature( + params, + returnType, + trailingParams = extraParams, + allowCoerceRequired = redirectType.allowCoerce, + ) } } - override val allowCoerce = true - override val mixinExtrasExpressionContextType = ExpressionContext.Type.REDIRECT private interface RedirectType { + val allowCoerce: Boolean get() = true + fun isInsnAllowed(node: AbstractInsnNode) = true fun expectedMethodSignature( annotation: PsiAnnotation, targetClass: ClassNode, targetMethod: MethodNode, - insns: List, + insn: AbstractInsnNode, ): List, PsiType>>? } @@ -138,24 +145,15 @@ class RedirectInjectorHandler : InjectorAnnotationHandler() { annotation: PsiAnnotation, targetClass: ClassNode, targetMethod: MethodNode, - insns: List, + insn: AbstractInsnNode, ): List, PsiType>> { - 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 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(parameters to fieldType) @@ -167,24 +165,15 @@ class RedirectInjectorHandler : InjectorAnnotationHandler() { annotation: PsiAnnotation, targetClass: ClassNode, targetMethod: MethodNode, - insns: List, + insn: AbstractInsnNode, ): List, PsiType>> { - 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 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) @@ -201,25 +190,14 @@ class RedirectInjectorHandler : InjectorAnnotationHandler() { annotation: PsiAnnotation, targetClass: ClassNode, targetMethod: MethodNode, - insns: List, + insn: AbstractInsnNode, ): List, PsiType>> { - 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 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( @@ -228,8 +206,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 } @@ -238,19 +216,19 @@ 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) + val returnType = signature?.first ?: Type.getReturnType(insn.desc).toPsiType(elementFactory) return listOf(parameters to returnType) } } @@ -260,13 +238,9 @@ class RedirectInjectorHandler : InjectorAnnotationHandler() { annotation: PsiAnnotation, targetClass: ClassNode, targetMethod: MethodNode, - insns: List, + insn: AbstractInsnNode, ): List, PsiType>>? { - 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) + val arrayType = AsmDfaUtil.getStackType(annotation.project, targetClass, targetMethod, insn, 0) ?: return null if (arrayType.sort != Type.ARRAY) { return null @@ -285,13 +259,9 @@ class RedirectInjectorHandler : InjectorAnnotationHandler() { annotation: PsiAnnotation, targetClass: ClassNode, targetMethod: MethodNode, - insns: List, + insn: AbstractInsnNode, ): List, PsiType>>? { - 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) + val arrayType = AsmDfaUtil.getStackType(annotation.project, targetClass, targetMethod, insn, 1) ?: return null if (arrayType.sort != Type.ARRAY) { return null @@ -314,13 +284,9 @@ class RedirectInjectorHandler : InjectorAnnotationHandler() { annotation: PsiAnnotation, targetClass: ClassNode, targetMethod: MethodNode, - insns: List, + insn: AbstractInsnNode, ): List, PsiType>>? { - 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) + val arrayType = AsmDfaUtil.getStackType(annotation.project, targetClass, targetMethod, insn, 2) ?: return null if (arrayType.sort != Type.ARRAY) { return null @@ -348,27 +314,18 @@ class RedirectInjectorHandler : InjectorAnnotationHandler() { annotation: PsiAnnotation, targetClass: ClassNode, targetMethod: MethodNode, - insns: List, + insn: AbstractInsnNode, ): List, PsiType>> { - 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 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) - }, + NewInsnInjectionPoint.Util.findInitCall(insn) ?: return emptyList() ).map { (params, _) -> // drop the instance parameter, return the constructed type params.drop(1) to constructedType @@ -377,16 +334,14 @@ class RedirectInjectorHandler : InjectorAnnotationHandler() { } private object InstanceOf : RedirectType { + override val allowCoerce: Boolean get() = false + override fun expectedMethodSignature( annotation: PsiAnnotation, targetClass: ClassNode, targetMethod: MethodNode, - insns: List, + insn: AbstractInsnNode, ): List, PsiType>> { - val firstMatch = insns.first() - val isValid = insns.all { it.opcode == firstMatch.opcode } - if (!isValid) return emptyList() - val psiManager = PsiManager.getInstance(annotation.project) val elementFactory = JavaPsiFacade.getElementFactory(annotation.project) val objectType = PsiType.getJavaLangObject(psiManager, annotation.resolveScope) 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 a4bb9cfb4..840b8d7ef 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 @@ -129,7 +130,7 @@ class ConstantInjectionPoint : InjectionPoint() { override fun getArgValueListDelimiter(at: PsiAnnotation, key: String) = Const.COMMA_LIST_DELIMITER.takeIf { key == "expandZeroConditions" } - fun getConstantInfo(at: PsiAnnotation): ConstantInfo? { + fun getConstantInfos(at: PsiAnnotation): List { val args = AtResolver.getArgs(at) val intValue = args["intValue"]?.toIntOrNull() val floatValue = args["floatValue"]?.toFloatOrNull() @@ -137,20 +138,13 @@ class ConstantInjectionPoint : InjectionPoint() { val doubleValue = args["doubleValue"]?.toDoubleOrNull() val stringValue = args["stringValue"] val classValue = args["classValue"]?.ifNotBlank { Type.getObjectType(it.replace('.', '/')) } - val count = - (intValue != null).toInt() + - (floatValue != null).toInt() + - (longValue != null).toInt() + - (doubleValue != null).toInt() + - (stringValue != null).toInt() + - (classValue != null).toInt() - if ("nullValue" in args || count != 1) { - return null - } - - val constant = intValue ?: floatValue ?: longValue ?: doubleValue ?: stringValue ?: classValue!! + val nullValue = args["nullValue"]?.toBooleanStrictOrNull() + val expandZeroConditions = parseExpandConditions(args) - return ConstantInfo(constant, parseExpandConditions(args)) + return listOfNotNull( + intValue, floatValue, longValue, doubleValue, stringValue, classValue + ).map { ConstantInfo(it, expandZeroConditions) } + + if (nullValue == true) listOf(ConstantInfo(null, expandZeroConditions)) else emptyList() } private fun parseExpandConditions(args: Map): Set { @@ -161,16 +155,12 @@ class ConstantInjectionPoint : InjectionPoint() { ?.toSet() ?: emptySet() } - private fun Boolean.toInt(): Int { - return if (this) 1 else 0 - } - override fun createNavigationVisitor( at: PsiAnnotation, target: MixinSelector?, targetClass: PsiClass, ): NavigationVisitor { - return MyNavigationVisitor(getConstantInfo(at)) + return MyNavigationVisitor(getConstantInfos(at)) } override fun doCreateCollectVisitor( @@ -179,7 +169,7 @@ class ConstantInjectionPoint : InjectionPoint() { targetClass: ClassNode, mode: CollectVisitor.Mode, ): CollectVisitor { - return MyCollectVisitor(at.project, mode, getConstantInfo(at)) + return MyCollectVisitor(at.project, mode, getConstantInfos(at)) } override fun createLookup( @@ -189,7 +179,11 @@ class ConstantInjectionPoint : InjectionPoint() { return null } - class ConstantInfo(val constant: Any, val expandConditions: Set) + 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) { LESS_THAN_ZERO(Opcodes.IFLT, Opcodes.IFGE), @@ -199,7 +193,7 @@ class ConstantInjectionPoint : InjectionPoint() { } class MyNavigationVisitor( - private val constantInfo: ConstantInfo?, + private val constantInfos: List, private val expectedType: Type? = null, ) : NavigationVisitor() { override fun visitForeachStatement(statement: PsiForeachStatement) { @@ -234,121 +228,96 @@ class ConstantInjectionPoint : InjectionPoint() { } private fun visitConstant(element: PsiElement, value: Any?) { - if (constantInfo != null && value != constantInfo.constant) { - return - } - - if (expectedType != null && value != null) { - // First check if we expect any String literal - if (value is String && - (expectedType.sort != Type.OBJECT || expectedType.className != CommonClassNames.JAVA_LANG_STRING) - ) { - return - } - - // then check if we expect any class literal - if (value is Type && ( - expectedType.sort != Type.ARRAY && expectedType.sort != Type.OBJECT || - expectedType.className != CommonClassNames.JAVA_LANG_CLASS - ) - ) { - return - } - - // otherwise we expect a primitive literal - if (expectedType.sort in Type.BOOLEAN..Type.DOUBLE && - value::class.javaPrimitiveType?.let(Type::getType) != expectedType - ) { - return + for (constantInfo in constantInfos) { + if (value != constantInfo.constant) { + continue } - } - - val parent = PsiUtil.skipParenthesizedExprUp(element.parent) - // check for expandZeroConditions - if (value == null || value == 0) { - if (parent is PsiBinaryExpression) { - val operation = parent.operationTokenType - if (operation == JavaTokenType.EQEQ || operation == JavaTokenType.NE) { - return + if (expectedType != null && value != null) { + // First check if we expect any String literal + if (value is String && + (expectedType.sort != Type.OBJECT || expectedType.className != CommonClassNames.JAVA_LANG_STRING) + ) { + continue } - val opcode = when (operation) { - JavaTokenType.LT -> Opcodes.IFLT - JavaTokenType.LE -> Opcodes.IFLE - JavaTokenType.GT -> Opcodes.IFGT - JavaTokenType.GE -> Opcodes.IFGE - else -> null + + // then check if we expect any class literal + if (value is Type && ( + expectedType.sort != Type.ARRAY && expectedType.sort != Type.OBJECT || + expectedType.className != CommonClassNames.JAVA_LANG_CLASS + ) + ) { + continue } - if (opcode != null && ( - constantInfo == null || - !constantInfo.expandConditions.any { opcode in it.opcodes } - ) + + // otherwise we expect a primitive literal + if (expectedType.sort in Type.BOOLEAN..Type.DOUBLE && + value::class.javaPrimitiveType?.let(Type::getType) != expectedType ) { - return + continue } } - } - // check for switch statement (compiles to tableswitch or lookupswitch which aren't targeted) - if (parent is PsiSwitchLabelStatementBase) { - return - } + val parent = PsiUtil.skipParenthesizedExprUp(element.parent) + + // check for expandZeroConditions + if (value == null || value == 0) { + if (parent is PsiBinaryExpression) { + val operation = parent.operationTokenType + if (operation == JavaTokenType.EQEQ || operation == JavaTokenType.NE) { + continue + } + val opcode = when (operation) { + JavaTokenType.LT -> Opcodes.IFLT + JavaTokenType.LE -> Opcodes.IFLE + JavaTokenType.GT -> Opcodes.IFGT + JavaTokenType.GE -> Opcodes.IFGE + else -> null + } + if (opcode != null && ( + !constantInfo.expandConditions.any { opcode in it.opcodes } + ) + ) { + continue + } + } + } - addResult(element) + // check for switch statement (compiles to tableswitch or lookupswitch which aren't targeted) + if (parent is PsiSwitchLabelStatementBase) { + continue + } + + addResult(element) + } } } class MyCollectVisitor( private val project: Project, mode: Mode, - private val constantInfo: ConstantInfo?, - private val expectedType: Type? = null, + private val constantInfos: List, ) : CollectVisitor(mode) { override fun accept(methodNode: MethodNode) = sequence { for (insn in methodNode.instructions ?: emptyList()) { - val constant = ( - insn.computeConstantValue(constantInfo?.expandConditions ?: emptySet()) - ?: continue - ).let { if (it is NullSentinel) null else it } - - if (constantInfo != null && constant != constantInfo.constant) { - continue - } + for (constantInfo in constantInfos) { + val constant = ( + insn.computeConstantValue(constantInfo.expandConditions) + ?: continue + ).let { if (it is NullSentinel) null else it } - if (expectedType != null && constant != null) { - // First check if we expect any String literal - if (constant is String && ( - expectedType.sort != Type.OBJECT || - expectedType.className != CommonClassNames.JAVA_LANG_STRING - ) - ) { - continue - } - - // then check if we expect any class literal - if (constant is Type && ( - expectedType.sort != Type.ARRAY && expectedType.sort != Type.OBJECT || - expectedType.className != CommonClassNames.JAVA_LANG_CLASS - ) - ) { + if (constant != constantInfo.constant) { continue } - // otherwise we expect a primitive literal - if (expectedType.sort in Type.BOOLEAN..Type.DOUBLE && - constant::class.javaPrimitiveType?.let(Type::getType) != expectedType - ) { - continue + val elementFactory = JavaPsiFacade.getElementFactory(project) + val literal = if (constant is Type) { + elementFactory.createExpressionFromText("${constant.className}.class", null) + } else { + elementFactory.createLiteralExpression(constant) } + addResult(insn, literal) } - - val elementFactory = JavaPsiFacade.getElementFactory(project) - val literal = if (constant is Type) { - elementFactory.createExpressionFromText("${constant.className}.class", null) - } else { - elementFactory.createLiteralExpression(constant) - } - addResult(insn, literal) } } } 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 b4fc5dfe6..b08fe69d3 100644 --- a/src/main/kotlin/platform/mixin/handlers/mixinextras/MixinExtrasInjectorAnnotationHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/MixinExtrasInjectorAnnotationHandler.kt @@ -20,7 +20,7 @@ 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.util.FieldTargetMember @@ -48,7 +48,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 { @@ -98,7 +98,7 @@ abstract class MixinExtrasInjectorAnnotationHandler : InjectorAnnotationHandler( return supportedInstructionTypes.any { it.matches(TargetInsn(insn, decorations)) } } - abstract fun expectedMethodSignature( + abstract fun expectedMethodSignatureImpl( annotation: PsiAnnotation, targetClass: ClassNode, targetMethod: MethodNode, @@ -107,70 +107,26 @@ abstract class MixinExtrasInjectorAnnotationHandler : InjectorAnnotationHandler( open fun intLikeTypePositions( target: TargetInsn - ): List = emptyList() + ): Set = emptySet() - 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: List, - returnType: PsiType, - intLikeTypePositions: List + targetInsn: TargetInsn ): List { - if (intLikeTypePositions.isEmpty()) { - return listOf( - makeSignature(annotation, targetClass, targetMethod, params, returnType, intLikeTypePositions) + val signature = expectedMethodSignatureImpl(annotation, targetClass, targetMethod, targetInsn) ?: return emptyList() + val intLikeTypePositions = intLikeTypePositions(targetInsn) + return listOf( + makeSignature( + annotation, + targetClass, + targetMethod, + signature.first, + signature.second, + intLikeTypePositions, ) - } - return buildList { - for (actualType in intLikePsiTypes) { - val newParams = params.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, - newParams, - newReturnType, - intLikeTypePositions - ) - ) - } - } + ) } private fun makeSignature( @@ -179,12 +135,13 @@ abstract class MixinExtrasInjectorAnnotationHandler : InjectorAnnotationHandler( targetMethod: MethodNode, params: List, returnType: PsiType, - intLikeTypePositions: List + intLikeTypePositions: Set ) = MethodSignature( params, returnType, - collectTargetMethodParameters(annotation.project, targetClass, targetMethod), - intLikeTypes = intLikeTypePositions + trailingParams = collectTargetMethodParameters(annotation.project, targetClass, targetMethod), + allowCoerceRequired = true, + intLikeTypes = intLikeTypePositions, ) protected fun getInsnReturnType(insn: AbstractInsnNode): Type? { diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyExpressionValueHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyExpressionValueHandler.kt index 5b2d6191e..f760b729f 100644 --- a/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyExpressionValueHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyExpressionValueHandler.kt @@ -55,7 +55,7 @@ class ModifyExpressionValueHandler : MixinExtrasInjectorAnnotationHandler() { override val allowedInsnDescription = "instructions that return a value" - override fun expectedMethodSignature( + override fun expectedMethodSignatureImpl( annotation: PsiAnnotation, targetClass: ClassNode, targetMethod: MethodNode, @@ -65,12 +65,12 @@ class ModifyExpressionValueHandler : MixinExtrasInjectorAnnotationHandler() { return listOf(Parameter("original", psiType)) to psiType } - override fun intLikeTypePositions(target: TargetInsn): List { + override fun intLikeTypePositions(target: TargetInsn): Set { val expressionType = target.getDecoration(ExpressionDecorations.SIMPLE_EXPRESSION_TYPE) if (expressionType == ExpressionASMUtils.INTLIKE_TYPE) { - return listOf(MethodSignature.TypePosition.Return, MethodSignature.TypePosition.Param(0)) + return setOf(MethodSignature.TypePosition.Return, MethodSignature.TypePosition.Param(0)) } - return emptyList() + return emptySet() } 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 31ca20f6a..3d8df455d 100644 --- a/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReceiverHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReceiverHandler.kt @@ -43,7 +43,7 @@ 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, diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReturnValueHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReturnValueHandler.kt index 2c2dd5902..bc8364b59 100644 --- a/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReturnValueHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReturnValueHandler.kt @@ -33,7 +33,7 @@ class ModifyReturnValueHandler : MixinExtrasInjectorAnnotationHandler() { override val allowedInsnDescription = "return instructions" - override fun expectedMethodSignature( + override fun expectedMethodSignatureImpl( annotation: PsiAnnotation, targetClass: ClassNode, targetMethod: MethodNode, 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 4d9155cee..10c2a0dfe 100644 --- a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt @@ -23,6 +23,8 @@ package com.demonwav.mcdev.platform.mixin.handlers.mixinextras import com.demonwav.mcdev.platform.mixin.handlers.InjectorAnnotationHandler 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.SuggestedSignature +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 @@ -30,30 +32,36 @@ import com.demonwav.mcdev.util.Parameter import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiElement 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, + ): List> { + return targets.map { (targetClass, targetMethod) -> + val returnType = targetMethod.getGenericReturnType(targetClass, annotation.project) - return listOf( - MethodSignature( - collectTargetMethodParameters(annotation.project, targetClass, targetMethod) + + listOf( + MethodSignature( + collectTargetMethodParameters(annotation.project, targetClass, targetMethod) + Parameter( "original", - mixinExtrasOperationType(annotation, returnType) ?: return emptyList() + mixinExtrasOperationType(annotation, returnType) ?: return@map emptyList() ), - returnType + returnType, + allowCoerceRequired = true, + ) ) - ) + } + } + + override fun suggestedMethodSignature( + annotation: PsiAnnotation, + targets: List + ): SuggestedSignature? { + return SuggestedSignature.operationWrapper(annotation, targets, this) } override fun isUnresolved( @@ -78,6 +86,4 @@ class WrapMethodHandler : InjectorAnnotationHandler() { canDecompile = true )?.let(::listOf).orEmpty() } - - override val mixinExtrasExpressionContextType = ExpressionContext.Type.CUSTOM } diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt index 719531ff5..18e24dd7c 100644 --- a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt @@ -21,6 +21,8 @@ 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.SuggestedSignature +import com.demonwav.mcdev.platform.mixin.util.ClassAndMethodNode import com.demonwav.mcdev.platform.mixin.util.mixinExtrasOperationType import com.demonwav.mcdev.platform.mixin.util.toPsiType import com.demonwav.mcdev.util.Parameter @@ -46,7 +48,7 @@ class WrapOperationHandler : MixinExtrasInjectorAnnotationHandler() { return if (annotation.hasAttribute("constant")) "constant" else "at" } - override fun expectedMethodSignature( + override fun expectedMethodSignatureImpl( annotation: PsiAnnotation, targetClass: ClassNode, targetMethod: MethodNode, @@ -60,7 +62,7 @@ class WrapOperationHandler : MixinExtrasInjectorAnnotationHandler() { ) to returnType } - override fun intLikeTypePositions(target: TargetInsn) = buildList { + override fun intLikeTypePositions(target: TargetInsn) = buildSet { if ( target.getDecoration(ExpressionDecorations.SIMPLE_OPERATION_RETURN_TYPE) == ExpressionASMUtils.INTLIKE_TYPE @@ -74,6 +76,13 @@ class WrapOperationHandler : MixinExtrasInjectorAnnotationHandler() { } } + override fun suggestedMethodSignature( + annotation: PsiAnnotation, + targets: List + ): SuggestedSignature? { + return SuggestedSignature.operationWrapper(annotation, targets, this) + } + private fun getParameterTypes( target: TargetInsn, targetClass: ClassNode, diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapWithConditionHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapWithConditionHandler.kt index 2d2ddcd34..8cb73d64f 100644 --- a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapWithConditionHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapWithConditionHandler.kt @@ -66,7 +66,7 @@ class WrapWithConditionHandler : MixinExtrasInjectorAnnotationHandler() { return getPsiParameters(insn, targetClass, annotation)?.size ?: 0 } - override fun expectedMethodSignature( + override fun expectedMethodSignatureImpl( annotation: PsiAnnotation, targetClass: ClassNode, targetMethod: MethodNode, 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/ExpectedSignatures.kt b/src/main/kotlin/platform/mixin/inspection/injector/ExpectedSignatures.kt new file mode 100644 index 000000000..d0cc8f658 --- /dev/null +++ b/src/main/kotlin/platform/mixin/inspection/injector/ExpectedSignatures.kt @@ -0,0 +1,25 @@ +/* + * 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 + +interface ExpectedSignatures { + val options: List +} diff --git a/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt b/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt index fa3ccbde9..456a941a1 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt @@ -21,9 +21,9 @@ 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.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.findDelegateConstructorCall @@ -31,10 +31,10 @@ 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.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,16 +53,15 @@ 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 @@ -70,13 +69,12 @@ 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,201 +88,132 @@ 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 methodReturnType = method.returnType ?: return + + 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 (expectedSignature in possibleSignatures) { - val paramsMatch = - Util.checkParameters(parameters, expectedSignature, handler.allowCoerce) - if (paramsMatch) { - val methodReturnType = method.returnType - if (methodReturnType != null && - checkReturnType(expectedSignature.returnType, 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 expectedSignature = possibleSignatures[0] - val (expectedParams, expectedReturnType, expectedTrailingParams, trailingByDefault, intLikeTypePositions) = expectedSignature - 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 paramsMatch = Util.checkParameters(parameters, expectedSignature, handler.allowCoerce) - val isWarning = false - val methodReturnType = method.returnType - val returnTypeOk = methodReturnType != null && - checkReturnType(normalizedReturnType, methodReturnType, method, handler.allowCoerce) - val isError = true - if (isWarning || isError) { - reportedSignature = true - - val description = - "Method signature does not match expected signature for $annotationName" - val quickFix = SignatureQuickFix( - method, - when { - paramsMatch -> null - trailingByDefault -> expectedParams + expectedTrailingParams - else -> expectedParams - }, - normalizedReturnType.takeUnless { returnTypeOk }, - intLikeTypePositions - ) - val declarationStart = (method.returnTypeElement ?: identifier).startOffsetInParent - val declarationEnd = method.parameterList.textRangeInParent.endOffset - holder.registerProblem( - method, - description, - ProblemHighlightType.GENERIC_ERROR_OR_WARNING, - 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 + val isAlreadyValid = handler.expectedMethodSignatures(annotation, targetMethods) + .all { options -> + options.any { it.matches(method) } + } + + if (isAlreadyValid) { + return } - return isAssignable(methodReturnType, expectedReturnType) - } - } - object Util { - fun checkParameters( - parameterList: PsiParameterList, - expected: MethodSignature, - allowCoerce: Boolean, - ): Boolean { - val parameters = parameterList.parameters - val parametersWithoutSugar = parameters.dropLastWhile { it.isMixinExtrasSugar } + val suggestedSignature = handler.suggestedMethodSignature(annotation, targetMethods)?.let(::lazyOf) - return expected.matchParams(parametersWithoutSugar, allowCoerce) + if (suggestedSignature == null) { + holder.registerProblem( + parameters, + "There are no possible signatures for this injector", + ) + } else { + val annotationName = annotation.nameReferenceElement?.referenceName + val description = + "Method signature does not match expected signature for $annotationName" + val quickFix = SignatureQuickFix(method, suggestedSignature) + val declarationStart = (method.returnTypeElement ?: identifier).startOffsetInParent + val declarationEnd = method.parameterList.textRangeInParent.endOffset + holder.registerProblem( + method, + description, + ProblemHighlightType.GENERIC_ERROR_OR_WARNING, + TextRange.create(declarationStart, declarationEnd), + quickFix + ) + } } } private class SignatureQuickFix( method: PsiMethod, - @SafeFieldForPreview - private val expectedParams: List?, - @SafeFieldForPreview - private val expectedReturnType: PsiType?, - private val intLikeTypePositions: List + suggestedSignature: Lazy, ) : LocalQuickFixAndIntentionActionOnPsiElement(method) { + @delegate:SafeFieldForPreview + private val suggestedSignature by suggestedSignature private val fixName = "Fix method signature" @@ -307,7 +236,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 { @@ -316,14 +246,12 @@ 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 - } // We want to preserve captured locals val locals = parameters.parameters.dropWhile { val fqname = (it.type as? PsiClassType)?.fullQualifiedName ?: return@dropWhile true @@ -338,13 +266,19 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() { val nameHelper = PsiNameHelper.getInstance(project) val languageLevel = PsiUtil.getLanguageLevel(parameters) - val newParams = expectedParams.mapIndexedTo(mutableListOf()) { i, p -> + val usedNames = mutableSetOf() + val newParams = suggestedSignature.params.mapIndexedTo(mutableListOf()) { i, p -> 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) + ?: 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) @@ -359,10 +293,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, suggestedSignature.returnType, false) if (preview) { fix.generatePreview(file.project, editor, file) } else { @@ -370,38 +301,53 @@ 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 = suggestedSignature.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); + { returnTypeElement.addBefore(annotation, returnTypeElement.firstChild) } + } + + else -> return + } + + if (preview) { + fixCoerce() + } else { + runWriteAction(fixCoerce) + } + } + + private fun fixIntLikeTypes(project: Project, method: PsiMethod, editor: Editor, preview: Boolean) { + if (preview || suggestedSignature.intLikeTypes.isEmpty()) { return } - val runnable = { - val template = makeIntLikeTypeTemplate(method, intLikeTypePositions) + runWriteAction { + PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(editor.document) + + val template = makeIntLikeTypeTemplate(method, suggestedSignature.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 + positionSet: Set ): Template? { + val positions = positionSet.sorted() val builder = TemplateBuilderImpl(method) builder.replaceElement( positions.first().getElement(method) ?: return null, diff --git a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt index a5f4c638b..a3edf1703 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt @@ -22,11 +22,12 @@ 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.platform.mixin.util.isMixinExtrasSugar import com.demonwav.mcdev.util.Parameter +import com.demonwav.mcdev.util.countIsLessThan import com.demonwav.mcdev.util.normalize import com.intellij.psi.PsiMethod import com.intellij.psi.PsiParameter -import com.intellij.psi.PsiPrimitiveType import com.intellij.psi.PsiType import com.intellij.psi.PsiTypeElement import com.intellij.psi.PsiTypes @@ -34,28 +35,69 @@ import com.intellij.psi.PsiTypes data class MethodSignature( val requiredParams: List, val returnType: PsiType, + val allowCoerceRequired: Boolean, val trailingParams: List = emptyList(), + val allowCoerceTrailing: Boolean = true, val trailingByDefault: Boolean = false, - val intLikeTypes: List = emptyList() + val intLikeTypes: Set = emptySet() ) { - fun matchParams(parameters: List, allowCoerce: Boolean): Boolean { - return parameters.size in requiredParams.size..requiredParams.size + trailingParams.size - && matchParams(requiredParams, parameters, allowCoerce) - && matchParams(trailingParams, parameters.subList(requiredParams.size, parameters.size), allowCoerce) + fun matches(method: PsiMethod): Boolean { + val returnType = method.returnType ?: return false + val parameters = method.parameterList.parameters.dropLastWhile { it.isMixinExtrasSugar } + + return intLikeTypes.asSequence().map { it.getElement(method)?.type }.distinct().countIsLessThan(2) + && matchReturnType(returnType, method.hasAnnotation(COERCE)) + && parameters.size in requiredParams.size..requiredParams.size + trailingParams.size + && matchParams(requiredParams, parameters, allowCoerceRequired, 0) + && matchParams(trailingParams, parameters, allowCoerceTrailing, requiredParams.size) } - sealed interface TypePosition { + sealed interface TypePosition : Comparable { fun getElement(method: PsiMethod): PsiTypeElement? data object Return : TypePosition { override fun getElement(method: PsiMethod) = method.returnTypeElement + + override fun compareTo(other: TypePosition): Int = if (other is Return) 0 else -1 } data class Param(val index: Int) : TypePosition { override fun getElement(method: PsiMethod) = method.parameterList.parameters[index].typeElement + + override fun compareTo(other: TypePosition): Int = if (other is Param) index.compareTo(other.index) else 1 } } + private fun matchReturnType(returnType: PsiType, hasCoerce: Boolean): Boolean = + matchType(returnType, this.returnType, allowCoerceRequired && hasCoerce, TypePosition.Return) + + private fun matchParams( + expectedParams: List, + actualParams: List, + allowCoerce: Boolean, + startIndex: Int, + ): Boolean { + return expectedParams.asSequence() + .zip(actualParams.asSequence().withIndex().drop(startIndex)) + .all { (expected, indexAndActual) -> + val (index, actual) = indexAndActual + matchType( + actual.type, + expected.type, + allowCoerce && actual.hasAnnotation(COERCE), + TypePosition.Param(index), + ) + } + } + + private fun matchType(actual: PsiType, expected: PsiType, coerce: Boolean, typePos: TypePosition): Boolean = when { + typePos in intLikeTypes -> actual in INT_TYPES + actual.normalize() == expected.normalize() -> true + !coerce -> false + expected in INT_TYPES -> actual == PsiTypes.intType() + else -> isAssignable(actual.normalize(), expected.normalize()) + } + companion object { private val INT_TYPES = setOf( PsiTypes.intType(), @@ -64,34 +106,5 @@ data class MethodSignature( PsiTypes.byteType(), PsiTypes.booleanType() ) - - private fun matchParams( - expectedParams: List, - actualParams: List, - allowCoerce: Boolean - ): Boolean { - return expectedParams.asSequence() - .zip(actualParams.asSequence()) - .all { (expected, actual) -> matchParam(expected.type, actual, allowCoerce) } - } - - private fun matchParam(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/MixinParameterNameInspection.kt b/src/main/kotlin/platform/mixin/inspection/injector/MixinParameterNameInspection.kt index 3913e2216..36b1c60cf 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/MixinParameterNameInspection.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/MixinParameterNameInspection.kt @@ -20,20 +20,14 @@ package com.demonwav.mcdev.platform.mixin.inspection.injector -import com.demonwav.mcdev.platform.mixin.handlers.InjectorAnnotationHandler -import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler +import com.demonwav.mcdev.platform.mixin.handlers.InsnInjectorAnnotationHandler import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.CollectVisitor import com.demonwav.mcdev.platform.mixin.inspection.MixinInspection import com.demonwav.mcdev.platform.mixin.util.LocalInfo import com.demonwav.mcdev.platform.mixin.util.MethodTargetMember 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.intellij.codeInsight.intention.LowPriorityAction -import com.intellij.codeInspection.LocalQuickFix import com.intellij.codeInspection.ProblemDescriptor import com.intellij.codeInspection.ProblemsHolder import com.intellij.codeInspection.options.OptPane @@ -45,7 +39,6 @@ import com.intellij.psi.JavaElementVisitor import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiMethod import com.intellij.psi.PsiParameter -import com.siyeh.ig.fixes.RenameFix class MixinParameterNameInspection : MixinInspection() { @JvmField @@ -62,128 +55,127 @@ class MixinParameterNameInspection : MixinInspection() { override fun buildVisitor(holder: ProblemsHolder) = object : JavaElementVisitor() { override fun visitMethod(method: PsiMethod) { - val module = method.findModule() ?: return - val parameters = method.parameterList.parameters - val parametersWithoutSugar = parameters.dropLastWhile { it.isMixinExtrasSugar } - - val validNames = arrayOfNulls>(parameters.size) - - for (annotation in method.annotations) { - val handler = MixinAnnotationHandler.forMixinAnnotation(annotation, holder.project) - as? InjectorAnnotationHandler ?: continue - for (target in MixinAnnotationHandler.resolveTarget(annotation)) { - if (!collectValidNames(validNames, module, method, parameters, parametersWithoutSugar, annotation, handler, target)) { - return - } - } - } - - for (index in validNames.indices) { - val names = validNames[index] ?: continue - if (names.isEmpty()) { - continue - } - - if (parameters[index].name !in names) { - val fixes = mutableListOf() - - for (name in names) { - fixes += RenameFix(name, false, false) - } - - fixes += if (index < parametersWithoutSugar.size) { - DontReportForMainSignatureFix() - } else { - DontReportForLocalFix() - } - - holder.registerProblem( - parameters[index].nameIdentifier ?: parameters[index], - "Parameter name does not match name ${names.joinToString(" / ") { "'$it'" } } in target class", - *fixes.toTypedArray() - ) - } - } +// val module = method.findModule() ?: return +// val parameters = method.parameterList.parameters +// val parametersWithoutSugar = parameters.dropLastWhile { it.isMixinExtrasSugar } +// +// val validNames = arrayOfNulls>(parameters.size) +// +// for (annotation in method.annotations) { +// val handler = MixinAnnotationHandler.forMixinAnnotation(annotation, holder.project) +// as? InjectorAnnotationHandler ?: continue +// for (target in MixinAnnotationHandler.resolveTarget(annotation)) { +// if (!collectValidNames(validNames, module, method, parameters, parametersWithoutSugar, annotation, handler, target)) { +// return +// } +// } +// } +// +// for (index in validNames.indices) { +// val names = validNames[index] ?: continue +// if (names.isEmpty()) { +// continue +// } +// +// if (parameters[index].name !in names) { +// val fixes = mutableListOf() +// +// for (name in names) { +// fixes += RenameFix(name, false, false) +// } +// +// fixes += if (index < parametersWithoutSugar.size) { +// DontReportForMainSignatureFix() +// } else { +// DontReportForLocalFix() +// } +// +// holder.registerProblem( +// parameters[index].nameIdentifier ?: parameters[index], +// "Parameter name does not match name ${names.joinToString(" / ") { "'$it'" } } in target class", +// *fixes.toTypedArray() +// ) +// } +// } } } - private fun collectValidNames( - validNames: Array?>, - module: Module, method: PsiMethod, - parameters: Array, - parametersWithoutSugar: List, - annotation: PsiAnnotation, - handler: InjectorAnnotationHandler, - target: MixinTargetMember - ): Boolean { - if (target !is MethodTargetMember) { - return true - } - - if (!method.hasNamedLocalVariables(target.classAndMethod.clazz.name.replace('/', '.'))) { - 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 (expectedSignature in expectedSignatures) { - if (!InvalidInjectorMethodSignatureInspection.Util.checkParameters( - method.parameterList, - expectedSignature, - handler.allowCoerce - ) - ) { - continue - } - - anyValidSignatures = true - - checkExpectedSignatureForKnownNames( - validNamesForThisTarget, - expectedSignature.requiredParams + expectedSignature.trailingParams, - parametersWithoutSugar - ) - } - - if (!anyValidSignatures) { - return false - } - } - - if (reportForLocal) { - for (pos in parametersWithoutSugar.size until parameters.size) { - if (!checkSugarForKnownNames( - validNamesForThisTarget, - module, - handler, - annotation, - target, - parameters[pos], - pos - ) - ) { - return false - } - } - } - - 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 collectValidNames( +// validNames: Array?>, +// module: Module, method: PsiMethod, +// parameters: Array, +// parametersWithoutSugar: List, +// annotation: PsiAnnotation, +// handler: InjectorAnnotationHandler, +// target: MixinTargetMember +// ): Boolean { +// if (target !is MethodTargetMember) { +// return true +// } +// +// if (!method.hasNamedLocalVariables(target.classAndMethod.clazz.name.replace('/', '.'))) { +// 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 (expectedSignature in expectedSignatures) { +// if (!InvalidInjectorMethodSignatureInspection.Util.checkParameters( +// method.parameterList, +// expectedSignature +// ) +// ) { +// continue +// } +// +// anyValidSignatures = true +// +// checkExpectedSignatureForKnownNames( +// validNamesForThisTarget, +// expectedSignature.requiredParams + expectedSignature.trailingParams, +// parametersWithoutSugar +// ) +// } +// +// if (!anyValidSignatures) { +// return false +// } +// } +// +// if (reportForLocal) { +// for (pos in parametersWithoutSugar.size until parameters.size) { +// if (!checkSugarForKnownNames( +// validNamesForThisTarget, +// module, +// handler, +// annotation, +// target, +// parameters[pos], +// pos +// ) +// ) { +// return false +// } +// } +// } +// +// 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?>, @@ -209,7 +201,7 @@ class MixinParameterNameInspection : MixinInspection() { private fun checkSugarForKnownNames( validNamesForThisTarget: Array?>, module: Module, - handler: InjectorAnnotationHandler, + handler: InsnInjectorAnnotationHandler, annotation: PsiAnnotation, target: MethodTargetMember, parameter: PsiParameter, 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/SuggestedSignature.kt b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt new file mode 100644 index 000000000..8102b7cbc --- /dev/null +++ b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt @@ -0,0 +1,252 @@ +/* + * 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.handlers.InjectorAnnotationHandler +import com.demonwav.mcdev.platform.mixin.util.ClassAndMethodNode +import com.demonwav.mcdev.util.normalize +import com.intellij.psi.GenericsUtil +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiManager +import com.intellij.psi.PsiType +import com.intellij.psi.PsiTypes + +data class SuggestedSignature( + val params: List, + val returnType: PsiType, + val intLikeTypes: Set = emptySet(), + val coerceReturnType: Boolean = false, +) { + data class Param(val name: String?, val type: PsiType, val coerce: Boolean = false) + + fun intersectCoerce(other: SuggestedSignature, manager: PsiManager): SuggestedSignature? { + if (roughShapeOf(this) != roughShapeOf(other)) { + return null + } + val intLikeForcings = mutableSetOf() + for (pos in this.intLikeTypes) { + if (pos !in other.intLikeTypes) { + intLikeForcings.add(other.getType(pos)) + } + } + for (pos in other.intLikeTypes) { + if (pos !in this.intLikeTypes) { + intLikeForcings.add(this.getType(pos)) + } + } + val intLikeAssignment = if (intLikeForcings.size > 1) PsiTypes.intType() else intLikeForcings.singleOrNull() + + val intLikeTypes = mutableSetOf() + + val (returnType, returnTypeIsIntLike, coerceReturnType) = getSupertype( + manager, + this.returnType, + other.returnType, + MethodSignature.TypePosition.Return in this.intLikeTypes, + MethodSignature.TypePosition.Return in other.intLikeTypes, + intLikeAssignment, + ) + 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) = getSupertype( + manager, + a.type, + b.type, + pos in this.intLikeTypes, + pos in other.intLikeTypes, + intLikeAssignment, + ) + if (isIntLike) { + intLikeTypes.add(pos) + } + val name = if (a.name == b.name) a.name else null + Param(name, type, a.coerce || b.coerce || coerce) + }, + returnType, + intLikeTypes, + this.coerceReturnType || other.coerceReturnType || coerceReturnType, + ) + } + + private fun getType(pos: MethodSignature.TypePosition) = when (pos) { + is MethodSignature.TypePosition.Param -> params[pos.index].type + MethodSignature.TypePosition.Return -> returnType + } + + companion object { + fun exact(signature: MethodSignature): SuggestedSignature { + return SuggestedSignature( + signature.requiredParams.map { Param(it.name, it.type) }, + signature.returnType, + signature.intLikeTypes, + ) + } + + fun modifierNoCoerce( + annotation: PsiAnnotation, + targets: List, + handler: InjectorAnnotationHandler, + ): SuggestedSignature? { + val parameterOptions = handler.expectedMethodSignatures(annotation, targets).map { signatures -> + signatures.mapNotNull { + require(it.intLikeTypes.isEmpty()) + it.requiredParams.singleOrNull() + }.ifEmpty { return null } + } + val psiManager = PsiManager.getInstance(annotation.project) + + val optionsByType = parameterOptions.asSequence().flatten().groupBy { it.type.normalize() } + val chosenParams = optionsByType.values.firstOrNull { it.size == parameterOptions.size } ?: return null + + val name = chosenParams.asSequence().map { it.name }.distinct().singleOrNull() ?: "original" + val type = chosenParams.asSequence().map { it.type } + .reduce { a, b -> GenericsUtil.getLeastUpperBound(a, b, psiManager) ?: a } + + return SuggestedSignature(listOf(Param(name, type)), type) + } + + fun operationWrapper( + annotation: PsiAnnotation, + targets: List, + handler: InjectorAnnotationHandler, + ): SuggestedSignature? { + val manager = PsiManager.getInstance(annotation.project) + return handler.expectedMethodSignatures(annotation, targets).map { + exact(it.single()) + }.reduceOrNull { acc, it -> acc?.intersectCoerce(it, manager) } + } + } +} + +private fun roughShapeOf(signature: SuggestedSignature): Shape { + return Shape(signature.params.map { TypeKind.of(it.type) }, TypeKind.of(signature.returnType)) +} + +private fun getSupertype( + manager: PsiManager, + a: PsiType, + b: PsiType, + aIsIntLike: Boolean, + bIsIntLike: Boolean, + intLikeAssignment: PsiType?, +): 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 -> TypeMergeResult( + intLikeAssignment ?: b, + isIntLike = intLikeAssignment == null && bIsIntLike, + coerce = intLikeAssignment != null && intLikeAssignment != b, + ) + bIsIntLike -> TypeMergeResult(a, isIntLike = false, coerce = false) + a == b -> TypeMergeResult(a, isIntLike = false, coerce = false) + else -> TypeMergeResult(PsiTypes.intType(), isIntLike = false, coerce = true) + } + } + + else -> { + require(a == b) + TypeMergeResult(a, isIntLike = false, coerce = false) + } +} + +private data class TypeMergeResult(val type: PsiType, val isIntLike: Boolean, val coerce: Boolean) + +private data class Shape(val params: List, val returnType: T) + +private 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 + } +} + +//private class Shape(private val init: Shape?, private val last: T) { +// private val hashCode = init.hashCode() * 31 + last.hashCode() +// +// override fun equals(other: Any?): Boolean { +// if (other !is Shape<*>) { +// return false +// } +// @Suppress("UNCHECKED_CAST") +// other as Shape +// +// var a: Shape? = this +// var b: Shape? = other +// while (a != null && b != null) { +// if (a === b) { +// return true +// } +// if (a.hashCode != b.hashCode || a.last != b.last) { +// return false +// } +// a = a.init +// b = b.init +// } +// return a == null && b == null +// } +// +// override fun hashCode() = hashCode +// +// override fun toString() = buildString { +// append('[') +// this@Shape.append(this@buildString) +// append(']') +// } +// +// private fun append(builder: StringBuilder) { +// init?.let { +// it.append(builder) +// builder.append(", ") +// } +// builder.append(last) +// } +//} 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/LocalInfo.kt b/src/main/kotlin/platform/mixin/util/LocalInfo.kt index 0b0d9565e..1414345f8 100644 --- a/src/main/kotlin/platform/mixin/util/LocalInfo.kt +++ b/src/main/kotlin/platform/mixin/util/LocalInfo.kt @@ -132,7 +132,7 @@ class LocalInfo( } // implicit mode - if (mode == CollectVisitor.Mode.COMPLETION) { + if (!mode.assumeCorrectSignature) { return locals.asSequence() .filterNotNull() .filter { local -> locals.count { it?.desc == local.desc } == 1 } diff --git a/src/main/kotlin/util/utils.kt b/src/main/kotlin/util/utils.kt index dfa6d9087..9d86cfdee 100644 --- a/src/main/kotlin/util/utils.kt +++ b/src/main/kotlin/util/utils.kt @@ -153,9 +153,9 @@ 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 { if (isNullOrEmpty()) { - func() + return func() } return this } From ef6df015bfcf51454b7b5743887a233c7e5c2927 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Sat, 19 Sep 2026 18:36:15 +0100 Subject: [PATCH 05/51] stash --- .../mixin/handlers/InjectAnnotationHandler.kt | 55 ++--- .../handlers/InjectorAnnotationHandler.kt | 6 +- .../handlers/InsnInjectorAnnotationHandler.kt | 8 +- .../mixin/handlers/ModifyArgHandler.kt | 55 +++-- .../mixin/handlers/ModifyArgsHandler.kt | 28 +-- .../mixin/handlers/ModifyConstantHandler.kt | 43 ++-- .../mixin/handlers/ModifyVariableHandler.kt | 33 +-- .../mixin/handlers/RedirectInjectorHandler.kt | 115 ++++++---- .../MixinExtrasInjectorAnnotationHandler.kt | 40 +--- .../ModifyExpressionValueHandler.kt | 25 ++- .../mixinextras/ModifyReceiverHandler.kt | 24 ++- .../mixinextras/ModifyReturnValueHandler.kt | 23 +- .../handlers/mixinextras/WrapMethodHandler.kt | 27 ++- .../mixinextras/WrapOperationHandler.kt | 35 ++-- .../mixinextras/WrapWithConditionHandler.kt | 24 ++- ...nvalidInjectorMethodSignatureInspection.kt | 8 +- .../inspection/injector/MethodSignature.kt | 4 +- .../inspection/injector/MethodSignatures.kt | 197 ++++++++++++++++++ .../inspection/injector/SuggestedSignature.kt | 93 ++++----- .../kotlin/platform/mixin/util/TypeKind.kt | 49 +++++ .../ExpectedSignatures.kt => util/lists.kt} | 21 +- src/main/kotlin/util/sequences.kt | 2 + 22 files changed, 636 insertions(+), 279 deletions(-) create mode 100644 src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt create mode 100644 src/main/kotlin/platform/mixin/util/TypeKind.kt rename src/main/kotlin/{platform/mixin/inspection/injector/ExpectedSignatures.kt => util/lists.kt} (62%) diff --git a/src/main/kotlin/platform/mixin/handlers/InjectAnnotationHandler.kt b/src/main/kotlin/platform/mixin/handlers/InjectAnnotationHandler.kt index c712859e3..d7dcd5931 100644 --- a/src/main/kotlin/platform/mixin/handlers/InjectAnnotationHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/InjectAnnotationHandler.kt @@ -21,10 +21,12 @@ package com.demonwav.mcdev.platform.mixin.handlers import com.demonwav.mcdev.platform.mixin.handlers.mixinextras.TargetInsn -import com.demonwav.mcdev.platform.mixin.inspection.injector.MethodSignature +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.knownSignatures +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 @@ -35,7 +37,6 @@ 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 @@ -49,20 +50,9 @@ class InjectAnnotationHandler : InsnInjectorAnnotationHandler() { targetClass: ClassNode, targetMethod: MethodNode, targetInsn: TargetInsn, - ): List { + ): ExpectedSignatures { val returnType = targetMethod.getGenericReturnType(targetClass, annotation.project) - val result = mutableListOf() - - val ciParam = 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) @@ -91,27 +81,24 @@ class InjectAnnotationHandler : InsnInjectorAnnotationHandler() { } } - // Long form - result.add( - MethodSignature( - targetParams + ciParam, - PsiTypes.voidType(), - trailingParams = capturedLocals, - allowCoerceRequired = true, - trailingByDefault = true, - ) + return ExpectedSignatures.Valid( + InjectSignatures( + annotation, + targetParams, + returnType, + capturedLocals, + ) ?: return ExpectedSignatures.Invalid ) + } - // Short form - result.add( - MethodSignature( - listOf(ciParam), - PsiTypes.voidType(), - allowCoerceRequired = true, - ) + override fun suggestedMethodSignature( + annotation: PsiAnnotation, + targets: List + ): SuggestedSignature? { + return SuggestedSignature.inject( + annotation, + expectedMethodSignatures(annotation, targets).knownSignatures() ?: return null, ) - - return result } override fun canAlwaysBeStatic(method: PsiMethod): Boolean { diff --git a/src/main/kotlin/platform/mixin/handlers/InjectorAnnotationHandler.kt b/src/main/kotlin/platform/mixin/handlers/InjectorAnnotationHandler.kt index d6cd07b37..485ac18d3 100644 --- a/src/main/kotlin/platform/mixin/handlers/InjectorAnnotationHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/InjectorAnnotationHandler.kt @@ -24,7 +24,7 @@ import com.demonwav.mcdev.asset.MixinAssets import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.AtResolver 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.MethodSignature +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 @@ -107,7 +107,7 @@ abstract class InjectorAnnotationHandler : MixinAnnotationHandler { abstract fun expectedMethodSignatures( annotation: PsiAnnotation, targets: List, - ): List> + ): List> open fun suggestedMethodSignature( annotation: PsiAnnotation, @@ -171,7 +171,7 @@ object DefaultInjectorAnnotationHandler : InsnInjectorAnnotationHandler() { targetClass: ClassNode, targetMethod: MethodNode, targetInsn: TargetInsn, - ) = null + ): ExpectedSignatures = ExpectedSignatures.Unknown override val isSoft = true diff --git a/src/main/kotlin/platform/mixin/handlers/InsnInjectorAnnotationHandler.kt b/src/main/kotlin/platform/mixin/handlers/InsnInjectorAnnotationHandler.kt index 0c33bb0cd..06d1c9809 100644 --- a/src/main/kotlin/platform/mixin/handlers/InsnInjectorAnnotationHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/InsnInjectorAnnotationHandler.kt @@ -24,7 +24,7 @@ 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.MethodSignature +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 @@ -107,8 +107,8 @@ abstract class InsnInjectorAnnotationHandler : InjectorAnnotationHandler() { final override fun expectedMethodSignatures( annotation: PsiAnnotation, targets: List, - ): List> { - return resolveInstructions(annotation, targets).mapNotNull { (target, result) -> + ): List> { + return resolveInstructions(annotation, targets).map { (target, result) -> val (targetClass, targetMethod) = target val targetInsn = TargetInsn(result.insn, result.decorations) expectedMethodSignature(annotation, targetClass, targetMethod, targetInsn) @@ -125,7 +125,7 @@ abstract class InsnInjectorAnnotationHandler : InjectorAnnotationHandler() { targetClass: ClassNode, targetMethod: MethodNode, targetInsn: TargetInsn, - ): List? + ): ExpectedSignatures<*> open fun isInsnAllowed(insn: AbstractInsnNode, decorations: Map): Boolean { return true diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt index e9e8821d1..efc8258e2 100644 --- a/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt @@ -21,8 +21,10 @@ package com.demonwav.mcdev.platform.mixin.handlers import com.demonwav.mcdev.platform.mixin.handlers.mixinextras.TargetInsn -import com.demonwav.mcdev.platform.mixin.inspection.injector.MethodSignature +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.knownSignatures import com.demonwav.mcdev.platform.mixin.util.ClassAndMethodNode import com.demonwav.mcdev.platform.mixin.util.getBytecodeParameter import com.demonwav.mcdev.platform.mixin.util.toPsiType @@ -50,10 +52,10 @@ class ModifyArgHandler : InsnInjectorAnnotationHandler() { targetClass: ClassNode, targetMethod: MethodNode, targetInsn: TargetInsn, - ): List? { + ): ExpectedSignatures { val insn = targetInsn.insn if (insn !is MethodInsnNode) { - return null + return ExpectedSignatures.Invalid } val project = annotation.project val index = annotation.findDeclaredAttributeValue("index")?.constantValue as? Int @@ -67,7 +69,7 @@ class ModifyArgHandler : InsnInjectorAnnotationHandler() { } if (validTypes.isEmpty()) { - return emptyList() + return ExpectedSignatures.Invalid } // get the source method for parameter names @@ -77,40 +79,37 @@ class ModifyArgHandler : InsnInjectorAnnotationHandler() { insn.owner.replace('/', '.') ).resolveMember(project) as PsiMethod? val elementFactory = JavaPsiFacade.getElementFactory(annotation.project) - return validTypes.flatMap { type -> - val psiParams = argTypes.indices.map { index -> sourceMethod?.getBytecodeParameter(index) } + val psiParams = argTypes.indices.map { index -> sourceMethod?.getBytecodeParameter(index) } + val paramOptions = validTypes.map { type -> val targetParam = psiParams[index ?: argTypes.indexOf(type)] val psiType = targetParam?.type ?: type.toPsiType(elementFactory) - val singleSignature = MethodSignature( - listOf( - sanitizedParameter(psiType, targetParam?.name), - ), - sanitizedReturnType(psiType), - allowCoerceRequired = false, - ) - if (argTypes.size > 1) { - val fullParams = - psiParams.zip(argTypes) { param, argType -> - sanitizedParameter( - param?.type ?: argType.toPsiType(elementFactory), - param?.name, - ) - } - listOf( - singleSignature, - MethodSignature(fullParams, sanitizedReturnType(psiType), allowCoerceRequired = false), + 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( + paramOptions, + allowCoerce = false, + fullParams, + ) + ) } override fun suggestedMethodSignature( annotation: PsiAnnotation, targets: List ): SuggestedSignature? { - return SuggestedSignature.modifierNoCoerce(annotation, targets, this) + return SuggestedSignature.modifierNoCoerce( + annotation, + expectedMethodSignatures(annotation, targets).knownSignatures() ?: 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 90e81d28d..9b6e0684d 100644 --- a/src/main/kotlin/platform/mixin/handlers/ModifyArgsHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/ModifyArgsHandler.kt @@ -21,6 +21,8 @@ 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.SuggestedSignature import com.demonwav.mcdev.platform.mixin.util.ClassAndMethodNode @@ -47,21 +49,23 @@ class ModifyArgsHandler : InsnInjectorAnnotationHandler() { targetClass: ClassNode, targetMethod: MethodNode, targetInsn: TargetInsn, - ): List { + ): ExpectedSignatures<*> { val argsType = JavaPsiFacade.getElementFactory(annotation.project) .createTypeByFQClassName(ARGS, annotation.resolveScope) val shortParams = listOf(Parameter("args", argsType)) - return listOf( - MethodSignature( - shortParams, - PsiTypes.voidType(), - allowCoerceRequired = false, - ), - MethodSignature( - shortParams + collectTargetMethodParameters(annotation.project, targetClass, targetMethod), - PsiTypes.voidType(), - allowCoerceRequired = false, - ), + return ExpectedSignatures.Valid( + BasicSignatures( + MethodSignature( + shortParams, + PsiTypes.voidType(), + allowCoerceRequired = false, + ), + MethodSignature( + shortParams + collectTargetMethodParameters(annotation.project, targetClass, targetMethod), + PsiTypes.voidType(), + allowCoerceRequired = false, + ), + ) ) } diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt index 31050df2a..3b7f26fc4 100644 --- a/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt @@ -23,9 +23,15 @@ 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.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.MethodSignatures +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.knownSignatures import com.demonwav.mcdev.platform.mixin.util.ClassAndMethodNode +import com.demonwav.mcdev.util.Parameter import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiElement @@ -75,10 +81,10 @@ class ModifyConstantHandler : InsnInjectorAnnotationHandler() { targetClass: ClassNode, targetMethod: MethodNode, targetInsn: TargetInsn, - ): List? { + ): ExpectedSignatures<*> { val targetParams = collectTargetMethodParameters(annotation.project, targetClass, targetMethod) - val cst = constantInjectionPoint.getTargetedConstant(targetInsn.insn) ?: return emptyList() - return basicMethodSignatures(annotation, cst)?.map { it.copy(trailingParams = targetParams) } + val cst = constantInjectionPoint.getTargetedConstant(targetInsn.insn) ?: return ExpectedSignatures.Invalid + return ExpectedSignatures.Valid(expectedSignatures(annotation, cst, targetParams)) } override fun suggestedMethodSignature( @@ -97,35 +103,40 @@ class ModifyConstantHandler : InsnInjectorAnnotationHandler() { PsiManager.getInstance(annotation.project), annotation, PsiTypes.booleanType(), + emptyList(), ) ) } else { - SuggestedSignature.modifierNoCoerce(annotation, targets, this) + SuggestedSignature.modifierNoCoerce( + annotation, + expectedMethodSignatures(annotation, targets).knownSignatures() ?: return null, + ) } } - private fun basicMethodSignatures(annotation: PsiAnnotation, cst: Any): List? { + private fun expectedSignatures(annotation: PsiAnnotation, cst: Any, trailingParams: List): MethodSignatures { val psiManager = PsiManager.getInstance(annotation.project) return if (cst is Type) { - listOf( + BasicSignatures( makeTypeCheckMethodSignature( psiManager, annotation, PsiTypes.booleanType(), + trailingParams, ), makeTypeCheckMethodSignature( psiManager, annotation, getClassType(psiManager, annotation), + trailingParams, ), ) } else { - listOf( - makeMethodSignature( - getConstantType(annotation, cst) - ?: throw IllegalStateException("Unknown constant type: ${cst.javaClass.name}") - ), + makeSignatures( + getConstantType(annotation, cst) + ?: throw IllegalStateException("Unknown constant type: ${cst.javaClass.name}"), + trailingParams, ) } } @@ -143,11 +154,11 @@ class ModifyConstantHandler : InsnInjectorAnnotationHandler() { else -> null } - private fun makeMethodSignature(type: PsiType): MethodSignature { - return MethodSignature( + private fun makeSignatures(type: PsiType, trailingParams: List): ModifierSignatures { + return ModifierSignatures( listOf(sanitizedParameter(type, "constant")), - type, - allowCoerceRequired = true, + allowCoerce = true, + trailingParams = trailingParams, ) } @@ -155,6 +166,7 @@ class ModifyConstantHandler : InsnInjectorAnnotationHandler() { psiManager: PsiManager, context: PsiElement, returnType: PsiType, + trailingParams: List, ): MethodSignature { return MethodSignature( listOf( @@ -163,6 +175,7 @@ class ModifyConstantHandler : InsnInjectorAnnotationHandler() { ), returnType, allowCoerceRequired = false, + trailingParams = trailingParams, ) } diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt index 9f8873b2c..78528a81c 100644 --- a/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt @@ -22,11 +22,14 @@ package com.demonwav.mcdev.platform.mixin.handlers import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.CollectVisitor import com.demonwav.mcdev.platform.mixin.handlers.mixinextras.TargetInsn -import com.demonwav.mcdev.platform.mixin.inspection.injector.MethodSignature +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.knownSignatures 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.Parameter import com.demonwav.mcdev.util.findContainingMethod import com.demonwav.mcdev.util.findModule import com.intellij.psi.JavaPsiFacade @@ -42,17 +45,17 @@ class ModifyVariableHandler : InsnInjectorAnnotationHandler() { targetClass: ClassNode, targetMethod: MethodNode, targetInsn: TargetInsn, - ): List? { - val module = annotation.findModule() ?: return null + ): ExpectedSignatures { + val module = annotation.findModule() ?: return ExpectedSignatures.Unknown val targetParams = collectTargetMethodParameters(annotation.project, targetClass, targetMethod) - 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() + val result = mutableListOf() val matchedLocals = info.matchLocals( module, targetClass, targetMethod, targetInsn.insn, CollectVisitor.Mode.SUGGESTION, matchType = false @@ -60,23 +63,27 @@ class ModifyVariableHandler : InsnInjectorAnnotationHandler() { for (local in matchedLocals) { if (seenParams.add(local.desc + local.name)) { val localType = Type.getType(local.desc).toPsiType(elementFactory) - result += MethodSignature( - listOf(sanitizedParameter(localType, local.name, local.isNamed)), - localType, - trailingParams = targetParams, - allowCoerceRequired = false, - ) + result += sanitizedParameter(localType, local.name, local.isNamed) } } - return result + return ExpectedSignatures.Valid( + ModifierSignatures( + result, + allowCoerce = false, + trailingParams = targetParams, + ) + ) } override fun suggestedMethodSignature( annotation: PsiAnnotation, targets: List ): SuggestedSignature? { - return SuggestedSignature.modifierNoCoerce(annotation, targets, this) + return SuggestedSignature.modifierNoCoerce( + annotation, + expectedMethodSignatures(annotation, targets).knownSignatures() ?: 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 e2214ca93..6c1552760 100644 --- a/src/main/kotlin/platform/mixin/handlers/RedirectInjectorHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/RedirectInjectorHandler.kt @@ -22,10 +22,15 @@ package com.demonwav.mcdev.platform.mixin.handlers import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.NewInsnInjectionPoint import com.demonwav.mcdev.platform.mixin.handlers.mixinextras.TargetInsn -import com.demonwav.mcdev.platform.mixin.inspection.injector.MethodSignature +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.knownSignatures 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 @@ -89,22 +94,26 @@ class RedirectInjectorHandler : InsnInjectorAnnotationHandler() { targetClass: ClassNode, targetMethod: MethodNode, targetInsn: TargetInsn, - ): List? { + ): ExpectedSignatures { val extraParams = collectTargetMethodParameters(annotation.project, targetClass, targetMethod) - val redirectType = getRedirectType(targetInsn.insn) ?: return null + val redirectType = getRedirectType(targetInsn.insn) ?: return ExpectedSignatures.Invalid return redirectType.expectedMethodSignature( annotation, targetClass, targetMethod, targetInsn.insn, - )?.map { (params, returnType) -> - MethodSignature( - params, - returnType, - trailingParams = extraParams, - allowCoerceRequired = redirectType.allowCoerce, - ) - } + extraParams, + )?.let(ExpectedSignatures<*>::Valid) ?: ExpectedSignatures.Invalid + } + + override fun suggestedMethodSignature( + annotation: PsiAnnotation, + targets: List + ): SuggestedSignature? { + return SuggestedSignature.general( + annotation, + expectedMethodSignatures(annotation, targets).knownSignatures() ?: return null + ) } override val mixinExtrasExpressionContextType = ExpressionContext.Type.REDIRECT @@ -119,7 +128,8 @@ class RedirectInjectorHandler : InsnInjectorAnnotationHandler() { targetClass: ClassNode, targetMethod: MethodNode, insn: AbstractInsnNode, - ): List, PsiType>>? + trailingParams: List, + ): GeneralSignatures? } private abstract class FieldAccess : RedirectType { @@ -146,7 +156,8 @@ class RedirectInjectorHandler : InsnInjectorAnnotationHandler() { targetClass: ClassNode, targetMethod: MethodNode, insn: AbstractInsnNode, - ): List, PsiType>> { + trailingParams: List, + ): GeneralSignatures { insn as FieldInsnNode val (elementFactory, fieldType) = determineFieldType(insn, annotation) @@ -156,7 +167,11 @@ class RedirectInjectorHandler : InsnInjectorAnnotationHandler() { parameters += Parameter("instance", Type.getObjectType(insn.owner).toPsiType(elementFactory)) } - return listOf(parameters to fieldType) + return GeneralSignatures( + parameters, + fieldType, + trailingParams, + ) } } @@ -166,7 +181,8 @@ class RedirectInjectorHandler : InsnInjectorAnnotationHandler() { targetClass: ClassNode, targetMethod: MethodNode, insn: AbstractInsnNode, - ): List, PsiType>> { + trailingParams: List, + ): GeneralSignatures { insn as FieldInsnNode val (elementFactory, fieldType) = determineFieldType(insn, annotation) @@ -177,7 +193,11 @@ class RedirectInjectorHandler : InsnInjectorAnnotationHandler() { } parameters += Parameter("value", fieldType) - return listOf(parameters to PsiTypes.voidType()) + return GeneralSignatures( + parameters, + PsiTypes.voidType(), + trailingParams, + ) } } @@ -191,7 +211,8 @@ class RedirectInjectorHandler : InsnInjectorAnnotationHandler() { targetClass: ClassNode, targetMethod: MethodNode, insn: AbstractInsnNode, - ): List, PsiType>> { + trailingParams: List, + ): GeneralSignatures { insn as MethodInsnNode val elementFactory = JavaPsiFacade.getElementFactory(annotation.project) @@ -229,7 +250,11 @@ class RedirectInjectorHandler : InsnInjectorAnnotationHandler() { } val returnType = signature?.first ?: Type.getReturnType(insn.desc).toPsiType(elementFactory) - return listOf(parameters to returnType) + return GeneralSignatures( + parameters, + returnType, + trailingParams, + ) } } @@ -239,7 +264,8 @@ class RedirectInjectorHandler : InsnInjectorAnnotationHandler() { targetClass: ClassNode, targetMethod: MethodNode, insn: AbstractInsnNode, - ): List, PsiType>>? { + trailingParams: List, + ): GeneralSignatures? { val arrayType = AsmDfaUtil.getStackType(annotation.project, targetClass, targetMethod, insn, 0) ?: return null if (arrayType.sort != Type.ARRAY) { @@ -248,8 +274,10 @@ class RedirectInjectorHandler : InsnInjectorAnnotationHandler() { val elementFactory = JavaPsiFacade.getElementFactory(annotation.project) - return listOf( - listOf(Parameter("array", arrayType.toPsiType(elementFactory))) to PsiTypes.intType(), + return GeneralSignatures( + listOf(Parameter("array", arrayType.toPsiType(elementFactory))), + PsiTypes.intType(), + trailingParams, ) } } @@ -260,7 +288,8 @@ class RedirectInjectorHandler : InsnInjectorAnnotationHandler() { targetClass: ClassNode, targetMethod: MethodNode, insn: AbstractInsnNode, - ): List, PsiType>>? { + trailingParams: List, + ): GeneralSignatures? { val arrayType = AsmDfaUtil.getStackType(annotation.project, targetClass, targetMethod, insn, 1) ?: return null if (arrayType.sort != Type.ARRAY) { @@ -270,11 +299,13 @@ class RedirectInjectorHandler : InsnInjectorAnnotationHandler() { val elementFactory = JavaPsiFacade.getElementFactory(annotation.project) val psiArrayType = arrayType.toPsiType(elementFactory) as PsiArrayType - return listOf( + return GeneralSignatures( listOf( Parameter("array", psiArrayType), Parameter("index", PsiTypes.intType()), - ) to psiArrayType.componentType, + ), + psiArrayType.componentType, + trailingParams, ) } } @@ -285,7 +316,8 @@ class RedirectInjectorHandler : InsnInjectorAnnotationHandler() { targetClass: ClassNode, targetMethod: MethodNode, insn: AbstractInsnNode, - ): List, PsiType>>? { + trailingParams: List, + ): GeneralSignatures? { val arrayType = AsmDfaUtil.getStackType(annotation.project, targetClass, targetMethod, insn, 2) ?: return null if (arrayType.sort != Type.ARRAY) { @@ -295,12 +327,14 @@ class RedirectInjectorHandler : InsnInjectorAnnotationHandler() { val elementFactory = JavaPsiFacade.getElementFactory(annotation.project) val psiArrayType = arrayType.toPsiType(elementFactory) as PsiArrayType - return listOf( + return GeneralSignatures( listOf( Parameter("array", psiArrayType), Parameter("index", PsiTypes.intType()), Parameter("value", psiArrayType.componentType), - ) to PsiTypes.voidType(), + ), + PsiTypes.voidType(), + trailingParams, ) } } @@ -315,7 +349,8 @@ class RedirectInjectorHandler : InsnInjectorAnnotationHandler() { targetClass: ClassNode, targetMethod: MethodNode, insn: AbstractInsnNode, - ): List, PsiType>> { + trailingParams: List, + ): GeneralSignatures? { insn as TypeInsnNode val elementFactory = JavaPsiFacade.getElementFactory(annotation.project) @@ -325,10 +360,14 @@ class RedirectInjectorHandler : InsnInjectorAnnotationHandler() { annotation, targetClass, targetMethod, - NewInsnInjectionPoint.Util.findInitCall(insn) ?: return emptyList() - ).map { (params, _) -> + NewInsnInjectionPoint.Util.findInitCall(insn) ?: return null, + trailingParams, + ).let { sig -> // drop the instance parameter, return the constructed type - params.drop(1) to constructedType + sig.copy( + params = sig.params.drop(1), + returnTypeOptions = linkedMapOf(TypeKind.OBJECT to constructedType), + ) } } } @@ -341,7 +380,8 @@ class RedirectInjectorHandler : InsnInjectorAnnotationHandler() { targetClass: ClassNode, targetMethod: MethodNode, insn: AbstractInsnNode, - ): List, PsiType>> { + trailingParams: List, + ): GeneralSignatures { val psiManager = PsiManager.getInstance(annotation.project) val elementFactory = JavaPsiFacade.getElementFactory(annotation.project) val objectType = PsiType.getJavaLangObject(psiManager, annotation.resolveScope) @@ -350,9 +390,14 @@ class RedirectInjectorHandler : InsnInjectorAnnotationHandler() { Parameter("instance", objectType), Parameter("type", classType), ) - return listOf( - parameters to PsiTypes.booleanType(), - parameters to classType, + return GeneralSignatures( + parameters, + linkedMapOf( + TypeKind.INT_LIKE to PsiTypes.booleanType(), + TypeKind.OBJECT to classType, + ), + allowCoerce = false, + trailingParams, ) } } diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/MixinExtrasInjectorAnnotationHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/MixinExtrasInjectorAnnotationHandler.kt index b08fe69d3..5ad4a3aec 100644 --- a/src/main/kotlin/platform/mixin/handlers/mixinextras/MixinExtrasInjectorAnnotationHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/MixinExtrasInjectorAnnotationHandler.kt @@ -22,7 +22,8 @@ package com.demonwav.mcdev.platform.mixin.handlers.mixinextras 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.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 @@ -103,47 +104,20 @@ abstract class MixinExtrasInjectorAnnotationHandler : InsnInjectorAnnotationHand targetClass: ClassNode, targetMethod: MethodNode, target: TargetInsn, - ): Pair, PsiType>? - - open fun intLikeTypePositions( - target: TargetInsn - ): Set = emptySet() + ): MethodSignatures? final override fun expectedMethodSignature( annotation: PsiAnnotation, targetClass: ClassNode, targetMethod: MethodNode, targetInsn: TargetInsn - ): List { - val signature = expectedMethodSignatureImpl(annotation, targetClass, targetMethod, targetInsn) ?: return emptyList() - val intLikeTypePositions = intLikeTypePositions(targetInsn) - return listOf( - makeSignature( - annotation, - targetClass, - targetMethod, - signature.first, - signature.second, - intLikeTypePositions, - ) + ): ExpectedSignatures { + return ExpectedSignatures.Valid( + expectedMethodSignatureImpl(annotation, targetClass, targetMethod, targetInsn) + ?: return ExpectedSignatures.Invalid ) } - private fun makeSignature( - annotation: PsiAnnotation, - targetClass: ClassNode, - targetMethod: MethodNode, - params: List, - returnType: PsiType, - intLikeTypePositions: Set - ) = MethodSignature( - params, - returnType, - trailingParams = collectTargetMethodParameters(annotation.project, targetClass, targetMethod), - allowCoerceRequired = true, - intLikeTypes = intLikeTypePositions, - ) - protected fun getInsnReturnType(insn: AbstractInsnNode): Type? { return when { insn is MethodInsnNode -> Type.getReturnType(insn.desc) diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyExpressionValueHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyExpressionValueHandler.kt index f760b729f..d1ee3f5ce 100644 --- a/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyExpressionValueHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyExpressionValueHandler.kt @@ -20,7 +20,11 @@ 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.SuggestedSignature +import com.demonwav.mcdev.platform.mixin.inspection.injector.knownSignatures +import com.demonwav.mcdev.platform.mixin.util.ClassAndMethodNode import com.demonwav.mcdev.platform.mixin.util.toPsiType import com.demonwav.mcdev.util.Parameter import com.intellij.psi.JavaPsiFacade @@ -60,12 +64,27 @@ class ModifyExpressionValueHandler : MixinExtrasInjectorAnnotationHandler() { targetClass: ClassNode, targetMethod: MethodNode, target: TargetInsn - ): Pair, PsiType>? { + ): GeneralSignatures? { val psiType = getReturnType(target, annotation) ?: return null - return 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): Set { + override fun suggestedMethodSignature( + annotation: PsiAnnotation, + targets: List + ): SuggestedSignature? { + return SuggestedSignature.general( + annotation, + expectedMethodSignatures(annotation, targets).knownSignatures() ?: return null + ) + } + + private fun intLikeTypePositions(target: TargetInsn): Set { val expressionType = target.getDecoration(ExpressionDecorations.SIMPLE_EXPRESSION_TYPE) if (expressionType == ExpressionASMUtils.INTLIKE_TYPE) { return setOf(MethodSignature.TypePosition.Return, MethodSignature.TypePosition.Param(0)) diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReceiverHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReceiverHandler.kt index 3d8df455d..3d415d221 100644 --- a/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReceiverHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReceiverHandler.kt @@ -20,9 +20,11 @@ package com.demonwav.mcdev.platform.mixin.handlers.mixinextras -import com.demonwav.mcdev.util.Parameter +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.knownSignatures +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 @@ -48,9 +50,23 @@ class ModifyReceiverHandler : MixinExtrasInjectorAnnotationHandler() { targetClass: ClassNode, targetMethod: MethodNode, target: TargetInsn - ): Pair, PsiType>? { + ): GeneralSignatures? { val params = getPsiParameters(target.insn, targetClass, annotation) ?: return null - return 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).knownSignatures() ?: 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 bc8364b59..14b7a77d0 100644 --- a/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReturnValueHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReturnValueHandler.kt @@ -20,10 +20,13 @@ 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.SuggestedSignature +import com.demonwav.mcdev.platform.mixin.inspection.injector.knownSignatures +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 @@ -38,9 +41,23 @@ class ModifyReturnValueHandler : MixinExtrasInjectorAnnotationHandler() { targetClass: ClassNode, targetMethod: MethodNode, target: TargetInsn - ): Pair, PsiType> { + ): GeneralSignatures { val returnType = targetMethod.getGenericReturnType(targetClass, annotation.project) - return 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).knownSignatures() ?: return null + ) } override val mixinExtrasExpressionContextType = ExpressionContext.Type.MODIFY_RETURN_VALUE diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt index 10c2a0dfe..56b315e79 100644 --- a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt @@ -22,13 +22,13 @@ package com.demonwav.mcdev.platform.mixin.handlers.mixinextras import com.demonwav.mcdev.platform.mixin.handlers.InjectorAnnotationHandler 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.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.knownSignatures 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.search.GlobalSearchScope @@ -39,20 +39,16 @@ class WrapMethodHandler : InjectorAnnotationHandler() { override fun expectedMethodSignatures( annotation: PsiAnnotation, targets: List, - ): List> { + ): List> { return targets.map { (targetClass, targetMethod) -> val returnType = targetMethod.getGenericReturnType(targetClass, annotation.project) - listOf( - MethodSignature( - collectTargetMethodParameters(annotation.project, targetClass, targetMethod) + - Parameter( - "original", - mixinExtrasOperationType(annotation, returnType) ?: return@map emptyList() - ), + ExpectedSignatures.Valid( + OperationWrapperSignatures( + annotation, + collectTargetMethodParameters(annotation.project, targetClass, targetMethod), returnType, - allowCoerceRequired = true, - ) + ) ?: return@map ExpectedSignatures.Invalid ) } } @@ -61,7 +57,10 @@ class WrapMethodHandler : InjectorAnnotationHandler() { annotation: PsiAnnotation, targets: List ): SuggestedSignature? { - return SuggestedSignature.operationWrapper(annotation, targets, this) + return SuggestedSignature.operationWrapper( + annotation, + expectedMethodSignatures(annotation, targets).knownSignatures() ?: return null + ) } override fun isUnresolved( diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt index 18e24dd7c..d6b5c6d55 100644 --- a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt @@ -21,9 +21,10 @@ 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.OperationWrapperSignatures import com.demonwav.mcdev.platform.mixin.inspection.injector.SuggestedSignature +import com.demonwav.mcdev.platform.mixin.inspection.injector.knownSignatures import com.demonwav.mcdev.platform.mixin.util.ClassAndMethodNode -import com.demonwav.mcdev.platform.mixin.util.mixinExtrasOperationType import com.demonwav.mcdev.platform.mixin.util.toPsiType import com.demonwav.mcdev.util.Parameter import com.intellij.psi.JavaPsiFacade @@ -53,16 +54,29 @@ class WrapOperationHandler : MixinExtrasInjectorAnnotationHandler() { targetClass: ClassNode, targetMethod: MethodNode, target: TargetInsn - ): Pair, PsiType>? { + ): OperationWrapperSignatures? { val params = getParameterTypes(target, targetClass, annotation) ?: return null val returnType = getReturnType(target, annotation) ?: return null - val operationType = mixinExtrasOperationType(annotation, returnType) ?: return null - return ( - params + Parameter("original", operationType) - ) to returnType + return OperationWrapperSignatures( + annotation, + params, + returnType, + intLikeTypePositions(target), + trailingParams = collectTargetMethodParameters(annotation.project, targetClass, targetMethod), + ) + } + + override fun suggestedMethodSignature( + annotation: PsiAnnotation, + targets: List + ): SuggestedSignature? { + return SuggestedSignature.operationWrapper( + annotation, + expectedMethodSignatures(annotation, targets).knownSignatures() ?: return null + ) } - override fun intLikeTypePositions(target: TargetInsn) = buildSet { + private fun intLikeTypePositions(target: TargetInsn) = buildSet { if ( target.getDecoration(ExpressionDecorations.SIMPLE_OPERATION_RETURN_TYPE) == ExpressionASMUtils.INTLIKE_TYPE @@ -76,13 +90,6 @@ class WrapOperationHandler : MixinExtrasInjectorAnnotationHandler() { } } - override fun suggestedMethodSignature( - annotation: PsiAnnotation, - targets: List - ): SuggestedSignature? { - return SuggestedSignature.operationWrapper(annotation, targets, this) - } - private fun getParameterTypes( target: TargetInsn, targetClass: ClassNode, diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapWithConditionHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapWithConditionHandler.kt index 8cb73d64f..f2cba0641 100644 --- a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapWithConditionHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapWithConditionHandler.kt @@ -20,10 +20,12 @@ 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.SuggestedSignature +import com.demonwav.mcdev.platform.mixin.inspection.injector.knownSignatures +import com.demonwav.mcdev.platform.mixin.util.ClassAndMethodNode import com.demonwav.mcdev.platform.mixin.util.nextRealInsn -import com.demonwav.mcdev.util.Parameter 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 @@ -71,9 +73,23 @@ class WrapWithConditionHandler : MixinExtrasInjectorAnnotationHandler() { targetClass: ClassNode, targetMethod: MethodNode, target: TargetInsn - ): Pair, PsiType>? { + ): GeneralSignatures? { val params = getPsiParameters(target.insn, targetClass, annotation) ?: return null - return 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).knownSignatures() ?: return null + ) } override val mixinExtrasExpressionContextType = ExpressionContext.Type.WRAP_WITH_CONDITION diff --git a/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt b/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt index 456a941a1..43a1721c8 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt @@ -28,7 +28,6 @@ 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.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.platform.mixin.util.mixinTargets @@ -67,12 +66,9 @@ 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.VariableKind import com.intellij.psi.util.PsiTreeUtil import com.intellij.psi.util.PsiUtil -import com.intellij.psi.util.TypeConversionUtil import com.intellij.psi.util.startOffset import com.siyeh.ig.psiutils.VariableNameGenerator import org.objectweb.asm.Opcodes @@ -175,9 +171,7 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() { } val isAlreadyValid = handler.expectedMethodSignatures(annotation, targetMethods) - .all { options -> - options.any { it.matches(method) } - } + .all { it.matches(method) } if (isAlreadyValid) { return diff --git a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt index a3edf1703..d2ac7ce0e 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt @@ -24,6 +24,7 @@ import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Annotations.COERCE import com.demonwav.mcdev.platform.mixin.util.isAssignable import com.demonwav.mcdev.platform.mixin.util.isMixinExtrasSugar import com.demonwav.mcdev.util.Parameter +import com.demonwav.mcdev.util.allSame import com.demonwav.mcdev.util.countIsLessThan import com.demonwav.mcdev.util.normalize import com.intellij.psi.PsiMethod @@ -38,14 +39,13 @@ data class MethodSignature( val allowCoerceRequired: Boolean, val trailingParams: List = emptyList(), val allowCoerceTrailing: Boolean = true, - val trailingByDefault: Boolean = false, val intLikeTypes: Set = emptySet() ) { fun matches(method: PsiMethod): Boolean { val returnType = method.returnType ?: return false val parameters = method.parameterList.parameters.dropLastWhile { it.isMixinExtrasSugar } - return intLikeTypes.asSequence().map { it.getElement(method)?.type }.distinct().countIsLessThan(2) + return intLikeTypes.asSequence().map { it.getElement(method)?.type }.allSame() && matchReturnType(returnType, method.hasAnnotation(COERCE)) && parameters.size in requiredParams.size..requiredParams.size + trailingParams.size && matchParams(requiredParams, parameters, allowCoerceRequired, 0) 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..31eff24ce --- /dev/null +++ b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt @@ -0,0 +1,197 @@ +/* + * 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.intellij.psi.PsiElement +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiType +import com.intellij.psi.PsiTypes +import java.util.SequencedMap + +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>.knownSignatures(): 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: List, + val allowCoerce: Boolean, + val fullParams: List? = null, + val trailingParams: List = emptyList(), +) : MethodSignatures { + override val options = paramOptions.flatMap { param -> + listOfNotNull( + MethodSignature( + listOf(param), + param.type, + allowCoerceRequired = allowCoerce, + trailingParams = trailingParams, + ), + fullParams?.let { + MethodSignature( + it, + param.type, + allowCoerceRequired = allowCoerce, + 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: Set, + val trailingParams: List, + operationType: PsiType, +) : MethodSignatures { + val signature = MethodSignature( + params + Parameter("original", operationType), + returnType, + allowCoerceRequired = true, + trailingParams = trailingParams, + intLikeTypes = intLikePositions, + ) + + override val options = listOf(signature) + + companion object { + operator fun invoke( + context: PsiElement, + params: List, + returnType: PsiType, + intLikePositions: Set = emptySet(), + 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: Set = emptySet(), +) : MethodSignatures { + constructor( + params: List, + returnType: PsiType, + trailingParams: List, + allowCoerce: Boolean = true, + intLikePositions: Set = emptySet(), + ) : this( + params, + linkedMapOf(TypeKind.of(returnType) to returnType), + allowCoerce, + trailingParams, + intLikePositions, + ) + + override val options = returnTypeOptions.values.map { returnType -> + MethodSignature( + params, + returnType, + allowCoerceRequired = allowCoerce, + trailingParams = trailingParams, + intLikeTypes = intLikePositions, + ) + } +} + +class InjectSignatures( + 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 { + operator fun invoke( + 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/SuggestedSignature.kt b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt index 8102b7cbc..d37a31f49 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt @@ -20,9 +20,11 @@ package com.demonwav.mcdev.platform.mixin.inspection.injector -import com.demonwav.mcdev.platform.mixin.handlers.InjectorAnnotationHandler -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.allSame import com.demonwav.mcdev.util.normalize +import com.demonwav.mcdev.util.sharedPrefixLength import com.intellij.psi.GenericsUtil import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiManager @@ -97,25 +99,21 @@ data class SuggestedSignature( } companion object { - fun exact(signature: MethodSignature): SuggestedSignature { + fun exact(signature: MethodSignature, takeTrailing: Int = 0): SuggestedSignature { return SuggestedSignature( - signature.requiredParams.map { Param(it.name, it.type) }, + (signature.requiredParams + signature.trailingParams.take(takeTrailing)).map { + Param( + it.name, + it.type, + ) + }, signature.returnType, signature.intLikeTypes, ) } - fun modifierNoCoerce( - annotation: PsiAnnotation, - targets: List, - handler: InjectorAnnotationHandler, - ): SuggestedSignature? { - val parameterOptions = handler.expectedMethodSignatures(annotation, targets).map { signatures -> - signatures.mapNotNull { - require(it.intLikeTypes.isEmpty()) - it.requiredParams.singleOrNull() - }.ifEmpty { return null } - } + fun modifierNoCoerce(annotation: PsiAnnotation, signatures: List, ): SuggestedSignature? { + val parameterOptions = signatures.map { it.paramOptions.ifEmpty { return null } } val psiManager = PsiManager.getInstance(annotation.project) val optionsByType = parameterOptions.asSequence().flatten().groupBy { it.type.normalize() } @@ -128,15 +126,37 @@ data class SuggestedSignature( return SuggestedSignature(listOf(Param(name, type)), type) } - fun operationWrapper( - annotation: PsiAnnotation, - targets: List, - handler: InjectorAnnotationHandler, - ): SuggestedSignature? { + fun operationWrapper(annotation: PsiAnnotation, signatures: List): SuggestedSignature? { + return intersectCoerce(annotation, signatures.asSequence().map { exact(it.signature) }) + } + + fun inject(annotation: PsiAnnotation, signatures: List): SuggestedSignature? { + if (!signatures.asSequence().map { it.params.kinds() }.allSame()) { + // Shape mismatch, use short form + return intersectCoerce( + annotation, + signatures.asSequence().map { exact(it.shortSignature ?: it.longSignature) }, + ) + } + + val localsToUse = sharedPrefixLength(signatures.map { it.locals.kinds() }) + + return intersectCoerce( + annotation, + signatures.asSequence() + .map { exact(it.longSignature, takeTrailing = localsToUse) }, + ) + } + + fun general(annotation: PsiAnnotation, signatures: List): SuggestedSignature? { + // TODO + + return signatures.firstOrNull()?.options?.firstOrNull()?.let(::exact) + } + + private fun intersectCoerce(annotation: PsiAnnotation, signatures: Sequence): SuggestedSignature? { val manager = PsiManager.getInstance(annotation.project) - return handler.expectedMethodSignatures(annotation, targets).map { - exact(it.single()) - }.reduceOrNull { acc, it -> acc?.intersectCoerce(it, manager) } + return signatures.reduceOrNull { acc, it -> acc?.intersectCoerce(it, manager) } } } } @@ -145,6 +165,8 @@ private fun roughShapeOf(signature: SuggestedSignature): Shape { return Shape(signature.params.map { TypeKind.of(it.type) }, TypeKind.of(signature.returnType)) } +private fun List.kinds() = map { TypeKind.of(it.type) } + private fun getSupertype( manager: PsiManager, a: PsiType, @@ -184,31 +206,6 @@ private data class TypeMergeResult(val type: PsiType, val isIntLike: Boolean, va private data class Shape(val params: List, val returnType: T) -private 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 - } -} - //private class Shape(private val init: Shape?, private val last: T) { // private val hashCode = init.hashCode() * 31 + last.hashCode() // 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/platform/mixin/inspection/injector/ExpectedSignatures.kt b/src/main/kotlin/util/lists.kt similarity index 62% rename from src/main/kotlin/platform/mixin/inspection/injector/ExpectedSignatures.kt rename to src/main/kotlin/util/lists.kt index d0cc8f658..c0d9e2567 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/ExpectedSignatures.kt +++ b/src/main/kotlin/util/lists.kt @@ -18,8 +18,23 @@ * along with this program. If not, see . */ -package com.demonwav.mcdev.platform.mixin.inspection.injector +package com.demonwav.mcdev.util -interface ExpectedSignatures { - val options: List +fun sharedPrefixLength(lists: List>): Int { + if (lists.isEmpty()) { + return 0 + } + lists.singleOrNull()?.let { return it.size } + + val firstList = lists.first() + + for (i in firstList.indices) { + val targetElement = firstList[i] + + if (lists.any { i >= it.size || it[i] != targetElement }) { + return i + } + } + + return firstList.size } diff --git a/src/main/kotlin/util/sequences.kt b/src/main/kotlin/util/sequences.kt index ea2c6f7de..e270f6d65 100644 --- a/src/main/kotlin/util/sequences.kt +++ b/src/main/kotlin/util/sequences.kt @@ -54,3 +54,5 @@ 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<*>.allSame() = zipWithNext().all { (a, b) -> a == b } From b8be5159ecff6db46067dc48a4a4ffd79f52d75a Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Sat, 19 Sep 2026 20:41:33 +0100 Subject: [PATCH 06/51] finish general intersection --- ...nvalidInjectorMethodSignatureInspection.kt | 1 - .../inspection/injector/MethodSignature.kt | 22 ++------- .../inspection/injector/MethodSignatures.kt | 18 ++++---- .../inspection/injector/SuggestedSignature.kt | 46 ++++++++++++++++++- src/main/kotlin/platform/mixin/util/Mixin.kt | 17 +++++++ 5 files changed, 74 insertions(+), 30 deletions(-) diff --git a/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt b/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt index 43a1721c8..acfcf6289 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt @@ -85,7 +85,6 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() { val identifier = method.nameIdentifier ?: return val modifiers = method.modifierList val parameters = method.parameterList - val methodReturnType = method.returnType ?: return val (annotation, handler) = modifiers.annotations.firstNotNullOfOrNull { annotation -> (MixinAnnotationHandler.forMixinAnnotation(annotation, annotation.project) diff --git a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt index d2ac7ce0e..0ece4e226 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt @@ -21,6 +21,7 @@ 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.isAssignable import com.demonwav.mcdev.platform.mixin.util.isMixinExtrasSugar import com.demonwav.mcdev.util.Parameter @@ -82,29 +83,14 @@ data class MethodSignature( .all { (expected, indexAndActual) -> val (index, actual) = indexAndActual matchType( - actual.type, expected.type, + actual.type, allowCoerce && actual.hasAnnotation(COERCE), TypePosition.Param(index), ) } } - private fun matchType(actual: PsiType, expected: PsiType, coerce: Boolean, typePos: TypePosition): Boolean = when { - typePos in intLikeTypes -> actual in INT_TYPES - actual.normalize() == expected.normalize() -> true - !coerce -> false - expected in INT_TYPES -> actual == PsiTypes.intType() - else -> isAssignable(actual.normalize(), expected.normalize()) - } - - companion object { - private val INT_TYPES = setOf( - PsiTypes.intType(), - PsiTypes.shortType(), - PsiTypes.charType(), - PsiTypes.byteType(), - PsiTypes.booleanType() - ) - } + private fun matchType(expected: PsiType, actual: PsiType, coerce: Boolean, typePos: TypePosition) = + checkCoerce(expected, actual, coerce, typePos in intLikeTypes) } diff --git a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt index 31eff24ce..efc316a96 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt @@ -141,15 +141,15 @@ data class GeneralSignatures( intLikePositions, ) - override val options = returnTypeOptions.values.map { returnType -> - MethodSignature( - params, - returnType, - allowCoerceRequired = allowCoerce, - trailingParams = trailingParams, - intLikeTypes = intLikePositions, - ) - } + override val options = returnTypeOptions.keys.map(::specificSignature) + + fun specificSignature(returnKind: TypeKind) = MethodSignature( + params, + returnTypeOptions.getValue(returnKind), + allowCoerceRequired = allowCoerce, + trailingParams = trailingParams, + intLikeTypes = intLikePositions, + ) } class InjectSignatures( diff --git a/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt index d37a31f49..90eae8b17 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt @@ -21,6 +21,7 @@ package com.demonwav.mcdev.platform.mixin.inspection.injector import com.demonwav.mcdev.platform.mixin.util.TypeKind +import com.demonwav.mcdev.platform.mixin.util.checkCoerce import com.demonwav.mcdev.util.Parameter import com.demonwav.mcdev.util.allSame import com.demonwav.mcdev.util.normalize @@ -149,9 +150,50 @@ data class SuggestedSignature( } fun general(annotation: PsiAnnotation, signatures: List): SuggestedSignature? { - // TODO + val signaturesByReturnKind = signatures.asSequence().flatMap { sig -> + sig.returnTypeOptions.keys.map { it to sig } + }.groupBy({ it.first }, { it.second }) + val returnKind = + signaturesByReturnKind.entries.firstOrNull { it.value.size == signatures.size }?.key ?: return null - return signatures.firstOrNull()?.options?.firstOrNull()?.let(::exact) + val numParams = signatures.maxOf { it.params.size } + val intersected = intersectCoerce( + annotation, + signatures.asSequence() + .map { + val signature = it.specificSignature(returnKind) + exact(signature, takeTrailing = numParams - signature.requiredParams.size) + } + ) ?: return null + + for (signature in signatures) { + if (signature.allowCoerce) { + continue + } + // Match strictly + val returnTypeMatches = checkCoerce( + signature.returnTypeOptions.getValue(returnKind), + intersected.returnType, + intersected.coerceReturnType, + MethodSignature.TypePosition.Return in signature.intLikePositions, + ) + if (!returnTypeMatches) { + return null + } + val paramsMatch = signature.params.withIndex().all { (index, param) -> + checkCoerce( + param.type, + intersected.params[index].type, + coerce = false, + MethodSignature.TypePosition.Param(index) in signature.intLikePositions, + ) + } + if (!paramsMatch) { + return null + } + } + + return intersected } private fun intersectCoerce(annotation: PsiAnnotation, signatures: Sequence): SuggestedSignature? { diff --git a/src/main/kotlin/platform/mixin/util/Mixin.kt b/src/main/kotlin/platform/mixin/util/Mixin.kt index 2181d55b4..77a350f47 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 + expected in INT_TYPES -> actual == PsiTypes.intType() + else -> isAssignable(actual.normalize(), expected.normalize()) +} From 59debcb2970234194fe20071a671b16807fb61a2 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Sat, 19 Sep 2026 21:46:24 +0100 Subject: [PATCH 07/51] cleanup --- .../mixin/handlers/InjectAnnotationHandler.kt | 4 +- .../handlers/InjectorAnnotationHandler.kt | 9 ++- .../mixin/handlers/ModifyArgHandler.kt | 4 +- .../mixin/handlers/ModifyConstantHandler.kt | 4 +- .../mixin/handlers/ModifyVariableHandler.kt | 4 +- .../mixin/handlers/RedirectInjectorHandler.kt | 4 +- .../ModifyExpressionValueHandler.kt | 4 +- .../mixinextras/ModifyReceiverHandler.kt | 4 +- .../mixinextras/ModifyReturnValueHandler.kt | 4 +- .../handlers/mixinextras/WrapMethodHandler.kt | 4 +- .../mixinextras/WrapOperationHandler.kt | 4 +- .../mixinextras/WrapWithConditionHandler.kt | 4 +- .../inspection/injector/MethodSignatures.kt | 2 +- .../inspection/injector/SuggestedSignature.kt | 61 +++---------------- 14 files changed, 39 insertions(+), 77 deletions(-) diff --git a/src/main/kotlin/platform/mixin/handlers/InjectAnnotationHandler.kt b/src/main/kotlin/platform/mixin/handlers/InjectAnnotationHandler.kt index d7dcd5931..a8bed6027 100644 --- a/src/main/kotlin/platform/mixin/handlers/InjectAnnotationHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/InjectAnnotationHandler.kt @@ -24,7 +24,7 @@ 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.knownSignatures +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.getGenericReturnType @@ -97,7 +97,7 @@ class InjectAnnotationHandler : InsnInjectorAnnotationHandler() { ): SuggestedSignature? { return SuggestedSignature.inject( annotation, - expectedMethodSignatures(annotation, targets).knownSignatures() ?: return null, + expectedMethodSignatures(annotation, targets).collectSignatures() ?: return null, ) } diff --git a/src/main/kotlin/platform/mixin/handlers/InjectorAnnotationHandler.kt b/src/main/kotlin/platform/mixin/handlers/InjectorAnnotationHandler.kt index 485ac18d3..4aab4e95a 100644 --- a/src/main/kotlin/platform/mixin/handlers/InjectorAnnotationHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/InjectorAnnotationHandler.kt @@ -109,10 +109,10 @@ abstract class InjectorAnnotationHandler : MixinAnnotationHandler { targets: List, ): List> - open fun suggestedMethodSignature( + abstract fun suggestedMethodSignature( annotation: PsiAnnotation, targets: List, - ): SuggestedSignature? = null + ): SuggestedSignature? open fun canAlwaysBeStatic(method: PsiMethod): Boolean { return true @@ -173,6 +173,11 @@ object DefaultInjectorAnnotationHandler : InsnInjectorAnnotationHandler() { targetInsn: TargetInsn, ): ExpectedSignatures = ExpectedSignatures.Unknown + override fun suggestedMethodSignature( + annotation: PsiAnnotation, + targets: List + ): SuggestedSignature? = null + override val isSoft = true override val mixinExtrasExpressionContextType = ExpressionContext.Type.CUSTOM diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt index efc8258e2..e12489507 100644 --- a/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt @@ -24,7 +24,7 @@ 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.knownSignatures +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 @@ -108,7 +108,7 @@ class ModifyArgHandler : InsnInjectorAnnotationHandler() { ): SuggestedSignature? { return SuggestedSignature.modifierNoCoerce( annotation, - expectedMethodSignatures(annotation, targets).knownSignatures() ?: return null, + expectedMethodSignatures(annotation, targets).collectSignatures() ?: return null, ) } diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt index 3b7f26fc4..6baa6ae2e 100644 --- a/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt @@ -29,7 +29,7 @@ import com.demonwav.mcdev.platform.mixin.inspection.injector.MethodSignature import com.demonwav.mcdev.platform.mixin.inspection.injector.MethodSignatures 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.knownSignatures +import com.demonwav.mcdev.platform.mixin.inspection.injector.collectSignatures import com.demonwav.mcdev.platform.mixin.util.ClassAndMethodNode import com.demonwav.mcdev.util.Parameter import com.intellij.psi.JavaPsiFacade @@ -109,7 +109,7 @@ class ModifyConstantHandler : InsnInjectorAnnotationHandler() { } else { SuggestedSignature.modifierNoCoerce( annotation, - expectedMethodSignatures(annotation, targets).knownSignatures() ?: return null, + expectedMethodSignatures(annotation, targets).collectSignatures() ?: return null, ) } } diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt index 78528a81c..5c1b710b3 100644 --- a/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt @@ -25,7 +25,7 @@ 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.knownSignatures +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 @@ -82,7 +82,7 @@ class ModifyVariableHandler : InsnInjectorAnnotationHandler() { ): SuggestedSignature? { return SuggestedSignature.modifierNoCoerce( annotation, - expectedMethodSignatures(annotation, targets).knownSignatures() ?: return null, + expectedMethodSignatures(annotation, targets).collectSignatures() ?: return null, ) } diff --git a/src/main/kotlin/platform/mixin/handlers/RedirectInjectorHandler.kt b/src/main/kotlin/platform/mixin/handlers/RedirectInjectorHandler.kt index 6c1552760..21dccd99b 100644 --- a/src/main/kotlin/platform/mixin/handlers/RedirectInjectorHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/RedirectInjectorHandler.kt @@ -25,7 +25,7 @@ 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.knownSignatures +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 @@ -112,7 +112,7 @@ class RedirectInjectorHandler : InsnInjectorAnnotationHandler() { ): SuggestedSignature? { return SuggestedSignature.general( annotation, - expectedMethodSignatures(annotation, targets).knownSignatures() ?: return null + expectedMethodSignatures(annotation, targets).collectSignatures() ?: return null ) } diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyExpressionValueHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyExpressionValueHandler.kt index d1ee3f5ce..ef0abf3ae 100644 --- a/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyExpressionValueHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyExpressionValueHandler.kt @@ -23,7 +23,7 @@ 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.SuggestedSignature -import com.demonwav.mcdev.platform.mixin.inspection.injector.knownSignatures +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 @@ -80,7 +80,7 @@ class ModifyExpressionValueHandler : MixinExtrasInjectorAnnotationHandler() { ): SuggestedSignature? { return SuggestedSignature.general( annotation, - expectedMethodSignatures(annotation, targets).knownSignatures() ?: return null + expectedMethodSignatures(annotation, targets).collectSignatures() ?: return null ) } diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReceiverHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReceiverHandler.kt index 3d415d221..3450e4cec 100644 --- a/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReceiverHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReceiverHandler.kt @@ -22,7 +22,7 @@ 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.SuggestedSignature -import com.demonwav.mcdev.platform.mixin.inspection.injector.knownSignatures +import com.demonwav.mcdev.platform.mixin.inspection.injector.collectSignatures import com.demonwav.mcdev.platform.mixin.util.ClassAndMethodNode import com.intellij.psi.PsiAnnotation import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext @@ -65,7 +65,7 @@ class ModifyReceiverHandler : MixinExtrasInjectorAnnotationHandler() { ): SuggestedSignature? { return SuggestedSignature.general( annotation, - expectedMethodSignatures(annotation, targets).knownSignatures() ?: return null + expectedMethodSignatures(annotation, targets).collectSignatures() ?: return null ) } diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReturnValueHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReturnValueHandler.kt index 14b7a77d0..d6003451c 100644 --- a/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReturnValueHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReturnValueHandler.kt @@ -22,7 +22,7 @@ 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.SuggestedSignature -import com.demonwav.mcdev.platform.mixin.inspection.injector.knownSignatures +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 @@ -56,7 +56,7 @@ class ModifyReturnValueHandler : MixinExtrasInjectorAnnotationHandler() { ): SuggestedSignature? { return SuggestedSignature.general( annotation, - expectedMethodSignatures(annotation, targets).knownSignatures() ?: return null + expectedMethodSignatures(annotation, targets).collectSignatures() ?: return null ) } diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt index 56b315e79..29d02c748 100644 --- a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt @@ -25,7 +25,7 @@ import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.InsnResolutionI 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.knownSignatures +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 @@ -59,7 +59,7 @@ class WrapMethodHandler : InjectorAnnotationHandler() { ): SuggestedSignature? { return SuggestedSignature.operationWrapper( annotation, - expectedMethodSignatures(annotation, targets).knownSignatures() ?: return null + expectedMethodSignatures(annotation, targets).collectSignatures() ?: return null ) } diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt index d6b5c6d55..c1d53a80a 100644 --- a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt @@ -23,7 +23,7 @@ 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.OperationWrapperSignatures import com.demonwav.mcdev.platform.mixin.inspection.injector.SuggestedSignature -import com.demonwav.mcdev.platform.mixin.inspection.injector.knownSignatures +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 @@ -72,7 +72,7 @@ class WrapOperationHandler : MixinExtrasInjectorAnnotationHandler() { ): SuggestedSignature? { return SuggestedSignature.operationWrapper( annotation, - expectedMethodSignatures(annotation, targets).knownSignatures() ?: return null + expectedMethodSignatures(annotation, targets).collectSignatures() ?: return null ) } diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapWithConditionHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapWithConditionHandler.kt index f2cba0641..af588256f 100644 --- a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapWithConditionHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapWithConditionHandler.kt @@ -22,7 +22,7 @@ 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.SuggestedSignature -import com.demonwav.mcdev.platform.mixin.inspection.injector.knownSignatures +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 @@ -88,7 +88,7 @@ class WrapWithConditionHandler : MixinExtrasInjectorAnnotationHandler() { ): SuggestedSignature? { return SuggestedSignature.general( annotation, - expectedMethodSignatures(annotation, targets).knownSignatures() ?: return null + expectedMethodSignatures(annotation, targets).collectSignatures() ?: return null ) } diff --git a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt index efc316a96..61b694ea2 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt @@ -47,7 +47,7 @@ sealed interface ExpectedSignatures { } } -inline fun List>.knownSignatures(): List? = +inline fun List>.collectSignatures(): List? = mapNotNull { when (it) { ExpectedSignatures.Unknown -> null diff --git a/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt index 90eae8b17..b783f4f84 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt @@ -41,7 +41,7 @@ data class SuggestedSignature( data class Param(val name: String?, val type: PsiType, val coerce: Boolean = false) fun intersectCoerce(other: SuggestedSignature, manager: PsiManager): SuggestedSignature? { - if (roughShapeOf(this) != roughShapeOf(other)) { + if (!kindsMatch(this, other)) { return null } val intLikeForcings = mutableSetOf() @@ -113,7 +113,7 @@ data class SuggestedSignature( ) } - fun modifierNoCoerce(annotation: PsiAnnotation, signatures: List, ): SuggestedSignature? { + fun modifierNoCoerce(annotation: PsiAnnotation, signatures: List): SuggestedSignature? { val parameterOptions = signatures.map { it.paramOptions.ifEmpty { return null } } val psiManager = PsiManager.getInstance(annotation.project) @@ -151,7 +151,7 @@ data class SuggestedSignature( fun general(annotation: PsiAnnotation, signatures: List): SuggestedSignature? { val signaturesByReturnKind = signatures.asSequence().flatMap { sig -> - sig.returnTypeOptions.keys.map { it to sig } + sig.returnTypeOptions.keys.asSequence().map { it to sig } }.groupBy({ it.first }, { it.second }) val returnKind = signaturesByReturnKind.entries.firstOrNull { it.value.size == signatures.size }?.key ?: return null @@ -163,7 +163,7 @@ data class SuggestedSignature( .map { val signature = it.specificSignature(returnKind) exact(signature, takeTrailing = numParams - signature.requiredParams.size) - } + }, ) ?: return null for (signature in signatures) { @@ -203,9 +203,10 @@ data class SuggestedSignature( } } -private fun roughShapeOf(signature: SuggestedSignature): Shape { - return Shape(signature.params.map { TypeKind.of(it.type) }, TypeKind.of(signature.returnType)) -} +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) } private fun List.kinds() = map { TypeKind.of(it.type) } @@ -230,7 +231,7 @@ private fun getSupertype( aIsIntLike -> TypeMergeResult( intLikeAssignment ?: b, isIntLike = intLikeAssignment == null && bIsIntLike, - coerce = intLikeAssignment != null && intLikeAssignment != b, + coerce = intLikeAssignment != null && !bIsIntLike && intLikeAssignment != b, ) bIsIntLike -> TypeMergeResult(a, isIntLike = false, coerce = false) a == b -> TypeMergeResult(a, isIntLike = false, coerce = false) @@ -245,47 +246,3 @@ private fun getSupertype( } private data class TypeMergeResult(val type: PsiType, val isIntLike: Boolean, val coerce: Boolean) - -private data class Shape(val params: List, val returnType: T) - -//private class Shape(private val init: Shape?, private val last: T) { -// private val hashCode = init.hashCode() * 31 + last.hashCode() -// -// override fun equals(other: Any?): Boolean { -// if (other !is Shape<*>) { -// return false -// } -// @Suppress("UNCHECKED_CAST") -// other as Shape -// -// var a: Shape? = this -// var b: Shape? = other -// while (a != null && b != null) { -// if (a === b) { -// return true -// } -// if (a.hashCode != b.hashCode || a.last != b.last) { -// return false -// } -// a = a.init -// b = b.init -// } -// return a == null && b == null -// } -// -// override fun hashCode() = hashCode -// -// override fun toString() = buildString { -// append('[') -// this@Shape.append(this@buildString) -// append(']') -// } -// -// private fun append(builder: StringBuilder) { -// init?.let { -// it.append(builder) -// builder.append(", ") -// } -// builder.append(last) -// } -//} From bf4e7689141d0173ad4ac0f55c8596b5d73d2439 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Sun, 20 Sep 2026 12:32:28 +0100 Subject: [PATCH 08/51] stash --- .../handlers/InjectorAnnotationHandler.kt | 2 + .../handlers/InsnInjectorAnnotationHandler.kt | 1 + .../mixin/handlers/ModifyArgHandler.kt | 2 +- .../mixin/handlers/ModifyConstantHandler.kt | 3 +- .../mixin/handlers/ModifyVariableHandler.kt | 9 +- .../handlers/mixinextras/WrapMethodHandler.kt | 2 + ...nvalidInjectorMethodSignatureInspection.kt | 8 +- .../inspection/injector/MethodSignature.kt | 8 +- .../inspection/injector/MethodSignatures.kt | 7 +- .../injector/MixinParameterNameInspection.kt | 274 +++++++++--------- .../inspection/injector/SuggestedSignature.kt | 10 +- .../kotlin/platform/mixin/util/LocalInfo.kt | 17 +- src/main/kotlin/util/sequences.kt | 2 +- src/main/kotlin/util/utils.kt | 5 + 14 files changed, 183 insertions(+), 167 deletions(-) diff --git a/src/main/kotlin/platform/mixin/handlers/InjectorAnnotationHandler.kt b/src/main/kotlin/platform/mixin/handlers/InjectorAnnotationHandler.kt index 4aab4e95a..99a399be1 100644 --- a/src/main/kotlin/platform/mixin/handlers/InjectorAnnotationHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/InjectorAnnotationHandler.kt @@ -22,6 +22,7 @@ package com.demonwav.mcdev.platform.mixin.handlers 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.handlers.mixinextras.TargetInsn import com.demonwav.mcdev.platform.mixin.inspection.injector.ExpectedSignatures @@ -107,6 +108,7 @@ abstract class InjectorAnnotationHandler : MixinAnnotationHandler { abstract fun expectedMethodSignatures( annotation: PsiAnnotation, targets: List, + mode: CollectVisitor.Mode = CollectVisitor.Mode.SUGGESTION, ): List> abstract fun suggestedMethodSignature( diff --git a/src/main/kotlin/platform/mixin/handlers/InsnInjectorAnnotationHandler.kt b/src/main/kotlin/platform/mixin/handlers/InsnInjectorAnnotationHandler.kt index 06d1c9809..fe15d2332 100644 --- a/src/main/kotlin/platform/mixin/handlers/InsnInjectorAnnotationHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/InsnInjectorAnnotationHandler.kt @@ -107,6 +107,7 @@ abstract class InsnInjectorAnnotationHandler : InjectorAnnotationHandler() { final override fun expectedMethodSignatures( annotation: PsiAnnotation, targets: List, + mode: CollectVisitor.Mode, ): List> { return resolveInstructions(annotation, targets).map { (target, result) -> val (targetClass, targetMethod) = target diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt index e12489507..1114452be 100644 --- a/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt @@ -80,7 +80,7 @@ class ModifyArgHandler : InsnInjectorAnnotationHandler() { ).resolveMember(project) as PsiMethod? val elementFactory = JavaPsiFacade.getElementFactory(annotation.project) val psiParams = argTypes.indices.map { index -> sourceMethod?.getBytecodeParameter(index) } - val paramOptions = validTypes.map { type -> + val paramOptions = validTypes.associateWith { type -> val targetParam = psiParams[index ?: argTypes.indexOf(type)] val psiType = targetParam?.type ?: type.toPsiType(elementFactory) sanitizedParameter(psiType, targetParam?.name) diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt index 6baa6ae2e..1425753fc 100644 --- a/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt @@ -32,6 +32,7 @@ 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.util.Parameter +import com.demonwav.mcdev.util.descriptor import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiElement @@ -156,7 +157,7 @@ class ModifyConstantHandler : InsnInjectorAnnotationHandler() { private fun makeSignatures(type: PsiType, trailingParams: List): ModifierSignatures { return ModifierSignatures( - listOf(sanitizedParameter(type, "constant")), + mapOf(Type.getType(type.descriptor) to sanitizedParameter(type, "constant")), allowCoerce = true, trailingParams = trailingParams, ) diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt index 5c1b710b3..dc5b73388 100644 --- a/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt @@ -54,16 +54,15 @@ class ModifyVariableHandler : InsnInjectorAnnotationHandler() { val info = LocalInfo.fromAnnotation(localType, annotation) val elementFactory = JavaPsiFacade.getElementFactory(annotation.project) - val seenParams = mutableSetOf() - val result = mutableListOf() + val result = mutableMapOf() val matchedLocals = info.matchLocals( module, targetClass, targetMethod, targetInsn.insn, CollectVisitor.Mode.SUGGESTION, matchType = false ).orEmpty() for (local in matchedLocals) { - if (seenParams.add(local.desc + local.name)) { - val localType = Type.getType(local.desc).toPsiType(elementFactory) - result += sanitizedParameter(localType, local.name, local.isNamed) + val type = Type.getType(local.desc ?: continue) + result.computeIfAbsent(type) { + sanitizedParameter(type.toPsiType(elementFactory), local.name, local.isNamed) } } diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt index 29d02c748..8df023c30 100644 --- a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt @@ -21,6 +21,7 @@ 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.ExpectedSignatures import com.demonwav.mcdev.platform.mixin.inspection.injector.OperationWrapperSignatures @@ -39,6 +40,7 @@ class WrapMethodHandler : InjectorAnnotationHandler() { override fun expectedMethodSignatures( annotation: PsiAnnotation, targets: List, + mode: CollectVisitor.Mode, ): List> { return targets.map { (targetClass, targetMethod) -> val returnType = targetMethod.getGenericReturnType(targetClass, annotation.project) diff --git a/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt b/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt index acfcf6289..3a28e1827 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt @@ -23,6 +23,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 import com.demonwav.mcdev.platform.mixin.util.MixinConstants import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Annotations.COERCE @@ -169,8 +170,11 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() { } } - val isAlreadyValid = handler.expectedMethodSignatures(annotation, targetMethods) - .all { it.matches(method) } + val isAlreadyValid = handler.expectedMethodSignatures( + annotation, + targetMethods, + CollectVisitor.Mode.RESOLUTION, + ).all { it.matches(method) } if (isAlreadyValid) { return diff --git a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt index 0ece4e226..0ad860352 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt @@ -22,17 +22,13 @@ 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.isAssignable import com.demonwav.mcdev.platform.mixin.util.isMixinExtrasSugar import com.demonwav.mcdev.util.Parameter -import com.demonwav.mcdev.util.allSame -import com.demonwav.mcdev.util.countIsLessThan -import com.demonwav.mcdev.util.normalize +import com.demonwav.mcdev.util.allEqual import com.intellij.psi.PsiMethod import com.intellij.psi.PsiParameter import com.intellij.psi.PsiType import com.intellij.psi.PsiTypeElement -import com.intellij.psi.PsiTypes data class MethodSignature( val requiredParams: List, @@ -46,7 +42,7 @@ data class MethodSignature( val returnType = method.returnType ?: return false val parameters = method.parameterList.parameters.dropLastWhile { it.isMixinExtrasSugar } - return intLikeTypes.asSequence().map { it.getElement(method)?.type }.allSame() + return intLikeTypes.asSequence().map { it.getElement(method)?.type }.allEqual() && matchReturnType(returnType, method.hasAnnotation(COERCE)) && parameters.size in requiredParams.size..requiredParams.size + trailingParams.size && matchParams(requiredParams, parameters, allowCoerceRequired, 0) diff --git a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt index 61b694ea2..9087c4f8d 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt @@ -30,6 +30,7 @@ import com.intellij.psi.PsiMethod import com.intellij.psi.PsiType import com.intellij.psi.PsiTypes import java.util.SequencedMap +import org.objectweb.asm.Type sealed interface ExpectedSignatures { fun matches(method: PsiMethod): Boolean @@ -52,7 +53,7 @@ inline fun List>.collectSignatures(): List? when (it) { ExpectedSignatures.Unknown -> null ExpectedSignatures.Invalid -> return null - is ExpectedSignatures.Valid<*> -> it.expected as R + is ExpectedSignatures.Valid -> it.expected as R } } @@ -61,12 +62,12 @@ interface MethodSignatures { } class ModifierSignatures( - val paramOptions: List, + val paramOptions: Map, val allowCoerce: Boolean, val fullParams: List? = null, val trailingParams: List = emptyList(), ) : MethodSignatures { - override val options = paramOptions.flatMap { param -> + override val options = paramOptions.values.flatMap { param -> listOfNotNull( MethodSignature( listOf(param), diff --git a/src/main/kotlin/platform/mixin/inspection/injector/MixinParameterNameInspection.kt b/src/main/kotlin/platform/mixin/inspection/injector/MixinParameterNameInspection.kt index 36b1c60cf..7b9537d3b 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/MixinParameterNameInspection.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/MixinParameterNameInspection.kt @@ -20,14 +20,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.util.LocalInfo import com.demonwav.mcdev.platform.mixin.util.MethodTargetMember 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 import com.intellij.codeInspection.ProblemsHolder import com.intellij.codeInspection.options.OptPane @@ -39,6 +47,7 @@ import com.intellij.psi.JavaElementVisitor import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiMethod import com.intellij.psi.PsiParameter +import com.siyeh.ig.fixes.RenameFix class MixinParameterNameInspection : MixinInspection() { @JvmField @@ -55,127 +64,125 @@ class MixinParameterNameInspection : MixinInspection() { override fun buildVisitor(holder: ProblemsHolder) = object : JavaElementVisitor() { override fun visitMethod(method: PsiMethod) { -// val module = method.findModule() ?: return -// val parameters = method.parameterList.parameters -// val parametersWithoutSugar = parameters.dropLastWhile { it.isMixinExtrasSugar } -// -// val validNames = arrayOfNulls>(parameters.size) -// -// for (annotation in method.annotations) { -// val handler = MixinAnnotationHandler.forMixinAnnotation(annotation, holder.project) -// as? InjectorAnnotationHandler ?: continue -// for (target in MixinAnnotationHandler.resolveTarget(annotation)) { -// if (!collectValidNames(validNames, module, method, parameters, parametersWithoutSugar, annotation, handler, target)) { -// return -// } -// } -// } -// -// for (index in validNames.indices) { -// val names = validNames[index] ?: continue -// if (names.isEmpty()) { -// continue -// } -// -// if (parameters[index].name !in names) { -// val fixes = mutableListOf() -// -// for (name in names) { -// fixes += RenameFix(name, false, false) -// } -// -// fixes += if (index < parametersWithoutSugar.size) { -// DontReportForMainSignatureFix() -// } else { -// DontReportForLocalFix() -// } -// -// holder.registerProblem( -// parameters[index].nameIdentifier ?: parameters[index], -// "Parameter name does not match name ${names.joinToString(" / ") { "'$it'" } } in target class", -// *fixes.toTypedArray() -// ) -// } -// } + val module = method.findModule() ?: return + val parameters = method.parameterList.parameters + val parametersWithoutSugar = parameters.dropLastWhile { it.isMixinExtrasSugar } + + val validNames = arrayOfNulls>(parameters.size) + + for (annotation in method.annotations) { + val handler = MixinAnnotationHandler.forMixinAnnotation(annotation, holder.project) + as? InjectorAnnotationHandler ?: continue + for (target in MixinAnnotationHandler.resolveTarget(annotation)) { + if (!collectValidNames(validNames, module, method, parameters, parametersWithoutSugar, annotation, handler, target)) { + return + } + } + } + + for (index in validNames.indices) { + val names = validNames[index].ifNullOrEmpty { continue } + + if (parameters[index].name !in names) { + val fixes = mutableListOf() + + for (name in names) { + fixes += RenameFix(name, false, false) + } + + fixes += if (index < parametersWithoutSugar.size) { + DontReportForMainSignatureFix() + } else { + DontReportForLocalFix() + } + + holder.registerProblem( + parameters[index].nameIdentifier ?: parameters[index], + "Parameter name does not match name ${names.joinToString(" / ") { "'$it'" } } in target class", + *fixes.toTypedArray() + ) + } + } } } -// private fun collectValidNames( -// validNames: Array?>, -// module: Module, method: PsiMethod, -// parameters: Array, -// parametersWithoutSugar: List, -// annotation: PsiAnnotation, -// handler: InjectorAnnotationHandler, -// target: MixinTargetMember -// ): Boolean { -// if (target !is MethodTargetMember) { -// return true -// } -// -// if (!method.hasNamedLocalVariables(target.classAndMethod.clazz.name.replace('/', '.'))) { -// 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 (expectedSignature in expectedSignatures) { -// if (!InvalidInjectorMethodSignatureInspection.Util.checkParameters( -// method.parameterList, -// expectedSignature -// ) -// ) { -// continue -// } -// -// anyValidSignatures = true -// -// checkExpectedSignatureForKnownNames( -// validNamesForThisTarget, -// expectedSignature.requiredParams + expectedSignature.trailingParams, -// parametersWithoutSugar -// ) -// } -// -// if (!anyValidSignatures) { -// return false -// } -// } -// -// if (reportForLocal) { -// for (pos in parametersWithoutSugar.size until parameters.size) { -// if (!checkSugarForKnownNames( -// validNamesForThisTarget, -// module, -// handler, -// annotation, -// target, -// parameters[pos], -// pos -// ) -// ) { -// return false -// } -// } -// } -// -// 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 collectValidNames( + validNames: Array?>, + module: Module, method: PsiMethod, + parameters: Array, + parametersWithoutSugar: List, + annotation: PsiAnnotation, + handler: InjectorAnnotationHandler, + target: MixinTargetMember + ): Boolean { + if (target !is MethodTargetMember) { + return true + } + + if (!method.hasNamedLocalVariables(target.classAndMethod.clazz.name.replace('/', '.'))) { + return true + } + + if (reportForMainSignature) { + 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 + + for (expectedSignature in signatures.options) { + if (!expectedSignature.matches(method)) { + continue + } + anyValidSignatures = true + + checkExpectedSignatureForKnownNames( + validNamesForThisTarget, + expectedSignature.requiredParams + expectedSignature.trailingParams, + parametersWithoutSugar, + ) + } + + 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( + validNames, + module, + handler, + annotation, + target, + parameters[pos], + pos + ) + ) { + return false + } + } + } + + return true + } private fun checkExpectedSignatureForKnownNames( validNamesForThisTarget: Array?>, @@ -188,25 +195,27 @@ class MixinParameterNameInspection : MixinInspection() { for (pos in parametersWithoutSugar.indices) { val expectedParam = expectedParams[pos] - if (expectedParam.knownName && expectedParam.name != null) { - if (validNamesForThisTarget[pos] == null) { - validNamesForThisTarget[pos] = mutableSetOf(expectedParam.name) - } else { - validNamesForThisTarget[pos]!!.add(expectedParam.name) - } + 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: InsnInjectorAnnotationHandler, + handler: InjectorAnnotationHandler, annotation: PsiAnnotation, target: MethodTargetMember, 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) @@ -221,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/SuggestedSignature.kt b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt index b783f4f84..fe295762b 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt @@ -23,7 +23,7 @@ package com.demonwav.mcdev.platform.mixin.inspection.injector import com.demonwav.mcdev.platform.mixin.util.TypeKind import com.demonwav.mcdev.platform.mixin.util.checkCoerce import com.demonwav.mcdev.util.Parameter -import com.demonwav.mcdev.util.allSame +import com.demonwav.mcdev.util.allEqual import com.demonwav.mcdev.util.normalize import com.demonwav.mcdev.util.sharedPrefixLength import com.intellij.psi.GenericsUtil @@ -114,10 +114,12 @@ data class SuggestedSignature( } fun modifierNoCoerce(annotation: PsiAnnotation, signatures: List): SuggestedSignature? { - val parameterOptions = signatures.map { it.paramOptions.ifEmpty { return null } } + val parameterOptions = signatures.map { it.paramOptions } val psiManager = PsiManager.getInstance(annotation.project) - val optionsByType = parameterOptions.asSequence().flatten().groupBy { it.type.normalize() } + val optionsByType = parameterOptions.asSequence() + .flatMap { it.entries } + .groupBy({ it.key }, { it.value }) val chosenParams = optionsByType.values.firstOrNull { it.size == parameterOptions.size } ?: return null val name = chosenParams.asSequence().map { it.name }.distinct().singleOrNull() ?: "original" @@ -132,7 +134,7 @@ data class SuggestedSignature( } fun inject(annotation: PsiAnnotation, signatures: List): SuggestedSignature? { - if (!signatures.asSequence().map { it.params.kinds() }.allSame()) { + if (!signatures.asSequence().map { it.params.kinds() }.allEqual()) { // Shape mismatch, use short form return intersectCoerce( annotation, diff --git a/src/main/kotlin/platform/mixin/util/LocalInfo.kt b/src/main/kotlin/platform/mixin/util/LocalInfo.kt index 1414345f8..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 @@ -133,14 +128,12 @@ class LocalInfo( // implicit mode if (!mode.assumeCorrectSignature) { - return locals.asSequence() - .filterNotNull() - .filter { local -> locals.count { it?.desc == local.desc } == 1 } - .toList() + 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/util/sequences.kt b/src/main/kotlin/util/sequences.kt index e270f6d65..1a6fd2f56 100644 --- a/src/main/kotlin/util/sequences.kt +++ b/src/main/kotlin/util/sequences.kt @@ -55,4 +55,4 @@ fun Sequence<*>.countIsAtLeast(n: Int) = n <= 0 || drop(n - 1).any() fun Sequence<*>.countIsLessThan(n: Int) = n > 0 && drop(n - 1).none() -fun Sequence<*>.allSame() = zipWithNext().all { (a, b) -> a == b } +fun Sequence<*>.allEqual() = zipWithNext().all { (a, b) -> a == b } diff --git a/src/main/kotlin/util/utils.kt b/src/main/kotlin/util/utils.kt index 9d86cfdee..7c4612507 100644 --- a/src/main/kotlin/util/utils.kt +++ b/src/main/kotlin/util/utils.kt @@ -50,6 +50,8 @@ 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 @@ -154,6 +156,9 @@ inline fun > T.ifEmpty(func: () -> Unit): T { } inline fun > T?.ifNullOrEmpty(func: () -> R): R where T : R { + contract { + callsInPlace(func, InvocationKind.AT_MOST_ONCE) + } if (isNullOrEmpty()) { return func() } From 497f1796535e12db34486155228a564795e383d6 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Sun, 20 Sep 2026 13:10:29 +0100 Subject: [PATCH 09/51] stash --- .../handlers/InjectorAnnotationHandler.kt | 9 --------- .../mixin/handlers/ModifyArgHandler.kt | 2 +- .../mixin/handlers/ModifyConstantHandler.kt | 2 +- .../mixin/handlers/ModifyVariableHandler.kt | 2 +- .../inspection/injector/MethodSignatures.kt | 8 ++++++-- .../inspection/injector/SuggestedSignature.kt | 19 +++++++++++++------ 6 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/main/kotlin/platform/mixin/handlers/InjectorAnnotationHandler.kt b/src/main/kotlin/platform/mixin/handlers/InjectorAnnotationHandler.kt index 99a399be1..5e9866dc5 100644 --- a/src/main/kotlin/platform/mixin/handlers/InjectorAnnotationHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/InjectorAnnotationHandler.kt @@ -155,15 +155,6 @@ abstract class InjectorAnnotationHandler : MixinAnnotationHandler { Parameter(name?.toJavaIdentifier(), type, knownName) } } - - @JvmStatic - protected fun sanitizedReturnType(type: PsiType): PsiType { - return if (type is PsiEllipsisType) { - type.toArrayType() - } else { - type - } - } } } diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt index 1114452be..c65cbff17 100644 --- a/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt @@ -80,7 +80,7 @@ class ModifyArgHandler : InsnInjectorAnnotationHandler() { ).resolveMember(project) as PsiMethod? val elementFactory = JavaPsiFacade.getElementFactory(annotation.project) val psiParams = argTypes.indices.map { index -> sourceMethod?.getBytecodeParameter(index) } - val paramOptions = validTypes.associateWith { type -> + val paramOptions = validTypes.associateWithTo(linkedMapOf()) { type -> val targetParam = psiParams[index ?: argTypes.indexOf(type)] val psiType = targetParam?.type ?: type.toPsiType(elementFactory) sanitizedParameter(psiType, targetParam?.name) diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt index 1425753fc..3d59bd1ab 100644 --- a/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt @@ -157,7 +157,7 @@ class ModifyConstantHandler : InsnInjectorAnnotationHandler() { private fun makeSignatures(type: PsiType, trailingParams: List): ModifierSignatures { return ModifierSignatures( - mapOf(Type.getType(type.descriptor) to sanitizedParameter(type, "constant")), + linkedMapOf(Type.getType(type.descriptor) to sanitizedParameter(type, "constant")), allowCoerce = true, trailingParams = trailingParams, ) diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt index dc5b73388..261eafbff 100644 --- a/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt @@ -54,7 +54,7 @@ class ModifyVariableHandler : InsnInjectorAnnotationHandler() { val info = LocalInfo.fromAnnotation(localType, annotation) val elementFactory = JavaPsiFacade.getElementFactory(annotation.project) - val result = mutableMapOf() + val result = linkedMapOf() val matchedLocals = info.matchLocals( module, targetClass, targetMethod, targetInsn.insn, CollectVisitor.Mode.SUGGESTION, matchType = false diff --git a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt index 9087c4f8d..8df837384 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt @@ -62,7 +62,7 @@ interface MethodSignatures { } class ModifierSignatures( - val paramOptions: Map, + val paramOptions: SequencedMap, val allowCoerce: Boolean, val fullParams: List? = null, val trailingParams: List = emptyList(), @@ -149,7 +149,11 @@ data class GeneralSignatures( returnTypeOptions.getValue(returnKind), allowCoerceRequired = allowCoerce, trailingParams = trailingParams, - intLikeTypes = intLikePositions, + intLikeTypes = if (returnKind == TypeKind.INT_LIKE) { + intLikePositions + } else { + intLikePositions - MethodSignature.TypePosition.Return + }, ) } diff --git a/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt index fe295762b..da0eb7bf8 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt @@ -129,7 +129,10 @@ data class SuggestedSignature( return SuggestedSignature(listOf(Param(name, type)), type) } - fun operationWrapper(annotation: PsiAnnotation, signatures: List): SuggestedSignature? { + fun operationWrapper( + annotation: PsiAnnotation, + signatures: List, + ): SuggestedSignature? { return intersectCoerce(annotation, signatures.asSequence().map { exact(it.signature) }) } @@ -172,22 +175,23 @@ data class SuggestedSignature( if (signature.allowCoerce) { continue } + val specific = signature.specificSignature(returnKind) // Match strictly val returnTypeMatches = checkCoerce( - signature.returnTypeOptions.getValue(returnKind), + specific.returnType, intersected.returnType, intersected.coerceReturnType, - MethodSignature.TypePosition.Return in signature.intLikePositions, + MethodSignature.TypePosition.Return in specific.intLikeTypes, ) if (!returnTypeMatches) { return null } - val paramsMatch = signature.params.withIndex().all { (index, param) -> + val paramsMatch = specific.requiredParams.withIndex().all { (index, param) -> checkCoerce( param.type, intersected.params[index].type, coerce = false, - MethodSignature.TypePosition.Param(index) in signature.intLikePositions, + MethodSignature.TypePosition.Param(index) in specific.intLikeTypes, ) } if (!paramsMatch) { @@ -198,7 +202,10 @@ data class SuggestedSignature( return intersected } - private fun intersectCoerce(annotation: PsiAnnotation, signatures: Sequence): SuggestedSignature? { + private fun intersectCoerce( + annotation: PsiAnnotation, + signatures: Sequence, + ): SuggestedSignature? { val manager = PsiManager.getInstance(annotation.project) return signatures.reduceOrNull { acc, it -> acc?.intersectCoerce(it, manager) } } From 74c91919d1e941d661c2846fdd4d5d02b7f6fa54 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Sun, 20 Sep 2026 13:28:51 +0100 Subject: [PATCH 10/51] fix general intersection --- .../inspection/injector/SuggestedSignature.kt | 80 ++++++++++--------- 1 file changed, 44 insertions(+), 36 deletions(-) diff --git a/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt index da0eb7bf8..63d176851 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt @@ -155,51 +155,58 @@ data class SuggestedSignature( } fun general(annotation: PsiAnnotation, signatures: List): SuggestedSignature? { - val signaturesByReturnKind = signatures.asSequence().flatMap { sig -> - sig.returnTypeOptions.keys.asSequence().map { it to sig } + 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 }) - val returnKind = - signaturesByReturnKind.entries.firstOrNull { it.value.size == signatures.size }?.key ?: return null - val numParams = signatures.maxOf { it.params.size } - val intersected = intersectCoerce( - annotation, - signatures.asSequence() - .map { - val signature = it.specificSignature(returnKind) - exact(signature, takeTrailing = numParams - signature.requiredParams.size) - }, - ) ?: return null - - for (signature in signatures) { - if (signature.allowCoerce) { + for (candidates in candidatesByReturnKind.values) { + if (candidates.size < signatures.size) { + // Not viable continue } - val specific = signature.specificSignature(returnKind) - // Match strictly - val returnTypeMatches = checkCoerce( - specific.returnType, - intersected.returnType, - intersected.coerceReturnType, - MethodSignature.TypePosition.Return in specific.intLikeTypes, - ) - if (!returnTypeMatches) { - return null - } - val paramsMatch = specific.requiredParams.withIndex().all { (index, param) -> - checkCoerce( - param.type, - intersected.params[index].type, + + val intersected = intersectCoerce( + annotation, + candidates.asSequence() + .map { + exact(it, takeTrailing = numParams - it.requiredParams.size) + }, + ) ?: return null + + for (signature in candidates) { + if (signature.allowCoerceRequired) { + continue + } + + // Match strictly + val returnTypeMatches = checkCoerce( + signature.returnType, + intersected.returnType, coerce = false, - MethodSignature.TypePosition.Param(index) in specific.intLikeTypes, + MethodSignature.TypePosition.Return in signature.intLikeTypes, ) - } - if (!paramsMatch) { - return null + if (!returnTypeMatches) { + break + } + + val paramsMatch = signature.requiredParams.withIndex().all { (index, param) -> + checkCoerce( + param.type, + intersected.params[index].type, + coerce = false, + MethodSignature.TypePosition.Param(index) in signature.intLikeTypes, + ) + } + if (!paramsMatch) { + break + } + + return intersected } } - return intersected + return null } private fun intersectCoerce( @@ -242,6 +249,7 @@ private fun getSupertype( isIntLike = intLikeAssignment == null && bIsIntLike, coerce = intLikeAssignment != null && !bIsIntLike && intLikeAssignment != b, ) + bIsIntLike -> TypeMergeResult(a, isIntLike = false, coerce = false) a == b -> TypeMergeResult(a, isIntLike = false, coerce = false) else -> TypeMergeResult(PsiTypes.intType(), isIntLike = false, coerce = true) From 8a198859800faeb53a2144a821c2d1837232e09e Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Sun, 20 Sep 2026 16:57:05 +0100 Subject: [PATCH 11/51] stash --- .../inspection/injector/MethodSignature.kt | 76 ++++++++++++------- .../inspection/injector/SuggestedSignature.kt | 43 +++-------- 2 files changed, 61 insertions(+), 58 deletions(-) diff --git a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt index 0ad860352..29890266c 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt @@ -26,7 +26,6 @@ import com.demonwav.mcdev.platform.mixin.util.isMixinExtrasSugar import com.demonwav.mcdev.util.Parameter import com.demonwav.mcdev.util.allEqual import com.intellij.psi.PsiMethod -import com.intellij.psi.PsiParameter import com.intellij.psi.PsiType import com.intellij.psi.PsiTypeElement @@ -39,14 +38,56 @@ data class MethodSignature( val intLikeTypes: Set = emptySet() ) { fun matches(method: PsiMethod): Boolean { - val returnType = method.returnType ?: return false - val parameters = method.parameterList.parameters.dropLastWhile { it.isMixinExtrasSugar } + return matches( + method.parameterList.parameters.dropLastWhile { it.isMixinExtrasSugar }, + method.returnType ?: return false, + method.hasAnnotation(COERCE), + { it.type }, + { it.hasAnnotation(COERCE) }, + ) + } - return intLikeTypes.asSequence().map { it.getElement(method)?.type }.allEqual() - && matchReturnType(returnType, method.hasAnnotation(COERCE)) - && parameters.size in requiredParams.size..requiredParams.size + trailingParams.size - && matchParams(requiredParams, parameters, allowCoerceRequired, 0) - && matchParams(trailingParams, parameters, allowCoerceTrailing, requiredParams.size) + fun matches(suggested: SuggestedSignature): Boolean { + return matches( + suggested.params, + suggested.returnType, + suggested.coerceReturnType, + { it.type }, + { it.coerce }, + ) + } + + private fun matches( + params: List, + returnType: PsiType, + returnCoerce: Boolean, + paramType: (ParamT) -> PsiType, + paramCoerce: (ParamT) -> Boolean, + ): Boolean { + 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), + allowCoerce && paramCoerce(actual), + TypePosition.Param(index), + ) + } + } + + return params.size in requiredParams.size..requiredParams.size + trailingParams.size + && intLikeTypes.asSequence().mapNotNull { pos -> + when (pos) { + TypePosition.Return -> returnType + is TypePosition.Param -> params.getOrNull(pos.index)?.let(paramType) + } + }.allEqual() + && matchReturnType(returnType, returnCoerce) + && matchParams(requiredParams, allowCoerceRequired, 0) + && matchParams(trailingParams, allowCoerceTrailing, requiredParams.size) } sealed interface TypePosition : Comparable { @@ -68,25 +109,6 @@ data class MethodSignature( private fun matchReturnType(returnType: PsiType, hasCoerce: Boolean): Boolean = matchType(returnType, this.returnType, allowCoerceRequired && hasCoerce, TypePosition.Return) - private fun matchParams( - expectedParams: List, - actualParams: List, - allowCoerce: Boolean, - startIndex: Int, - ): Boolean { - return expectedParams.asSequence() - .zip(actualParams.asSequence().withIndex().drop(startIndex)) - .all { (expected, indexAndActual) -> - val (index, actual) = indexAndActual - matchType( - expected.type, - actual.type, - allowCoerce && actual.hasAnnotation(COERCE), - TypePosition.Param(index), - ) - } - } - private fun matchType(expected: PsiType, actual: PsiType, coerce: Boolean, typePos: TypePosition) = checkCoerce(expected, actual, coerce, typePos in intLikeTypes) } diff --git a/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt index 63d176851..32b962b5e 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt @@ -156,11 +156,15 @@ data class SuggestedSignature( 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 }) - for (candidates in candidatesByReturnKind.values) { + 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 @@ -174,34 +178,11 @@ data class SuggestedSignature( }, ) ?: return null - for (signature in candidates) { - if (signature.allowCoerceRequired) { - continue - } - - // Match strictly - val returnTypeMatches = checkCoerce( - signature.returnType, - intersected.returnType, - coerce = false, - MethodSignature.TypePosition.Return in signature.intLikeTypes, - ) - if (!returnTypeMatches) { - break - } - - val paramsMatch = signature.requiredParams.withIndex().all { (index, param) -> - checkCoerce( - param.type, - intersected.params[index].type, - coerce = false, - MethodSignature.TypePosition.Param(index) in signature.intLikeTypes, - ) - } - if (!paramsMatch) { - break - } + val actuallyValid = candidates.all { + it.allowCoerceRequired || it.matches(intersected) + } + if (actuallyValid) { return intersected } } From a58bcd46f4b7c5ea72461174f1fa7108febfc49f Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Sun, 20 Sep 2026 17:04:45 +0100 Subject: [PATCH 12/51] fix tests --- .../mixin/invalidInjectorMethodSignature/modifyArgs.after.java | 1 - .../mixin/invalidInjectorMethodSignature/modifyArgs.java | 1 - .../simpleMethodWithInnerType.after.java | 1 - .../simpleMethodWithInnerType.java | 1 - 4 files changed, 4 deletions(-) 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.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.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.java index bc1e9920c..e6ff5982d 100644 --- a/src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/modifyArgs.java +++ b/src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/modifyArgs.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/simpleMethodWithInnerType.after.java b/src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/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/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/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/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 { From 1015633c14f23eb8c05cf731496dccca417c3e65 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Sun, 20 Sep 2026 20:24:50 +0100 Subject: [PATCH 13/51] return type only suggestions --- .../mixin/handlers/ModifyArgsHandler.kt | 3 +- ...nvalidInjectorMethodSignatureInspection.kt | 25 +-- .../inspection/injector/MethodSignature.kt | 74 ++++++--- .../inspection/injector/SuggestedSignature.kt | 154 ++++++++++++++++-- 4 files changed, 206 insertions(+), 50 deletions(-) diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyArgsHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyArgsHandler.kt index 9b6e0684d..2157606ed 100644 --- a/src/main/kotlin/platform/mixin/handlers/ModifyArgsHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/ModifyArgsHandler.kt @@ -24,6 +24,7 @@ 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.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 @@ -76,7 +77,7 @@ class ModifyArgsHandler : InsnInjectorAnnotationHandler() { val argsType = JavaPsiFacade.getElementFactory(annotation.project) .createTypeByFQClassName(ARGS, annotation.resolveScope) return SuggestedSignature( - listOf(SuggestedSignature.Param("args", argsType)), + listOf(SignatureSuggestion.Param("args", argsType)), PsiTypes.voidType(), ) } diff --git a/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt b/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt index 3a28e1827..ecea833ee 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt @@ -180,9 +180,12 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() { return } - val suggestedSignature = handler.suggestedMethodSignature(annotation, targetMethods)?.let(::lazyOf) + val signatureOptions = + handler.expectedMethodSignatures(annotation, targetMethods).collectSignatures() + val signatureSuggestion = signatureOptions?.let { SuggestedReturnType.forParams(parameters, it) } + ?: handler.suggestedMethodSignature(annotation, targetMethods) - if (suggestedSignature == null) { + if (signatureSuggestion == null) { holder.registerProblem( parameters, "There are no possible signatures for this injector", @@ -191,7 +194,7 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() { val annotationName = annotation.nameReferenceElement?.referenceName val description = "Method signature does not match expected signature for $annotationName" - val quickFix = SignatureQuickFix(method, suggestedSignature) + val quickFix = SignatureQuickFix(method, signatureSuggestion) val declarationStart = (method.returnTypeElement ?: identifier).startOffsetInParent val declarationEnd = method.parameterList.textRangeInParent.endOffset holder.registerProblem( @@ -207,10 +210,9 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() { private class SignatureQuickFix( method: PsiMethod, - suggestedSignature: Lazy, + @SafeFieldForPreview + private val signatureSuggestion: SignatureSuggestion, ) : LocalQuickFixAndIntentionActionOnPsiElement(method) { - @delegate:SafeFieldForPreview - private val suggestedSignature by suggestedSignature private val fixName = "Fix method signature" @@ -249,6 +251,7 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() { } private fun fixParameters(project: Project, parameters: PsiParameterList, preview: Boolean) { + 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 @@ -264,7 +267,7 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() { val languageLevel = PsiUtil.getLanguageLevel(parameters) val usedNames = mutableSetOf() - val newParams = suggestedSignature.params.mapIndexedTo(mutableListOf()) { i, p -> + val newParams = suggestedParams.mapIndexedTo(mutableListOf()) { i, p -> val paramName = p.name?.takeIf { name -> nameHelper.isIdentifier(name, languageLevel) } ?: VariableNameGenerator(parameters, VariableKind.PARAMETER) .byType(p.type) @@ -290,7 +293,7 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() { } private fun fixReturnType(method: PsiMethod, editor: Editor, file: PsiFile, preview: Boolean) { - val fix = QuickFixFactory.getInstance().createMethodReturnFix(method, suggestedSignature.returnType, false) + val fix = QuickFixFactory.getInstance().createMethodReturnFix(method, signatureSuggestion.returnType, false) if (preview) { fix.generatePreview(file.project, editor, file) } else { @@ -300,7 +303,7 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() { private fun fixCoerce(project: Project, method: PsiMethod, preview: Boolean) { val existingCoerce = method.modifierList.findAnnotation(COERCE) - val needsCoerce = suggestedSignature.coerceReturnType + val needsCoerce = signatureSuggestion.coerceReturnType val returnTypeElement = method.returnTypeElement!! val fixCoerce: () -> Unit = when { @@ -325,13 +328,13 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() { } private fun fixIntLikeTypes(project: Project, method: PsiMethod, editor: Editor, preview: Boolean) { - if (preview || suggestedSignature.intLikeTypes.isEmpty()) { + if (preview || signatureSuggestion.intLikeTypes.isEmpty()) { return } runWriteAction { PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(editor.document) - val template = makeIntLikeTypeTemplate(method, suggestedSignature.intLikeTypes) + val template = makeIntLikeTypeTemplate(method, signatureSuggestion.intLikeTypes) if (template != null) { editor.caretModel.moveToOffset(method.startOffset) TemplateManager.getInstance(method.project) diff --git a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt index 29890266c..5cfef0488 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt @@ -26,6 +26,7 @@ import com.demonwav.mcdev.platform.mixin.util.isMixinExtrasSugar import com.demonwav.mcdev.util.Parameter import com.demonwav.mcdev.util.allEqual import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiParameterList import com.intellij.psi.PsiType import com.intellij.psi.PsiTypeElement @@ -37,6 +38,22 @@ data class MethodSignature( val allowCoerceTrailing: Boolean = true, val intLikeTypes: Set = emptySet() ) { + sealed interface TypePosition : Comparable { + fun getElement(method: PsiMethod): PsiTypeElement? + + data object Return : TypePosition { + override fun getElement(method: PsiMethod) = method.returnTypeElement + + override fun compareTo(other: TypePosition): Int = if (other is Return) 0 else -1 + } + + data class Param(val index: Int) : TypePosition { + override fun getElement(method: PsiMethod) = method.parameterList.parameters[index].typeElement + + override fun compareTo(other: TypePosition): Int = if (other is Param) index.compareTo(other.index) else 1 + } + } + fun matches(method: PsiMethod): Boolean { return matches( method.parameterList.parameters.dropLastWhile { it.isMixinExtrasSugar }, @@ -57,12 +74,40 @@ data class MethodSignature( ) } + fun matchesParams(params: PsiParameterList): Boolean { + return matchesParams( + params.parameters.dropLastWhile { it.isMixinExtrasSugar }, + { it.type }, + { it.hasAnnotation(COERCE) }, + ) + } + + fun matchesReturnType(returnType: PsiType, hasCoerce: Boolean): Boolean = + matchType(this.returnType, returnType, allowCoerceRequired && hasCoerce, TypePosition.Return) + private fun matches( params: List, returnType: PsiType, returnCoerce: Boolean, paramType: (ParamT) -> PsiType, paramCoerce: (ParamT) -> Boolean, + ): Boolean { + val intLikeMismatch = TypePosition.Return in intLikeTypes + && intLikeTypes.asSequence() + .filterIsInstance() + .mapNotNull { params.getOrNull(it.index) } + .take(1) + .any { paramType(it) != returnType } + + return !intLikeMismatch + && matchesReturnType(returnType, returnCoerce) + && matchesParams(params, paramType, paramCoerce) + } + + private fun matchesParams( + params: List, + paramType: (ParamT) -> PsiType, + paramCoerce: (ParamT) -> Boolean, ): Boolean { fun matchParams(expected: List, allowCoerce: Boolean, startIndex: Int): Boolean { return expected.asSequence() @@ -79,36 +124,13 @@ data class MethodSignature( } return params.size in requiredParams.size..requiredParams.size + trailingParams.size - && intLikeTypes.asSequence().mapNotNull { pos -> - when (pos) { - TypePosition.Return -> returnType - is TypePosition.Param -> params.getOrNull(pos.index)?.let(paramType) - } - }.allEqual() - && matchReturnType(returnType, returnCoerce) + && intLikeTypes.asSequence().filterIsInstance().mapNotNull { (index) -> + params.getOrNull(index)?.let(paramType) + }.allEqual() && matchParams(requiredParams, allowCoerceRequired, 0) && matchParams(trailingParams, allowCoerceTrailing, requiredParams.size) } - sealed interface TypePosition : Comparable { - fun getElement(method: PsiMethod): PsiTypeElement? - - data object Return : TypePosition { - override fun getElement(method: PsiMethod) = method.returnTypeElement - - override fun compareTo(other: TypePosition): Int = if (other is Return) 0 else -1 - } - - data class Param(val index: Int) : TypePosition { - override fun getElement(method: PsiMethod) = method.parameterList.parameters[index].typeElement - - override fun compareTo(other: TypePosition): Int = if (other is Param) index.compareTo(other.index) else 1 - } - } - - private fun matchReturnType(returnType: PsiType, hasCoerce: Boolean): Boolean = - matchType(returnType, this.returnType, allowCoerceRequired && hasCoerce, TypePosition.Return) - private fun matchType(expected: PsiType, actual: PsiType, coerce: Boolean, typePos: TypePosition) = checkCoerce(expected, actual, coerce, typePos in intLikeTypes) } diff --git a/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt index 32b962b5e..73761130b 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt @@ -21,25 +21,155 @@ package com.demonwav.mcdev.platform.mixin.inspection.injector import com.demonwav.mcdev.platform.mixin.util.TypeKind -import com.demonwav.mcdev.platform.mixin.util.checkCoerce import com.demonwav.mcdev.util.Parameter import com.demonwav.mcdev.util.allEqual import com.demonwav.mcdev.util.normalize import com.demonwav.mcdev.util.sharedPrefixLength import com.intellij.psi.GenericsUtil import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiElement import com.intellij.psi.PsiManager +import com.intellij.psi.PsiParameterList import com.intellij.psi.PsiType import com.intellij.psi.PsiTypes -data class SuggestedSignature( - val params: List, - val returnType: PsiType, - val intLikeTypes: Set = emptySet(), - val coerceReturnType: Boolean = false, -) { +sealed interface SignatureSuggestion { + val params: List? + val returnType: PsiType + val intLikeTypes: Set + 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, + override val coerceReturnType: Boolean = false, +) : SignatureSuggestion { + override val intLikeTypes = if (returnTypeIsIntLike) setOf(MethodSignature.TypePosition.Return) else emptySet() + override val params: Nothing? get() = null + + fun intersectCoerce(other: SuggestedReturnType, manager: PsiManager): SuggestedReturnType? { + if (TypeKind.of(this.returnType) != TypeKind.of(other.returnType)) { + return null + } + val (returnType, returnTypeIsIntLike, coerceReturnType) = getSupertype( + manager, + this.returnType, + other.returnType, + this.returnTypeIsIntLike, + other.returnTypeIsIntLike, + intLikeAssignment = null, + ) + + return SuggestedReturnType(returnType, returnTypeIsIntLike, coerceReturnType) + } + + companion object { + fun exact(signature: MethodSignature): SuggestedReturnType { + return SuggestedReturnType( + signature.returnType, + MethodSignature.TypePosition.Return in signature.intLikeTypes, + ) + } + + fun forParams(params: PsiParameterList, signatures: List): SuggestedReturnType? { + val validSignatures = signatures.map { expected -> + expected.options.filter { it.matchesParams(params) }.ifEmpty { return null } + } + + var returnTypeOptions: MutableSet? = null + outer@ for (options in validSignatures) { + val ourReturnTypeOptions = options.mapTo(mutableSetOf()) { + it.forcedReturnType(params)?.normalize() ?: continue@outer + } + if (returnTypeOptions == null) { + returnTypeOptions = ourReturnTypeOptions + } else { + returnTypeOptions.retainAll(ourReturnTypeOptions) + } + } + + if (returnTypeOptions == null) { + // Can choose whatever we like + val optionsByKind = validSignatures.asSequence() + .flatMap { options -> + options.asSequence() + .filter { it.forcedReturnType(params) == null } + .distinctBy { TypeKind.of(it.returnType) } + } + .groupBy { TypeKind.of(it.returnType) } + val chosen = + optionsByKind.entries.firstOrNull { it.value.size == signatures.size }?.value ?: return null + return intersectReturnTypes(params, chosen.asSequence().map { exact(it) }) + } + + outer@ for (returnType in returnTypeOptions) { + val choices = validSignatures.map { signatures -> + signatures.firstNotNullOfOrNull { signature -> + val forcedReturnType = signature.forcedReturnType(params) + if (forcedReturnType != null) { + if (forcedReturnType.normalize() == returnType) { + SuggestedReturnType( + forcedReturnType, + returnTypeIsIntLike = false, + ) + } else { + null + } + } else { + // Here, the signature definitely allows coercion, or it would force a return type. + // Additionally, if the signature's return type is "int-like", we must have free choice + // since none of its parameters forced the return type + if (signature.matchesReturnType(returnType, hasCoerce = true)) { + SuggestedReturnType( + signature.returnType, + returnTypeIsIntLike = MethodSignature.TypePosition.Return in signature.intLikeTypes, + ) + } else { + null + } + } + } ?: continue@outer + } + return intersectReturnTypes(params, choices.asSequence()) + } + + return null + } + + private fun MethodSignature.forcedReturnType(params: PsiParameterList): PsiType? { + if (MethodSignature.TypePosition.Return in intLikeTypes) { + val params = params.parameters + return intLikeTypes.firstNotNullOfOrNull { + when (it) { + is MethodSignature.TypePosition.Param -> params.getOrNull(it.index)?.type + is MethodSignature.TypePosition.Return -> null + } + } + } + + return returnType.takeUnless { allowCoerceRequired } + } + + private fun intersectReturnTypes( + context: PsiElement, + types: Sequence, + ): SuggestedReturnType? { + val manager = PsiManager.getInstance(context.project) + return types.reduceOrNull { acc, it -> acc?.intersectCoerce(it, manager) } + } + } +} + +data class SuggestedSignature( + override val params: List, + override val returnType: PsiType, + override val intLikeTypes: Set = emptySet(), + override val coerceReturnType: Boolean = false, +) : SignatureSuggestion { fun intersectCoerce(other: SuggestedSignature, manager: PsiManager): SuggestedSignature? { if (!kindsMatch(this, other)) { return null @@ -86,7 +216,7 @@ data class SuggestedSignature( intLikeTypes.add(pos) } val name = if (a.name == b.name) a.name else null - Param(name, type, a.coerce || b.coerce || coerce) + SignatureSuggestion.Param(name, type, a.coerce || b.coerce || coerce) }, returnType, intLikeTypes, @@ -103,7 +233,7 @@ data class SuggestedSignature( fun exact(signature: MethodSignature, takeTrailing: Int = 0): SuggestedSignature { return SuggestedSignature( (signature.requiredParams + signature.trailingParams.take(takeTrailing)).map { - Param( + SignatureSuggestion.Param( it.name, it.type, ) @@ -126,7 +256,7 @@ data class SuggestedSignature( val type = chosenParams.asSequence().map { it.type } .reduce { a, b -> GenericsUtil.getLeastUpperBound(a, b, psiManager) ?: a } - return SuggestedSignature(listOf(Param(name, type)), type) + return SuggestedSignature(listOf(SignatureSuggestion.Param(name, type)), type) } fun operationWrapper( @@ -191,10 +321,10 @@ data class SuggestedSignature( } private fun intersectCoerce( - annotation: PsiAnnotation, + context: PsiElement, signatures: Sequence, ): SuggestedSignature? { - val manager = PsiManager.getInstance(annotation.project) + val manager = PsiManager.getInstance(context.project) return signatures.reduceOrNull { acc, it -> acc?.intersectCoerce(it, manager) } } } From 90a2214c50c5dea849abe278befa77bb31ca00fa Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Sun, 20 Sep 2026 20:35:05 +0100 Subject: [PATCH 14/51] prioritise existing return type in modifier suggestions --- .../mixin/inspection/injector/SuggestedSignature.kt | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt index 73761130b..a70a6407a 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt @@ -23,15 +23,19 @@ package com.demonwav.mcdev.platform.mixin.inspection.injector import com.demonwav.mcdev.platform.mixin.util.TypeKind import com.demonwav.mcdev.util.Parameter import com.demonwav.mcdev.util.allEqual +import com.demonwav.mcdev.util.descriptor import com.demonwav.mcdev.util.normalize import com.demonwav.mcdev.util.sharedPrefixLength 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? @@ -250,7 +254,12 @@ data class SuggestedSignature( val optionsByType = parameterOptions.asSequence() .flatMap { it.entries } .groupBy({ it.key }, { it.value }) - val chosenParams = optionsByType.values.firstOrNull { it.size == parameterOptions.size } ?: return null + + 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 }.distinct().singleOrNull() ?: "original" val type = chosenParams.asSequence().map { it.type } From b8dce64c93df21f84d76b025135345dec2b793d3 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Mon, 21 Sep 2026 00:18:10 +0100 Subject: [PATCH 15/51] restore BeforeConstant's behaviour of ensuring only 1 discriminator is specified, and supporting wildcard matches --- .../injectionPoint/ConstantInjectionPoint.kt | 194 +++++++++++------- 1 file changed, 118 insertions(+), 76 deletions(-) diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantInjectionPoint.kt index 840b8d7ef..168943ad6 100644 --- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantInjectionPoint.kt +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantInjectionPoint.kt @@ -130,21 +130,34 @@ class ConstantInjectionPoint : InjectionPoint() { override fun getArgValueListDelimiter(at: PsiAnnotation, key: String) = Const.COMMA_LIST_DELIMITER.takeIf { key == "expandZeroConditions" } - fun getConstantInfos(at: PsiAnnotation): List { + fun getConstantInfo(at: PsiAnnotation): ConstantInfo? { val args = AtResolver.getArgs(at) + val nullValue = args["nullValue"]?.let(java.lang.Boolean::parseBoolean) ?: false val intValue = args["intValue"]?.toIntOrNull() val floatValue = args["floatValue"]?.toFloatOrNull() val longValue = args["longValue"]?.toLongOrNull() val doubleValue = args["doubleValue"]?.toDoubleOrNull() val stringValue = args["stringValue"] val classValue = args["classValue"]?.ifNotBlank { Type.getObjectType(it.replace('.', '/')) } - val nullValue = args["nullValue"]?.toBooleanStrictOrNull() - val expandZeroConditions = parseExpandConditions(args) + val count = + nullValue.toInt() + + (intValue != null).toInt() + + (floatValue != null).toInt() + + (longValue != null).toInt() + + (doubleValue != null).toInt() + + (stringValue != null).toInt() + + (classValue != null).toInt() + if (count != 1) { + return null + } + + val constant = if (nullValue) { + null + } else { + intValue ?: floatValue ?: longValue ?: doubleValue ?: stringValue ?: classValue!! + } - return listOfNotNull( - intValue, floatValue, longValue, doubleValue, stringValue, classValue - ).map { ConstantInfo(it, expandZeroConditions) } + - if (nullValue == true) listOf(ConstantInfo(null, expandZeroConditions)) else emptyList() + return ConstantInfo(constant, parseExpandConditions(args)) } private fun parseExpandConditions(args: Map): Set { @@ -155,12 +168,16 @@ class ConstantInjectionPoint : InjectionPoint() { ?.toSet() ?: emptySet() } + private fun Boolean.toInt(): Int { + return if (this) 1 else 0 + } + override fun createNavigationVisitor( at: PsiAnnotation, target: MixinSelector?, targetClass: PsiClass, ): NavigationVisitor { - return MyNavigationVisitor(getConstantInfos(at)) + return MyNavigationVisitor(getConstantInfo(at)) } override fun doCreateCollectVisitor( @@ -169,7 +186,7 @@ class ConstantInjectionPoint : InjectionPoint() { targetClass: ClassNode, mode: CollectVisitor.Mode, ): CollectVisitor { - return MyCollectVisitor(at.project, mode, getConstantInfos(at)) + return MyCollectVisitor(at.project, mode, getConstantInfo(at)) } override fun createLookup( @@ -193,7 +210,7 @@ class ConstantInjectionPoint : InjectionPoint() { } class MyNavigationVisitor( - private val constantInfos: List, + private val constantInfo: ConstantInfo?, private val expectedType: Type? = null, ) : NavigationVisitor() { override fun visitForeachStatement(statement: PsiForeachStatement) { @@ -228,96 +245,121 @@ class ConstantInjectionPoint : InjectionPoint() { } private fun visitConstant(element: PsiElement, value: Any?) { - for (constantInfo in constantInfos) { - if (value != constantInfo.constant) { - continue - } - - if (expectedType != null && value != null) { - // First check if we expect any String literal - if (value is String && - (expectedType.sort != Type.OBJECT || expectedType.className != CommonClassNames.JAVA_LANG_STRING) - ) { - continue - } + if (constantInfo != null && value != constantInfo.constant) { + return + } - // then check if we expect any class literal - if (value is Type && ( - expectedType.sort != Type.ARRAY && expectedType.sort != Type.OBJECT || - expectedType.className != CommonClassNames.JAVA_LANG_CLASS - ) - ) { - continue - } + if (expectedType != null && value != null) { + // First check if we expect any String literal + if (value is String && + (expectedType.sort != Type.OBJECT || expectedType.className != CommonClassNames.JAVA_LANG_STRING) + ) { + return + } - // otherwise we expect a primitive literal - if (expectedType.sort in Type.BOOLEAN..Type.DOUBLE && - value::class.javaPrimitiveType?.let(Type::getType) != expectedType - ) { - continue - } + // then check if we expect any class literal + if (value is Type && ( + expectedType.sort != Type.ARRAY && expectedType.sort != Type.OBJECT || + expectedType.className != CommonClassNames.JAVA_LANG_CLASS + ) + ) { + return } - val parent = PsiUtil.skipParenthesizedExprUp(element.parent) - - // check for expandZeroConditions - if (value == null || value == 0) { - if (parent is PsiBinaryExpression) { - val operation = parent.operationTokenType - if (operation == JavaTokenType.EQEQ || operation == JavaTokenType.NE) { - continue - } - val opcode = when (operation) { - JavaTokenType.LT -> Opcodes.IFLT - JavaTokenType.LE -> Opcodes.IFLE - JavaTokenType.GT -> Opcodes.IFGT - JavaTokenType.GE -> Opcodes.IFGE - else -> null - } - if (opcode != null && ( - !constantInfo.expandConditions.any { opcode in it.opcodes } - ) - ) { - continue - } - } + // otherwise we expect a primitive literal + if (expectedType.sort in Type.BOOLEAN..Type.DOUBLE && + value::class.javaPrimitiveType?.let(Type::getType) != expectedType + ) { + return } + } - // check for switch statement (compiles to tableswitch or lookupswitch which aren't targeted) - if (parent is PsiSwitchLabelStatementBase) { - continue + val parent = PsiUtil.skipParenthesizedExprUp(element.parent) + + // check for expandZeroConditions + if (value == null || value == 0) { + if (parent is PsiBinaryExpression) { + val operation = parent.operationTokenType + if (operation == JavaTokenType.EQEQ || operation == JavaTokenType.NE) { + return + } + val opcode = when (operation) { + JavaTokenType.LT -> Opcodes.IFLT + JavaTokenType.LE -> Opcodes.IFLE + JavaTokenType.GT -> Opcodes.IFGT + JavaTokenType.GE -> Opcodes.IFGE + else -> null + } + if (opcode != null && ( + constantInfo == null || + !constantInfo.expandConditions.any { opcode in it.opcodes } + ) + ) { + return + } } + } - addResult(element) + // check for switch statement (compiles to tableswitch or lookupswitch which aren't targeted) + if (parent is PsiSwitchLabelStatementBase) { + return } + + addResult(element) } } class MyCollectVisitor( private val project: Project, mode: Mode, - private val constantInfos: List, + private val constantInfo: ConstantInfo?, + private val expectedType: Type? = null, ) : CollectVisitor(mode) { override fun accept(methodNode: MethodNode) = sequence { for (insn in methodNode.instructions ?: emptyList()) { - for (constantInfo in constantInfos) { - val constant = ( - insn.computeConstantValue(constantInfo.expandConditions) - ?: continue - ).let { if (it is NullSentinel) null else it } + val constant = ( + insn.computeConstantValue(constantInfo?.expandConditions ?: emptySet()) + ?: continue + ).let { if (it is NullSentinel) null else it } + + if (constantInfo != null && constant != constantInfo.constant) { + continue + } - if (constant != constantInfo.constant) { + if (expectedType != null && constant != null) { + // First check if we expect any String literal + if (constant is String && ( + expectedType.sort != Type.OBJECT || + expectedType.className != CommonClassNames.JAVA_LANG_STRING + ) + ) { + continue + } + + // then check if we expect any class literal + if (constant is Type && ( + expectedType.sort != Type.ARRAY && expectedType.sort != Type.OBJECT || + expectedType.className != CommonClassNames.JAVA_LANG_CLASS + ) + ) { continue } - val elementFactory = JavaPsiFacade.getElementFactory(project) - val literal = if (constant is Type) { - elementFactory.createExpressionFromText("${constant.className}.class", null) - } else { - elementFactory.createLiteralExpression(constant) + // otherwise we expect a primitive literal + if (expectedType.sort in Type.BOOLEAN..Type.DOUBLE && + constant::class.javaPrimitiveType?.let(Type::getType) != expectedType + ) { + continue } - addResult(insn, literal) } + + val elementFactory = JavaPsiFacade.getElementFactory(project) + val literal = if (constant is Type) { + elementFactory.createExpressionFromText("${constant.className}.class", null) + } else { + elementFactory.createLiteralExpression(constant) + } + addResult(insn, literal) } } } From e2130f0b20640ed7b38470ed19d79bf85d1d56e5 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Mon, 21 Sep 2026 00:27:58 +0100 Subject: [PATCH 16/51] Fix: Support `.class` constants in `@ModifyConstant`. --- .../mixin/handlers/ModifyConstantHandler.kt | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt index 3d59bd1ab..774e9fb07 100644 --- a/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt @@ -45,6 +45,7 @@ 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 } @@ -85,7 +86,7 @@ class ModifyConstantHandler : InsnInjectorAnnotationHandler() { ): ExpectedSignatures<*> { val targetParams = collectTargetMethodParameters(annotation.project, targetClass, targetMethod) val cst = constantInjectionPoint.getTargetedConstant(targetInsn.insn) ?: return ExpectedSignatures.Invalid - return ExpectedSignatures.Valid(expectedSignatures(annotation, cst, targetParams)) + return ExpectedSignatures.Valid(expectedSignatures(annotation, targetInsn.insn, cst, targetParams)) } override fun suggestedMethodSignature( @@ -115,10 +116,15 @@ class ModifyConstantHandler : InsnInjectorAnnotationHandler() { } } - private fun expectedSignatures(annotation: PsiAnnotation, cst: Any, trailingParams: List): MethodSignatures { + private fun expectedSignatures( + annotation: PsiAnnotation, + targetInsn: AbstractInsnNode, + cst: Any, + trailingParams: List, + ): MethodSignatures { val psiManager = PsiManager.getInstance(annotation.project) - return if (cst is Type) { + return if (targetInsn is TypeInsnNode) { BasicSignatures( makeTypeCheckMethodSignature( psiManager, @@ -149,7 +155,11 @@ class ModifyConstantHandler : InsnInjectorAnnotationHandler() { is Double -> PsiTypes.doubleType() is String -> PsiType.getJavaLangString( PsiManager.getInstance(context.project), - context.resolveScope + context.resolveScope, + ) + is Type -> getClassType( + PsiManager.getInstance(context.project), + context, ) else -> null From 1d86ce129e2d8661dd4da02d333c36637dba0ddf Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Mon, 21 Sep 2026 00:38:02 +0100 Subject: [PATCH 17/51] Switch `@ModifyConstant` to a general signature type. --- .../mixin/handlers/ModifyArgHandler.kt | 1 - .../mixin/handlers/ModifyConstantHandler.kt | 83 ++++++------------- .../mixin/handlers/ModifyVariableHandler.kt | 1 - .../inspection/injector/MethodSignatures.kt | 5 +- 4 files changed, 26 insertions(+), 64 deletions(-) diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt index c65cbff17..5072a4025 100644 --- a/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt @@ -96,7 +96,6 @@ class ModifyArgHandler : InsnInjectorAnnotationHandler() { return ExpectedSignatures.Valid( ModifierSignatures( paramOptions, - allowCoerce = false, fullParams, ) ) diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt index 774e9fb07..a73f9eb21 100644 --- a/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt @@ -23,16 +23,13 @@ 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.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.MethodSignatures -import com.demonwav.mcdev.platform.mixin.inspection.injector.ModifierSignatures +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.descriptor import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiElement @@ -83,7 +80,7 @@ class ModifyConstantHandler : InsnInjectorAnnotationHandler() { targetClass: ClassNode, targetMethod: MethodNode, targetInsn: TargetInsn, - ): ExpectedSignatures<*> { + ): 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)) @@ -93,27 +90,10 @@ class ModifyConstantHandler : InsnInjectorAnnotationHandler() { annotation: PsiAnnotation, targets: List ): SuggestedSignature? { - val isTypeCheck = - resolveInstructions(annotation, targets).asSequence() - .map { it.result.insn.opcode == Opcodes.INSTANCEOF } - .distinct() - .singleOrNull() ?: return null - - return if (isTypeCheck) { - SuggestedSignature.exact( - makeTypeCheckMethodSignature( - PsiManager.getInstance(annotation.project), - annotation, - PsiTypes.booleanType(), - emptyList(), - ) - ) - } else { - SuggestedSignature.modifierNoCoerce( - annotation, - expectedMethodSignatures(annotation, targets).collectSignatures() ?: return null, - ) - } + return SuggestedSignature.general( + annotation, + expectedMethodSignatures(annotation, targets).collectSignatures() ?: return null + ) } private fun expectedSignatures( @@ -121,23 +101,18 @@ class ModifyConstantHandler : InsnInjectorAnnotationHandler() { targetInsn: AbstractInsnNode, cst: Any, trailingParams: List, - ): MethodSignatures { + ): GeneralSignatures { val psiManager = PsiManager.getInstance(annotation.project) return if (targetInsn is TypeInsnNode) { - BasicSignatures( - makeTypeCheckMethodSignature( - psiManager, - annotation, - PsiTypes.booleanType(), - trailingParams, - ), - makeTypeCheckMethodSignature( - psiManager, - annotation, - getClassType(psiManager, annotation), - trailingParams, + GeneralSignatures( + makeTypeCheckParams(psiManager, annotation), + linkedMapOf( + TypeKind.INT_LIKE to PsiTypes.booleanType(), + TypeKind.OBJECT to getClassType(psiManager, annotation), ), + allowCoerce = false, + trailingParams, ) } else { makeSignatures( @@ -165,28 +140,18 @@ class ModifyConstantHandler : InsnInjectorAnnotationHandler() { else -> null } - private fun makeSignatures(type: PsiType, trailingParams: List): ModifierSignatures { - return ModifierSignatures( - linkedMapOf(Type.getType(type.descriptor) to sanitizedParameter(type, "constant")), - allowCoerce = true, - trailingParams = trailingParams, + private fun makeSignatures(type: PsiType, trailingParams: List): GeneralSignatures { + return GeneralSignatures( + listOf(sanitizedParameter(type, "constant")), + type, + trailingParams, ) } - private fun makeTypeCheckMethodSignature( - psiManager: PsiManager, - context: PsiElement, - returnType: PsiType, - trailingParams: List, - ): MethodSignature { - return MethodSignature( - listOf( - sanitizedParameter(PsiType.getJavaLangObject(psiManager, context.resolveScope), "instance"), - sanitizedParameter(getClassType(psiManager, context), "type"), - ), - returnType, - allowCoerceRequired = false, - trailingParams = trailingParams, + 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 261eafbff..717b57483 100644 --- a/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt @@ -69,7 +69,6 @@ class ModifyVariableHandler : InsnInjectorAnnotationHandler() { return ExpectedSignatures.Valid( ModifierSignatures( result, - allowCoerce = false, trailingParams = targetParams, ) ) diff --git a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt index 8df837384..cc62691cc 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt @@ -63,7 +63,6 @@ interface MethodSignatures { class ModifierSignatures( val paramOptions: SequencedMap, - val allowCoerce: Boolean, val fullParams: List? = null, val trailingParams: List = emptyList(), ) : MethodSignatures { @@ -72,14 +71,14 @@ class ModifierSignatures( MethodSignature( listOf(param), param.type, - allowCoerceRequired = allowCoerce, + allowCoerceRequired = false, trailingParams = trailingParams, ), fullParams?.let { MethodSignature( it, param.type, - allowCoerceRequired = allowCoerce, + allowCoerceRequired = false, trailingParams = trailingParams, ) }, From c6125f3a4dd5c1c9cb9d92c6a674615d2e6faf1f Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Mon, 21 Sep 2026 21:55:53 +0100 Subject: [PATCH 18/51] int coercion kinda fixed ish --- .../inspection/injector/SuggestedSignature.kt | 140 ++++++++++++------ src/main/kotlin/platform/mixin/util/Mixin.kt | 4 +- 2 files changed, 97 insertions(+), 47 deletions(-) diff --git a/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt index a70a6407a..cf83e3c2f 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt @@ -21,11 +21,8 @@ package com.demonwav.mcdev.platform.mixin.inspection.injector import com.demonwav.mcdev.platform.mixin.util.TypeKind -import com.demonwav.mcdev.util.Parameter -import com.demonwav.mcdev.util.allEqual import com.demonwav.mcdev.util.descriptor import com.demonwav.mcdev.util.normalize -import com.demonwav.mcdev.util.sharedPrefixLength import com.intellij.psi.GenericsUtil import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiElement @@ -59,14 +56,18 @@ data class SuggestedReturnType( return null } + val aIntLikeAssignment = other.returnType.takeIf { this.returnTypeIsIntLike && !other.returnTypeIsIntLike } + val bIntLikeAssignment = this.returnType.takeIf { other.returnTypeIsIntLike && !this.returnTypeIsIntLike } + val (returnType, returnTypeIsIntLike, coerceReturnType) = getSupertype( manager, this.returnType, other.returnType, this.returnTypeIsIntLike, other.returnTypeIsIntLike, - intLikeAssignment = null, - ) + aIntLikeAssignment, + bIntLikeAssignment, + ) ?: return null return SuggestedReturnType(returnType, returnTypeIsIntLike, coerceReturnType) } @@ -154,6 +155,9 @@ data class SuggestedReturnType( } } } + if (TypeKind.of(returnType) == TypeKind.INT_LIKE && returnType != PsiTypes.intType()) { + return returnType + } return returnType.takeUnless { allowCoerceRequired } } @@ -178,18 +182,8 @@ data class SuggestedSignature( if (!kindsMatch(this, other)) { return null } - val intLikeForcings = mutableSetOf() - for (pos in this.intLikeTypes) { - if (pos !in other.intLikeTypes) { - intLikeForcings.add(other.getType(pos)) - } - } - for (pos in other.intLikeTypes) { - if (pos !in this.intLikeTypes) { - intLikeForcings.add(this.getType(pos)) - } - } - val intLikeAssignment = if (intLikeForcings.size > 1) PsiTypes.intType() else intLikeForcings.singleOrNull() + val aIntLikeAssignment = this.intLikeAssignment(other, onConflict = { return null }) + val bIntLikeAssignment = other.intLikeAssignment(this, onConflict = { return null }) val intLikeTypes = mutableSetOf() @@ -199,8 +193,10 @@ data class SuggestedSignature( other.returnType, MethodSignature.TypePosition.Return in this.intLikeTypes, MethodSignature.TypePosition.Return in other.intLikeTypes, - intLikeAssignment, - ) + aIntLikeAssignment, + bIntLikeAssignment, + ) ?: return null + if (returnTypeIsIntLike) { intLikeTypes.add(MethodSignature.TypePosition.Return) } @@ -214,8 +210,9 @@ data class SuggestedSignature( b.type, pos in this.intLikeTypes, pos in other.intLikeTypes, - intLikeAssignment, - ) + aIntLikeAssignment, + bIntLikeAssignment, + ) ?: return null if (isIntLike) { intLikeTypes.add(pos) } @@ -233,6 +230,20 @@ data class SuggestedSignature( MethodSignature.TypePosition.Return -> returnType } + private inline fun intLikeAssignment(other: SuggestedSignature, onConflict: () -> Nothing): PsiType? { + var result: PsiType? = null + for (pos in this.intLikeTypes) { + if (pos !in other.intLikeTypes) { + result = if (result == null) { + other.getType(pos) + } else { + mergeIntTypes(result, other.getType(pos))?.first ?: onConflict() + } + } + } + return result + } + companion object { fun exact(signature: MethodSignature, takeTrailing: Int = 0): SuggestedSignature { return SuggestedSignature( @@ -276,21 +287,19 @@ data class SuggestedSignature( } fun inject(annotation: PsiAnnotation, signatures: List): SuggestedSignature? { - if (!signatures.asSequence().map { it.params.kinds() }.allEqual()) { - // Shape mismatch, use short form - return intersectCoerce( - annotation, - signatures.asSequence().map { exact(it.shortSignature ?: it.longSignature) }, - ) - } - - val localsToUse = sharedPrefixLength(signatures.map { it.locals.kinds() }) + val localsToUse = + coerciblePrefixLength(signatures.asSequence().map { sig -> sig.locals.asSequence().map { it.type } }) - return intersectCoerce( + val longForm = intersectCoerce( annotation, signatures.asSequence() .map { exact(it.longSignature, takeTrailing = localsToUse) }, ) + + return longForm ?: intersectCoerce( + annotation, + signatures.asSequence().map { exact(it.shortSignature ?: it.longSignature) }, + ) } fun general(annotation: PsiAnnotation, signatures: List): SuggestedSignature? { @@ -315,7 +324,7 @@ data class SuggestedSignature( .map { exact(it, takeTrailing = numParams - it.requiredParams.size) }, - ) ?: return null + ) ?: continue val actuallyValid = candidates.all { it.allowCoerceRequired || it.matches(intersected) @@ -344,16 +353,15 @@ private fun kindsMatch(a: SuggestedSignature, b: SuggestedSignature) = && a.params.size == b.params.size && a.params.indices.all { TypeKind.of(a.params[it].type) == TypeKind.of(b.params[it].type) } -private fun List.kinds() = map { TypeKind.of(it.type) } - private fun getSupertype( manager: PsiManager, a: PsiType, b: PsiType, aIsIntLike: Boolean, bIsIntLike: Boolean, - intLikeAssignment: PsiType?, -): TypeMergeResult = when (TypeKind.of(a)) { + aIntLikeAssignment: PsiType?, + bIntLikeAssignment: PsiType?, +): TypeMergeResult? = when (TypeKind.of(a)) { TypeKind.OBJECT -> { TypeMergeResult( GenericsUtil.getLeastUpperBound(a, b, manager)!!, @@ -364,15 +372,19 @@ private fun getSupertype( TypeKind.INT_LIKE -> { when { - aIsIntLike -> TypeMergeResult( - intLikeAssignment ?: b, - isIntLike = intLikeAssignment == null && bIsIntLike, - coerce = intLikeAssignment != null && !bIsIntLike && intLikeAssignment != b, - ) - - bIsIntLike -> TypeMergeResult(a, isIntLike = false, coerce = false) - a == b -> TypeMergeResult(a, isIntLike = false, coerce = false) - else -> TypeMergeResult(PsiTypes.intType(), isIntLike = false, coerce = true) + aIsIntLike && bIsIntLike && (aIntLikeAssignment != null || bIntLikeAssignment != null) -> { + if (aIntLikeAssignment == bIntLikeAssignment) { + TypeMergeResult(aIntLikeAssignment!!, isIntLike = false, coerce = false) + } else { + null + } + } + aIsIntLike && bIsIntLike -> TypeMergeResult(a, isIntLike = true, coerce = false) + aIsIntLike -> TypeMergeResult(aIntLikeAssignment!!, isIntLike = false, coerce = b != aIntLikeAssignment) + bIsIntLike -> TypeMergeResult(bIntLikeAssignment!!, isIntLike = false, coerce = a != bIntLikeAssignment) + else -> mergeIntTypes(a, b)?.let { (type, coerce) -> + TypeMergeResult(type, isIntLike = false, coerce = coerce) + } } } @@ -383,3 +395,41 @@ private fun getSupertype( } private data class TypeMergeResult(val type: PsiType, val isIntLike: Boolean, val coerce: Boolean) + +private fun mergeIntTypes(a: PsiType, b: PsiType): Pair? = when { + a == b -> a to false + a == PsiTypes.intType() -> b to true + b == PsiTypes.intType() -> a to true + else -> null +} + +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++ + } +} diff --git a/src/main/kotlin/platform/mixin/util/Mixin.kt b/src/main/kotlin/platform/mixin/util/Mixin.kt index 77a350f47..f12a8b1ea 100644 --- a/src/main/kotlin/platform/mixin/util/Mixin.kt +++ b/src/main/kotlin/platform/mixin/util/Mixin.kt @@ -336,13 +336,13 @@ private val INT_TYPES = setOf( PsiTypes.shortType(), PsiTypes.charType(), PsiTypes.byteType(), - PsiTypes.booleanType() + 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 - expected in INT_TYPES -> actual == PsiTypes.intType() + actual in INT_TYPES -> expected == PsiTypes.intType() else -> isAssignable(actual.normalize(), expected.normalize()) } From 58f31e95db544c9a181d6a2d120b54de081a3f39 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Tue, 22 Sep 2026 00:05:19 +0100 Subject: [PATCH 19/51] fix int coercion properly --- .../inspection/injector/SuggestedSignature.kt | 206 ++++++++++++------ src/main/kotlin/util/sequences.kt | 11 + 2 files changed, 156 insertions(+), 61 deletions(-) diff --git a/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt index cf83e3c2f..51a5d7d0b 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt @@ -21,8 +21,11 @@ package com.demonwav.mcdev.platform.mixin.inspection.injector import com.demonwav.mcdev.platform.mixin.util.TypeKind +import com.demonwav.mcdev.util.allEqual import com.demonwav.mcdev.util.descriptor +import com.demonwav.mcdev.util.mapReduceFallible import com.demonwav.mcdev.util.normalize +import com.demonwav.mcdev.util.reduceFallible import com.intellij.psi.GenericsUtil import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiElement @@ -51,22 +54,13 @@ data class SuggestedReturnType( override val intLikeTypes = if (returnTypeIsIntLike) setOf(MethodSignature.TypePosition.Return) else emptySet() override val params: Nothing? get() = null - fun intersectCoerce(other: SuggestedReturnType, manager: PsiManager): SuggestedReturnType? { - if (TypeKind.of(this.returnType) != TypeKind.of(other.returnType)) { - return null - } - - val aIntLikeAssignment = other.returnType.takeIf { this.returnTypeIsIntLike && !other.returnTypeIsIntLike } - val bIntLikeAssignment = this.returnType.takeIf { other.returnTypeIsIntLike && !this.returnTypeIsIntLike } - + private fun intersectCoerce(other: SuggestedReturnType, manager: PsiManager): SuggestedReturnType? { val (returnType, returnTypeIsIntLike, coerceReturnType) = getSupertype( manager, this.returnType, other.returnType, this.returnTypeIsIntLike, other.returnTypeIsIntLike, - aIntLikeAssignment, - bIntLikeAssignment, ) ?: return null return SuggestedReturnType(returnType, returnTypeIsIntLike, coerceReturnType) @@ -108,7 +102,7 @@ data class SuggestedReturnType( .groupBy { TypeKind.of(it.returnType) } val chosen = optionsByKind.entries.firstOrNull { it.value.size == signatures.size }?.value ?: return null - return intersectReturnTypes(params, chosen.asSequence().map { exact(it) }) + return intersectCoerce(params, chosen.map { exact(it) }) } outer@ for (returnType in returnTypeOptions) { @@ -139,7 +133,7 @@ data class SuggestedReturnType( } } ?: continue@outer } - return intersectReturnTypes(params, choices.asSequence()) + return intersectCoerce(params, choices) } return null @@ -162,12 +156,41 @@ data class SuggestedReturnType( return returnType.takeUnless { allowCoerceRequired } } - private fun intersectReturnTypes( + private fun intersectCoerce( context: PsiElement, - types: Sequence, + types: List, ): SuggestedReturnType? { + if (!types.asSequence().map { TypeKind.of(it.returnType) }.allEqual()) { + return null + } + val intLikeAssignment = types + .filter { !it.returnTypeIsIntLike && TypeKind.of(it.returnType) == TypeKind.INT_LIKE } + .let { types -> + if (types.isEmpty()) { + null + } else { + types.asSequence() + .map { it.returnType } + .reduceFallible { acc, it -> mergeIntTypes(acc, it)?.first } + ?: return null + } + } + + val transformedTypes = if (intLikeAssignment == null) { + types.asSequence() + } else { + types.asSequence() + .map { type -> + if (!type.returnTypeIsIntLike) { + type + } else { + type.copy(returnType = intLikeAssignment, returnTypeIsIntLike = false) + } + } + } + val manager = PsiManager.getInstance(context.project) - return types.reduceOrNull { acc, it -> acc?.intersectCoerce(it, manager) } + return transformedTypes.reduceFallible { acc, it -> acc.intersectCoerce(it, manager) } } } } @@ -178,13 +201,7 @@ data class SuggestedSignature( override val intLikeTypes: Set = emptySet(), override val coerceReturnType: Boolean = false, ) : SignatureSuggestion { - fun intersectCoerce(other: SuggestedSignature, manager: PsiManager): SuggestedSignature? { - if (!kindsMatch(this, other)) { - return null - } - val aIntLikeAssignment = this.intLikeAssignment(other, onConflict = { return null }) - val bIntLikeAssignment = other.intLikeAssignment(this, onConflict = { return null }) - + private fun intersectCoerce(other: SuggestedSignature, manager: PsiManager): SuggestedSignature? { val intLikeTypes = mutableSetOf() val (returnType, returnTypeIsIntLike, coerceReturnType) = getSupertype( @@ -193,8 +210,6 @@ data class SuggestedSignature( other.returnType, MethodSignature.TypePosition.Return in this.intLikeTypes, MethodSignature.TypePosition.Return in other.intLikeTypes, - aIntLikeAssignment, - bIntLikeAssignment, ) ?: return null if (returnTypeIsIntLike) { @@ -210,8 +225,6 @@ data class SuggestedSignature( b.type, pos in this.intLikeTypes, pos in other.intLikeTypes, - aIntLikeAssignment, - bIntLikeAssignment, ) ?: return null if (isIntLike) { intLikeTypes.add(pos) @@ -230,20 +243,6 @@ data class SuggestedSignature( MethodSignature.TypePosition.Return -> returnType } - private inline fun intLikeAssignment(other: SuggestedSignature, onConflict: () -> Nothing): PsiType? { - var result: PsiType? = null - for (pos in this.intLikeTypes) { - if (pos !in other.intLikeTypes) { - result = if (result == null) { - other.getType(pos) - } else { - mergeIntTypes(result, other.getType(pos))?.first ?: onConflict() - } - } - } - return result - } - companion object { fun exact(signature: MethodSignature, takeTrailing: Int = 0): SuggestedSignature { return SuggestedSignature( @@ -283,7 +282,7 @@ data class SuggestedSignature( annotation: PsiAnnotation, signatures: List, ): SuggestedSignature? { - return intersectCoerce(annotation, signatures.asSequence().map { exact(it.signature) }) + return intersectCoerce(annotation, signatures.map { exact(it.signature) }) } fun inject(annotation: PsiAnnotation, signatures: List): SuggestedSignature? { @@ -292,13 +291,12 @@ data class SuggestedSignature( val longForm = intersectCoerce( annotation, - signatures.asSequence() - .map { exact(it.longSignature, takeTrailing = localsToUse) }, + signatures.map { exact(it.longSignature, takeTrailing = localsToUse) }, ) return longForm ?: intersectCoerce( annotation, - signatures.asSequence().map { exact(it.shortSignature ?: it.longSignature) }, + signatures.map { exact(it.shortSignature ?: it.longSignature) }, ) } @@ -320,10 +318,9 @@ data class SuggestedSignature( val intersected = intersectCoerce( annotation, - candidates.asSequence() - .map { - exact(it, takeTrailing = numParams - it.requiredParams.size) - }, + candidates.map { + exact(it, takeTrailing = numParams - it.requiredParams.size) + }, ) ?: continue val actuallyValid = candidates.all { @@ -340,10 +337,107 @@ data class SuggestedSignature( private fun intersectCoerce( context: PsiElement, - signatures: Sequence, + signatures: List, ): SuggestedSignature? { + if (signatures.asSequence().zipWithNext { a, b -> !kindsMatch(a, b) }.any { it }) { + return null + } + val intLikeAssignments = decideIntLikeAssignments(signatures) ?: return null + val transformedSignatures = signatures.zip(intLikeAssignments) { sig, assignment -> + if (assignment == null) { + sig + } else { + val params = sig.params.toMutableList() + var returnType = sig.returnType + for (pos in sig.intLikeTypes) { + when (pos) { + is MethodSignature.TypePosition.Param -> { + params[pos.index] = params[pos.index].copy(type = assignment) + } + MethodSignature.TypePosition.Return -> { + returnType = assignment + } + } + } + SuggestedSignature(params, returnType) + } + } val manager = PsiManager.getInstance(context.project) - return signatures.reduceOrNull { acc, it -> acc?.intersectCoerce(it, manager) } + return transformedSignatures.asSequence() + .reduceFallible { acc, it -> acc.intersectCoerce(it, manager) } + } + + private fun decideIntLikeAssignments(signatures: List): List? { + val intLikeForcingsByPos = signatures.asSequence() + .flatMap { sig -> + val positions = sig.params.indices.asSequence().map { MethodSignature.TypePosition.Param(it) } + + MethodSignature.TypePosition.Return + positions + .filter { TypeKind.of(sig.getType(it)) == TypeKind.INT_LIKE } + .filter { it !in sig.intLikeTypes } + .map { it to sig.getType(it) } + } + .groupingBy { it.first } + .mapReduceFallible({ it.second }) { _, acc, it -> mergeIntTypes(acc, it)?.first } + ?: return null + + val forcings = signatures + .mapTo(mutableListOf()) { sig -> + sig.intLikeTypes + .mapNotNull { intLikeForcingsByPos[it] } + .ifEmpty { return@mapTo null } + .asSequence() + .reduceFallible { acc, it -> mergeIntTypes(acc, it)?.first } + ?: return null + } + + val signaturesByIntLikePos = signatures.asSequence() + .withIndex() + .flatMap { (index, sig) -> + sig.intLikeTypes.asSequence().map { it to index } + } + .groupBy({ it.first }, { it.second }) + + val visited = BooleanArray(signatures.size) + + fun linkDfs(start: Int): List { + val result = mutableListOf() + val stack = mutableListOf(start) + + while (stack.isNotEmpty()) { + val current = stack.removeLast() + result.add(current) + val linked = signatures[current].intLikeTypes.asSequence() + .flatMap { signaturesByIntLikePos.getValue(it) } + for (child in linked) { + if (!visited[child]) { + visited[child] = true + stack.add(child) + } + } + } + + return result + } + + for (i in visited.indices) { + if (visited[i]) { + continue + } + visited[i] = true + val group = linkDfs(i) + val assignment = group + .mapNotNull { forcings[it] } + .ifEmpty { continue } + .asSequence() + .reduceFallible { acc, it -> mergeIntTypes(acc, it)?.first } + ?: return null + for (member in group) { + forcings[member] = assignment + } + } + + return forcings } } } @@ -359,8 +453,6 @@ private fun getSupertype( b: PsiType, aIsIntLike: Boolean, bIsIntLike: Boolean, - aIntLikeAssignment: PsiType?, - bIntLikeAssignment: PsiType?, ): TypeMergeResult? = when (TypeKind.of(a)) { TypeKind.OBJECT -> { TypeMergeResult( @@ -372,16 +464,8 @@ private fun getSupertype( TypeKind.INT_LIKE -> { when { - aIsIntLike && bIsIntLike && (aIntLikeAssignment != null || bIntLikeAssignment != null) -> { - if (aIntLikeAssignment == bIntLikeAssignment) { - TypeMergeResult(aIntLikeAssignment!!, isIntLike = false, coerce = false) - } else { - null - } - } aIsIntLike && bIsIntLike -> TypeMergeResult(a, isIntLike = true, coerce = false) - aIsIntLike -> TypeMergeResult(aIntLikeAssignment!!, isIntLike = false, coerce = b != aIntLikeAssignment) - bIsIntLike -> TypeMergeResult(bIntLikeAssignment!!, isIntLike = false, coerce = a != bIntLikeAssignment) + aIsIntLike || bIsIntLike -> error("Int-like type should have been forced") else -> mergeIntTypes(a, b)?.let { (type, coerce) -> TypeMergeResult(type, isIntLike = false, coerce = coerce) } diff --git a/src/main/kotlin/util/sequences.kt b/src/main/kotlin/util/sequences.kt index 1a6fd2f56..b310ef67f 100644 --- a/src/main/kotlin/util/sequences.kt +++ b/src/main/kotlin/util/sequences.kt @@ -56,3 +56,14 @@ 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 } + +inline fun Grouping.mapReduceFallible( + mapper: (T) -> R, + operation: (key: K, accumulator: R, element: R) -> R? +): Map? = + fold({ _, it -> mapper(it) }) { key, accumulator, element -> + operation(key, accumulator, mapper(element)) ?: return null + } From 054c3d46cdf951d9360f3611fb6342eb8dc8d2fe Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Tue, 22 Sep 2026 00:06:39 +0100 Subject: [PATCH 20/51] cleanup --- src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt | 2 +- .../kotlin/platform/mixin/handlers/ModifyVariableHandler.kt | 2 +- .../platform/mixin/inspection/injector/SuggestedSignature.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt index 5072a4025..86d21820e 100644 --- a/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt @@ -105,7 +105,7 @@ class ModifyArgHandler : InsnInjectorAnnotationHandler() { annotation: PsiAnnotation, targets: List ): SuggestedSignature? { - return SuggestedSignature.modifierNoCoerce( + return SuggestedSignature.modifier( annotation, expectedMethodSignatures(annotation, targets).collectSignatures() ?: return null, ) diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt index 717b57483..d63460401 100644 --- a/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt @@ -78,7 +78,7 @@ class ModifyVariableHandler : InsnInjectorAnnotationHandler() { annotation: PsiAnnotation, targets: List ): SuggestedSignature? { - return SuggestedSignature.modifierNoCoerce( + return SuggestedSignature.modifier( annotation, expectedMethodSignatures(annotation, targets).collectSignatures() ?: return null, ) diff --git a/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt index 51a5d7d0b..611362a1a 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt @@ -257,7 +257,7 @@ data class SuggestedSignature( ) } - fun modifierNoCoerce(annotation: PsiAnnotation, signatures: List): SuggestedSignature? { + fun modifier(annotation: PsiAnnotation, signatures: List): SuggestedSignature? { val parameterOptions = signatures.map { it.paramOptions } val psiManager = PsiManager.getInstance(annotation.project) From 285995a6bfd9d2eafe8425b6608d68c6190b7fe9 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Fri, 25 Sep 2026 23:25:57 +0100 Subject: [PATCH 21/51] proper int-like logic --- .../mixin/handlers/ModifyArgHandler.kt | 3 +- .../mixin/handlers/ModifyConstantHandler.kt | 3 +- .../mixin/handlers/ModifyVariableHandler.kt | 3 +- .../mixin/handlers/RedirectInjectorHandler.kt | 5 +- .../ModifyExpressionValueHandler.kt | 9 +- .../mixinextras/WrapOperationHandler.kt | 13 +- ...nvalidInjectorMethodSignatureInspection.kt | 4 +- .../inspection/injector/MethodSignature.kt | 109 ++++- .../inspection/injector/MethodSignatures.kt | 16 +- .../inspection/injector/SuggestedSignature.kt | 423 +++++++++--------- .../mixin/util/IntLikeAnchorSolver.kt | 82 ++++ src/main/kotlin/util/lists.kt | 1 + src/main/kotlin/util/sequenced-collections.kt | 65 +++ src/main/kotlin/util/sequences.kt | 17 + 14 files changed, 506 insertions(+), 247 deletions(-) create mode 100644 src/main/kotlin/platform/mixin/util/IntLikeAnchorSolver.kt create mode 100644 src/main/kotlin/util/sequenced-collections.kt diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt index 86d21820e..091824491 100644 --- a/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt @@ -29,6 +29,7 @@ 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.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiAnnotation @@ -95,7 +96,7 @@ class ModifyArgHandler : InsnInjectorAnnotationHandler() { } else null return ExpectedSignatures.Valid( ModifierSignatures( - paramOptions, + SequencedMap(paramOptions), fullParams, ) ) diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt index a73f9eb21..d9efc18a9 100644 --- a/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt @@ -30,6 +30,7 @@ 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 @@ -107,7 +108,7 @@ class ModifyConstantHandler : InsnInjectorAnnotationHandler() { return if (targetInsn is TypeInsnNode) { GeneralSignatures( makeTypeCheckParams(psiManager, annotation), - linkedMapOf( + sequencedMapOf( TypeKind.INT_LIKE to PsiTypes.booleanType(), TypeKind.OBJECT to getClassType(psiManager, annotation), ), diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt index d63460401..ee8c26b2a 100644 --- a/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt @@ -30,6 +30,7 @@ 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.Parameter +import com.demonwav.mcdev.util.SequencedMap import com.demonwav.mcdev.util.findContainingMethod import com.demonwav.mcdev.util.findModule import com.intellij.psi.JavaPsiFacade @@ -68,7 +69,7 @@ class ModifyVariableHandler : InsnInjectorAnnotationHandler() { return ExpectedSignatures.Valid( ModifierSignatures( - result, + SequencedMap(result), trailingParams = targetParams, ) ) diff --git a/src/main/kotlin/platform/mixin/handlers/RedirectInjectorHandler.kt b/src/main/kotlin/platform/mixin/handlers/RedirectInjectorHandler.kt index 21dccd99b..0dddfd208 100644 --- a/src/main/kotlin/platform/mixin/handlers/RedirectInjectorHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/RedirectInjectorHandler.kt @@ -36,6 +36,7 @@ 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 @@ -366,7 +367,7 @@ class RedirectInjectorHandler : InsnInjectorAnnotationHandler() { // drop the instance parameter, return the constructed type sig.copy( params = sig.params.drop(1), - returnTypeOptions = linkedMapOf(TypeKind.OBJECT to constructedType), + returnTypeOptions = sequencedMapOf(TypeKind.OBJECT to constructedType), ) } } @@ -392,7 +393,7 @@ class RedirectInjectorHandler : InsnInjectorAnnotationHandler() { ) return GeneralSignatures( parameters, - linkedMapOf( + sequencedMapOf( TypeKind.INT_LIKE to PsiTypes.booleanType(), TypeKind.OBJECT to classType, ), diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyExpressionValueHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyExpressionValueHandler.kt index ef0abf3ae..da949c80f 100644 --- a/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyExpressionValueHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyExpressionValueHandler.kt @@ -27,6 +27,9 @@ 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 @@ -84,12 +87,12 @@ class ModifyExpressionValueHandler : MixinExtrasInjectorAnnotationHandler() { ) } - private fun intLikeTypePositions(target: TargetInsn): Set { + private fun intLikeTypePositions(target: TargetInsn): SequencedSet { val expressionType = target.getDecoration(ExpressionDecorations.SIMPLE_EXPRESSION_TYPE) if (expressionType == ExpressionASMUtils.INTLIKE_TYPE) { - return setOf(MethodSignature.TypePosition.Return, MethodSignature.TypePosition.Param(0)) + return sequencedSetOf(MethodSignature.TypePosition.Return, MethodSignature.TypePosition.Param(0)) } - return emptySet() + return emptySequencedSet() } private fun getReturnType( diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt index c1d53a80a..b072cf390 100644 --- a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt @@ -27,6 +27,7 @@ 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 @@ -76,18 +77,18 @@ class WrapOperationHandler : MixinExtrasInjectorAnnotationHandler() { ) } - private fun intLikeTypePositions(target: TargetInsn) = buildSet { + 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/inspection/injector/InvalidInjectorMethodSignatureInspection.kt b/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt index ecea833ee..0520ad991 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt @@ -32,6 +32,7 @@ import com.demonwav.mcdev.platform.mixin.util.hasAccess import com.demonwav.mcdev.platform.mixin.util.isConstructor import com.demonwav.mcdev.platform.mixin.util.isMixinExtrasSugar 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 @@ -345,9 +346,8 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() { private fun makeIntLikeTypeTemplate( method: PsiMethod, - positionSet: Set + positions: SequencedSet ): Template? { - val positions = positionSet.sorted() val builder = TemplateBuilderImpl(method) builder.replaceElement( positions.first().getElement(method) ?: return null, diff --git a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt index 5cfef0488..637b7b48a 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt @@ -24,11 +24,15 @@ 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 requiredParams: List, @@ -36,24 +40,37 @@ data class MethodSignature( val allowCoerceRequired: Boolean, val trailingParams: List = emptyList(), val allowCoerceTrailing: Boolean = true, - val intLikeTypes: Set = emptySet() + val intLikeTypes: SequencedSet = emptySequencedSet(), ) { - sealed interface TypePosition : Comparable { + 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 compareTo(other: TypePosition): Int = if (other is Return) 0 else -1 + 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 compareTo(other: TypePosition): Int = if (other is Param) index.compareTo(other.index) else 1 + 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, @@ -92,23 +115,52 @@ data class MethodSignature( paramType: (ParamT) -> PsiType, paramCoerce: (ParamT) -> Boolean, ): Boolean { - val intLikeMismatch = TypePosition.Return in intLikeTypes - && intLikeTypes.asSequence() - .filterIsInstance() - .mapNotNull { params.getOrNull(it.index) } - .take(1) - .any { paramType(it) != returnType } - - return !intLikeMismatch - && matchesReturnType(returnType, returnCoerce) - && matchesParams(params, paramType, paramCoerce) + val intLikeAssignment = when (val anchor = intLikeTypes.firstOrNull()) { + null -> null + is TypePosition.Param -> paramType(params.getOrNull(anchor.index) ?: return false) + TypePosition.Return -> returnType + } + val transformedReturnType = if (TypePosition.Return in intLikeTypes) 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 + ?: (intLikeTypes.firstOrNull() as? TypePosition.Param)?.let { paramType(params[it.index]) } + + if (intLikeAssignment == null && intLikeTypes.isNotEmpty()) { + // We don't know the return type, but we should make sure the combination is feasible for some return type + val intLikeIndices = intLikeTypes.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)) @@ -117,20 +169,33 @@ data class MethodSignature( matchType( expected.type, paramType(actual), - allowCoerce && paramCoerce(actual), - TypePosition.Param(index), + isIntLike = intLikeAssignment == null && TypePosition.Param(index) in intLikeTypes, + coerce = allowCoerce && paramCoerce(actual), ) } } - return params.size in requiredParams.size..requiredParams.size + trailingParams.size - && intLikeTypes.asSequence().filterIsInstance().mapNotNull { (index) -> + val transformedRequiredParams = if (intLikeAssignment == null) { + requiredParams + } else { + requiredParams.mapIndexed { i, param -> + if (TypePosition.Param(i) in intLikeTypes) { + param.copy(type = intLikeAssignment) + } else { + param + } + } + } + + return intLikeTypes.asSequence().filterIsInstance().mapNotNull { (index) -> params.getOrNull(index)?.let(paramType) }.allEqual() - && matchParams(requiredParams, allowCoerceRequired, 0) + && matchParams(transformedRequiredParams, allowCoerceRequired, 0) && matchParams(trailingParams, allowCoerceTrailing, requiredParams.size) } - private fun matchType(expected: PsiType, actual: PsiType, coerce: Boolean, typePos: TypePosition) = - checkCoerce(expected, actual, coerce, typePos in intLikeTypes) + 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 index cc62691cc..be77111df 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt @@ -25,11 +25,15 @@ 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 java.util.SequencedMap import org.objectweb.asm.Type sealed interface ExpectedSignatures { @@ -93,7 +97,7 @@ class BasicSignatures(override val options: List) : MethodSigna class OperationWrapperSignatures private constructor( val params: List, val returnType: PsiType, - val intLikePositions: Set, + val intLikePositions: SequencedSet, val trailingParams: List, operationType: PsiType, ) : MethodSignatures { @@ -112,7 +116,7 @@ class OperationWrapperSignatures private constructor( context: PsiElement, params: List, returnType: PsiType, - intLikePositions: Set = emptySet(), + intLikePositions: SequencedSet = emptySequencedSet(), trailingParams: List = emptyList(), ) = mixinExtrasOperationType(context, returnType)?.let { OperationWrapperSignatures(params, returnType, intLikePositions, trailingParams, it) @@ -125,17 +129,17 @@ data class GeneralSignatures( val returnTypeOptions: SequencedMap, val allowCoerce: Boolean, val trailingParams: List, - val intLikePositions: Set = emptySet(), + val intLikePositions: SequencedSet = emptySequencedSet(), ) : MethodSignatures { constructor( params: List, returnType: PsiType, trailingParams: List, allowCoerce: Boolean = true, - intLikePositions: Set = emptySet(), + intLikePositions: SequencedSet = emptySequencedSet(), ) : this( params, - linkedMapOf(TypeKind.of(returnType) to returnType), + sequencedMapOf(TypeKind.of(returnType) to returnType), allowCoerce, trailingParams, intLikePositions, diff --git a/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt index 611362a1a..09f3aea81 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt @@ -20,12 +20,19 @@ package com.demonwav.mcdev.platform.mixin.inspection.injector +import com.demonwav.mcdev.platform.mixin.util.IntLikeAnchorSolver +import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Annotations.COERCE import com.demonwav.mcdev.platform.mixin.util.TypeKind +import com.demonwav.mcdev.util.MutableSequencedSet +import com.demonwav.mcdev.util.SequencedSet import com.demonwav.mcdev.util.allEqual import com.demonwav.mcdev.util.descriptor -import com.demonwav.mcdev.util.mapReduceFallible +import com.demonwav.mcdev.util.emptySequencedSet +import com.demonwav.mcdev.util.interleaved import com.demonwav.mcdev.util.normalize import com.demonwav.mcdev.util.reduceFallible +import com.demonwav.mcdev.util.sequencedSetOf +import com.demonwav.mcdev.util.sequencedSetOfNotNull import com.intellij.psi.GenericsUtil import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiElement @@ -35,12 +42,13 @@ import com.intellij.psi.PsiParameterList import com.intellij.psi.PsiType import com.intellij.psi.PsiTypes import com.intellij.psi.util.parentOfType +import java.util.IdentityHashMap import org.objectweb.asm.Type sealed interface SignatureSuggestion { val params: List? val returnType: PsiType - val intLikeTypes: Set + val intLikeTypes: SequencedSet val coerceReturnType: Boolean data class Param(val name: String?, val type: PsiType, val coerce: Boolean = false) @@ -51,7 +59,7 @@ data class SuggestedReturnType( val returnTypeIsIntLike: Boolean, override val coerceReturnType: Boolean = false, ) : SignatureSuggestion { - override val intLikeTypes = if (returnTypeIsIntLike) setOf(MethodSignature.TypePosition.Return) else emptySet() + override val intLikeTypes = if (returnTypeIsIntLike) sequencedSetOf(MethodSignature.TypePosition.Return) else emptySequencedSet() override val params: Nothing? get() = null private fun intersectCoerce(other: SuggestedReturnType, manager: PsiManager): SuggestedReturnType? { @@ -63,7 +71,11 @@ data class SuggestedReturnType( other.returnTypeIsIntLike, ) ?: return null - return SuggestedReturnType(returnType, returnTypeIsIntLike, coerceReturnType) + return SuggestedReturnType( + returnType, + returnTypeIsIntLike, + this.coerceReturnType || other.coerceReturnType || coerceReturnType, + ) } companion object { @@ -79,81 +91,126 @@ data class SuggestedReturnType( expected.options.filter { it.matchesParams(params) }.ifEmpty { return null } } - var returnTypeOptions: MutableSet? = null - outer@ for (options in validSignatures) { - val ourReturnTypeOptions = options.mapTo(mutableSetOf()) { - it.forcedReturnType(params)?.normalize() ?: continue@outer - } - if (returnTypeOptions == null) { - returnTypeOptions = ourReturnTypeOptions + val forcedReturnTypesBySig = validSignatures.asSequence() + .flatten() + .mapNotNull { sig -> sig.forcedReturnTypes(params)?.let { sig to it } } + .toMap(IdentityHashMap()) + + val constrainedSignatures = mutableMapOf>() + val unconstrainedSignatures = mutableMapOf>() + val typesByKind = mutableMapOf>() + val preferences = linkedSetOf() + + for ((index, signature) in validSignatures.asSequence().interleaved()) { + val forcedReturnTypes = forcedReturnTypesBySig[signature] + if (forcedReturnTypes != null) { + for (forcedReturnType in forcedReturnTypes) { + val normalized = forcedReturnType.normalize() + constrainedSignatures.getOrPut(normalized, ::mutableMapOf).putIfAbsent(index, signature) + typesByKind.getOrPut(TypeKind.of(normalized), ::linkedSetOf).add(normalized) + preferences.add(ReturnTypePreference.Constrained(normalized)) + } } else { - returnTypeOptions.retainAll(ourReturnTypeOptions) + val kind = TypeKind.of(signature.returnType) + unconstrainedSignatures.getOrPut(kind, ::mutableMapOf).putIfAbsent(index, signature) + preferences.add(ReturnTypePreference.Unconstrained(kind)) } } - if (returnTypeOptions == null) { - // Can choose whatever we like - val optionsByKind = validSignatures.asSequence() - .flatMap { options -> - options.asSequence() - .filter { it.forcedReturnType(params) == null } - .distinctBy { TypeKind.of(it.returnType) } - } - .groupBy { TypeKind.of(it.returnType) } - val chosen = - optionsByKind.entries.firstOrNull { it.value.size == signatures.size }?.value ?: return null - return intersectCoerce(params, chosen.map { exact(it) }) - } + val visitedReturnTypes = mutableSetOf() - outer@ for (returnType in returnTypeOptions) { - val choices = validSignatures.map { signatures -> - signatures.firstNotNullOfOrNull { signature -> - val forcedReturnType = signature.forcedReturnType(params) - if (forcedReturnType != null) { - if (forcedReturnType.normalize() == returnType) { - SuggestedReturnType( - forcedReturnType, - returnTypeIsIntLike = false, - ) - } else { - null - } + fun tryReturnType(option: PsiType): SuggestedReturnType? { + if (!visitedReturnTypes.add(option)) { + return null + } + val unconstrained = unconstrainedSignatures[TypeKind.of(option)].orEmpty() + val constrained = constrainedSignatures.getValue(option) + val combined = unconstrained + constrained + if (combined.size != signatures.size) { + return null + } + val intersected = intersectCoerce( + params, + combined.values.map { sig -> + val forcedReturnTypes = forcedReturnTypesBySig[sig] + if (forcedReturnTypes == null) { + exact(sig) } else { - // Here, the signature definitely allows coercion, or it would force a return type. - // Additionally, if the signature's return type is "int-like", we must have free choice - // since none of its parameters forced the return type - if (signature.matchesReturnType(returnType, hasCoerce = true)) { - SuggestedReturnType( - signature.returnType, - returnTypeIsIntLike = MethodSignature.TypePosition.Return in signature.intLikeTypes, - ) - } else { - null - } + SuggestedReturnType( + forcedReturnTypes.first { it.normalize() == option }, + returnTypeIsIntLike = false, + ) + } + }, + ) ?: return null + + return intersected.takeIf { it.returnType.normalize() == option } + } + + for (preference in preferences) { + when (preference) { + is ReturnTypePreference.Constrained -> { + tryReturnType(preference.type)?.let { return it } + } + is ReturnTypePreference.Unconstrained -> { + val unconstrained = unconstrainedSignatures.getValue(preference.kind) + if (unconstrained.size == signatures.size) { + // Will definitely work + return intersectCoerce( + params, + signatures.indices.map { exact(unconstrained.getValue(it)) }, + )!! } - } ?: continue@outer + val returnTypeOptions = typesByKind[preference.kind] ?: continue + for (option in returnTypeOptions) { + tryReturnType(option)?.let { return it } + } + } } - return intersectCoerce(params, choices) } return null } - private fun MethodSignature.forcedReturnType(params: PsiParameterList): PsiType? { + /** + * Returns null iff the signature can be intersected with all types of its kind. Otherwise, returns the possible + * return types that this signature can support. + */ + private fun MethodSignature.forcedReturnTypes(parameterList: PsiParameterList): SequencedSet? { if (MethodSignature.TypePosition.Return in intLikeTypes) { - val params = params.parameters - return intLikeTypes.firstNotNullOfOrNull { - when (it) { - is MethodSignature.TypePosition.Param -> params.getOrNull(it.index)?.type - is MethodSignature.TypePosition.Return -> null + val params = parameterList.parameters + val anchor = (intLikeTypes.first() as? MethodSignature.TypePosition.Param) + ?.let { params[it.index]?.type } + return when (anchor) { + PsiTypes.intType() -> null + null -> { + val intLikeParams = intLikeTypes.asSequence() + .mapNotNull { it.getParam(params) } + .groupBy { it.type } + when { + intLikeParams.isEmpty() -> null + PsiTypes.intType() in intLikeParams -> sequencedSetOf(PsiTypes.intType()) + intLikeParams.size == 1 -> sequencedSetOfNotNull( + intLikeParams.keys.single(), + PsiTypes.intType() + .takeIf { intLikeParams.values.single().all { it.hasAnnotation(COERCE) } }, + ) + else -> { + for ((type, params) in intLikeParams) { + check(type == PsiTypes.intType() || params.all { it.hasAnnotation(COERCE) }) + } + sequencedSetOf(PsiTypes.intType()) + } + } } + else -> sequencedSetOf(anchor) } } if (TypeKind.of(returnType) == TypeKind.INT_LIKE && returnType != PsiTypes.intType()) { - return returnType + return sequencedSetOf(returnType) } - return returnType.takeUnless { allowCoerceRequired } + return sequencedSetOf(returnType).takeUnless { allowCoerceRequired } } private fun intersectCoerce( @@ -163,34 +220,15 @@ data class SuggestedReturnType( if (!types.asSequence().map { TypeKind.of(it.returnType) }.allEqual()) { return null } - val intLikeAssignment = types - .filter { !it.returnTypeIsIntLike && TypeKind.of(it.returnType) == TypeKind.INT_LIKE } - .let { types -> - if (types.isEmpty()) { - null - } else { - types.asSequence() - .map { it.returnType } - .reduceFallible { acc, it -> mergeIntTypes(acc, it)?.first } - ?: return null - } - } - - val transformedTypes = if (intLikeAssignment == null) { - types.asSequence() - } else { - types.asSequence() - .map { type -> - if (!type.returnTypeIsIntLike) { - type - } else { - type.copy(returnType = intLikeAssignment, returnTypeIsIntLike = false) - } - } - } val manager = PsiManager.getInstance(context.project) - return transformedTypes.reduceFallible { acc, it -> acc.intersectCoerce(it, manager) } + return types.asSequence().reduceFallible { acc, it -> acc.intersectCoerce(it, manager) } + } + + private sealed interface ReturnTypePreference { + data class Constrained(val type: PsiType) : ReturnTypePreference + + data class Unconstrained(val kind: TypeKind) : ReturnTypePreference } } } @@ -198,11 +236,11 @@ data class SuggestedReturnType( data class SuggestedSignature( override val params: List, override val returnType: PsiType, - override val intLikeTypes: Set = emptySet(), + override val intLikeTypes: SequencedSet = emptySequencedSet(), override val coerceReturnType: Boolean = false, ) : SignatureSuggestion { private fun intersectCoerce(other: SuggestedSignature, manager: PsiManager): SuggestedSignature? { - val intLikeTypes = mutableSetOf() + val intLikeTypes = linkedSetOf() val (returnType, returnTypeIsIntLike, coerceReturnType) = getSupertype( manager, @@ -233,27 +271,39 @@ data class SuggestedSignature( SignatureSuggestion.Param(name, type, a.coerce || b.coerce || coerce) }, returnType, - intLikeTypes, + SequencedSet(intLikeTypes), this.coerceReturnType || other.coerceReturnType || coerceReturnType, ) } - private fun getType(pos: MethodSignature.TypePosition) = when (pos) { - is MethodSignature.TypePosition.Param -> params[pos.index].type - MethodSignature.TypePosition.Return -> returnType - } - companion object { - fun exact(signature: MethodSignature, takeTrailing: Int = 0): SuggestedSignature { - return SuggestedSignature( - (signature.requiredParams + signature.trailingParams.take(takeTrailing)).map { + 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, ) - }, - signature.returnType, - signature.intLikeTypes, + }.toMutableList() + if (intLikeAssignment != null) { + for (pos in signature.intLikeTypes) { + 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.intLikeTypes else emptySequencedSet(), ) } @@ -282,7 +332,11 @@ data class SuggestedSignature( annotation: PsiAnnotation, signatures: List, ): SuggestedSignature? { - return intersectCoerce(annotation, signatures.map { exact(it.signature) }) + return intersectCoerce( + annotation, + signatures.map { it.signature }, + numParams = { it.requiredParams.size }, + ) } fun inject(annotation: PsiAnnotation, signatures: List): SuggestedSignature? { @@ -291,12 +345,14 @@ data class SuggestedSignature( val longForm = intersectCoerce( annotation, - signatures.map { exact(it.longSignature, takeTrailing = localsToUse) }, + signatures.map { it.longSignature }, + numParams = { it.requiredParams.size + localsToUse }, ) return longForm ?: intersectCoerce( annotation, - signatures.map { exact(it.shortSignature ?: it.longSignature) }, + signatures.map { it.shortSignature ?: it.longSignature }, + numParams = { it.requiredParams.size }, ) } @@ -316,20 +372,16 @@ data class SuggestedSignature( continue } - val intersected = intersectCoerce( + val (soft, hard) = candidates.partition { it.allowCoerceRequired } + + val intersected = intersect( annotation, - candidates.map { - exact(it, takeTrailing = numParams - it.requiredParams.size) - }, + softSignatures = soft, + hardSignatures = hard, + numParams = { numParams }, ) ?: continue - val actuallyValid = candidates.all { - it.allowCoerceRequired || it.matches(intersected) - } - - if (actuallyValid) { - return intersected - } + return intersected } return null @@ -337,107 +389,72 @@ data class SuggestedSignature( private fun intersectCoerce( context: PsiElement, - signatures: List, + signatures: List, + numParams: (MethodSignature) -> Int, ): SuggestedSignature? { - if (signatures.asSequence().zipWithNext { a, b -> !kindsMatch(a, b) }.any { it }) { - return null - } - val intLikeAssignments = decideIntLikeAssignments(signatures) ?: return null - val transformedSignatures = signatures.zip(intLikeAssignments) { sig, assignment -> - if (assignment == null) { - sig - } else { - val params = sig.params.toMutableList() - var returnType = sig.returnType - for (pos in sig.intLikeTypes) { - when (pos) { - is MethodSignature.TypePosition.Param -> { - params[pos.index] = params[pos.index].copy(type = assignment) - } - MethodSignature.TypePosition.Return -> { - returnType = assignment - } - } - } - SuggestedSignature(params, returnType) - } - } - val manager = PsiManager.getInstance(context.project) - return transformedSignatures.asSequence() - .reduceFallible { acc, it -> acc.intersectCoerce(it, manager) } + return intersect( + context, + softSignatures = signatures, + hardSignatures = emptyList(), + numParams = numParams, + ) } - private fun decideIntLikeAssignments(signatures: List): List? { - val intLikeForcingsByPos = signatures.asSequence() - .flatMap { sig -> - val positions = sig.params.indices.asSequence().map { MethodSignature.TypePosition.Param(it) } + - MethodSignature.TypePosition.Return - positions - .filter { TypeKind.of(sig.getType(it)) == TypeKind.INT_LIKE } - .filter { it !in sig.intLikeTypes } - .map { it to sig.getType(it) } - } - .groupingBy { it.first } - .mapReduceFallible({ it.second }) { _, acc, it -> mergeIntTypes(acc, it)?.first } - ?: return null - - val forcings = signatures - .mapTo(mutableListOf()) { sig -> - sig.intLikeTypes - .mapNotNull { intLikeForcingsByPos[it] } - .ifEmpty { return@mapTo null } - .asSequence() - .reduceFallible { acc, it -> mergeIntTypes(acc, it)?.first } - ?: return null - } - - val signaturesByIntLikePos = signatures.asSequence() - .withIndex() - .flatMap { (index, sig) -> - sig.intLikeTypes.asSequence().map { it to index } - } - .groupBy({ it.first }, { it.second }) + private fun intersect( + context: PsiElement, + softSignatures: List, + hardSignatures: List, + numParams: (MethodSignature) -> Int, + ): SuggestedSignature? { + val allSignatures = softSignatures.asSequence() + hardSignatures + val paramsToUse = allSignatures.map { numParams(it) }.distinct().singleOrNull() ?: return null - val visited = BooleanArray(signatures.size) - - fun linkDfs(start: Int): List { - val result = mutableListOf() - val stack = mutableListOf(start) - - while (stack.isNotEmpty()) { - val current = stack.removeLast() - result.add(current) - val linked = signatures[current].intLikeTypes.asSequence() - .flatMap { signaturesByIntLikePos.getValue(it) } - for (child in linked) { - if (!visited[child]) { - visited[child] = true - stack.add(child) + val intLikeAnchor = run { + allSignatures.mapNotNull { it.intLikeTypes.firstOrNull() }.toList() + .ifEmpty { return@run null } + .asSequence() + .distinct() + .singleOrNull() ?: return null + } + val intLikeAssignment = if (intLikeAnchor == null) { + null + } else { + val solver = IntLikeAnchorSolver() + val signatures = + softSignatures.asSequence().map { it to false } + hardSignatures.asSequence().map { it to true } + for ((signature, isHard) in signatures) { + for (pos in signature.allPositions(paramsToUse)) { + if (pos in signature.intLikeTypes) { + continue + } + val isValid = solver.constrain( + pos.getType(signature), + isAnchor = pos == intLikeAnchor, + isHard = isHard, + ) + if (!isValid) { + return null } } } - - return result + solver.solve() } - for (i in visited.indices) { - if (visited[i]) { - continue - } - visited[i] = true - val group = linkDfs(i) - val assignment = group - .mapNotNull { forcings[it] } - .ifEmpty { continue } - .asSequence() - .reduceFallible { acc, it -> mergeIntTypes(acc, it)?.first } - ?: return null - for (member in group) { - forcings[member] = assignment - } + val suggestedSignatures = allSignatures.map { + exact(it, takeTrailing = paramsToUse - it.requiredParams.size, intLikeAssignment = intLikeAssignment) + }.toList() + + if (suggestedSignatures.asSequence().zipWithNext { a, b -> !kindsMatch(a, b) }.any { it }) { + return null } - return forcings + val manager = PsiManager.getInstance(context.project) + val intersected = suggestedSignatures.asSequence() + .reduceFallible { acc, it -> acc.intersectCoerce(it, manager) } ?: return null + + return intersected.takeIf { + hardSignatures.all { it.matches(intersected) } + } } } } 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..6ce5d3659 --- /dev/null +++ b/src/main/kotlin/platform/mixin/util/IntLikeAnchorSolver.kt @@ -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.platform.mixin.util + +import com.intellij.psi.PsiType +import com.intellij.psi.PsiTypes +import java.util.EnumSet + +class IntLikeAnchorSolver { + private val candidates = EnumSet.allOf(IntType::class.java) + private var hasIntPreference = false + private var hasLeafPreference = false + + fun constrain(desiredType: PsiType, isAnchor: Boolean, isHard: Boolean): Boolean { + val type = IntType.of(desiredType) ?: return false + when { + isHard && (isAnchor || type == IntType.INT) -> candidates.retainAll(setOf(type)) + type == IntType.INT -> hasIntPreference = true + isAnchor -> candidates.retainAll(setOf(type)) + else -> { + candidates.retainAll(setOf(type, IntType.INT)) + hasLeafPreference = true + } + } + return candidates.isNotEmpty() + } + + fun solve(): PsiType? { + require(candidates.isNotEmpty()) + + candidates.singleOrNull()?.let { return it.type } + + return when { + hasIntPreference -> { + // Cannot have 2 leaves without int + check(IntType.INT in candidates) + PsiTypes.intType() + } + 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/util/lists.kt b/src/main/kotlin/util/lists.kt index c0d9e2567..4eae5cabe 100644 --- a/src/main/kotlin/util/lists.kt +++ b/src/main/kotlin/util/lists.kt @@ -38,3 +38,4 @@ fun sharedPrefixLength(lists: List>): Int { return firstList.size } + diff --git a/src/main/kotlin/util/sequenced-collections.kt b/src/main/kotlin/util/sequenced-collections.kt new file mode 100644 index 000000000..516aa1567 --- /dev/null +++ b/src/main/kotlin/util/sequenced-collections.kt @@ -0,0 +1,65 @@ +/* + * 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 + +import java.util.Collections + +@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)) + +fun sequencedSetOfNotNull(vararg elements: T?): SequencedSet { + return SequencedSet(elements.filterNotNullTo(linkedSetOf())) +} + +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 b310ef67f..d2b167078 100644 --- a/src/main/kotlin/util/sequences.kt +++ b/src/main/kotlin/util/sequences.kt @@ -20,6 +20,8 @@ package com.demonwav.mcdev.util +import java.util.LinkedList + inline fun Sequence.toTypedArray(): Array { return toList().toTypedArray() } @@ -67,3 +69,18 @@ inline fun Grouping.mapReduceFallible( fold({ _, it -> mapper(it) }) { key, accumulator, element -> operation(key, accumulator, mapper(element)) ?: return null } + +fun Sequence>.interleaved(): Sequence> = sequence { + val iterators = this@interleaved.mapIndexedTo(LinkedList()) { i, it -> IndexedValue(i, it.iterator()) } + while (iterators.isNotEmpty()) { + val outer = iterators.iterator() + while (outer.hasNext()) { + val (index, inner) = outer.next() + if (inner.hasNext()) { + yield(IndexedValue(index, inner.next())) + } else { + outer.remove() + } + } + } +} From f44abeb30ec927759bf063cda66baa4fe942d824 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Fri, 25 Sep 2026 23:32:19 +0100 Subject: [PATCH 22/51] cleanup --- .../handlers/InsnInjectorAnnotationHandler.kt | 2 +- .../MixinExtrasInjectorAnnotationHandler.kt | 4 -- .../inspection/injector/SuggestedSignature.kt | 4 +- src/main/kotlin/util/lists.kt | 41 ------------------- src/main/kotlin/util/sequenced-collections.kt | 2 - src/main/kotlin/util/sequences.kt | 8 ---- 6 files changed, 2 insertions(+), 59 deletions(-) delete mode 100644 src/main/kotlin/util/lists.kt diff --git a/src/main/kotlin/platform/mixin/handlers/InsnInjectorAnnotationHandler.kt b/src/main/kotlin/platform/mixin/handlers/InsnInjectorAnnotationHandler.kt index fe15d2332..4ee76f775 100644 --- a/src/main/kotlin/platform/mixin/handlers/InsnInjectorAnnotationHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/InsnInjectorAnnotationHandler.kt @@ -109,7 +109,7 @@ abstract class InsnInjectorAnnotationHandler : InjectorAnnotationHandler() { targets: List, mode: CollectVisitor.Mode, ): List> { - return resolveInstructions(annotation, targets).map { (target, result) -> + return resolveInstructions(annotation, targets, mode).map { (target, result) -> val (targetClass, targetMethod) = target val targetInsn = TargetInsn(result.insn, result.decorations) expectedMethodSignature(annotation, targetClass, targetMethod, targetInsn) diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/MixinExtrasInjectorAnnotationHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/MixinExtrasInjectorAnnotationHandler.kt index 5ad4a3aec..f8cf7d107 100644 --- a/src/main/kotlin/platform/mixin/handlers/mixinextras/MixinExtrasInjectorAnnotationHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/MixinExtrasInjectorAnnotationHandler.kt @@ -356,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/inspection/injector/SuggestedSignature.kt b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt index 09f3aea81..c37602582 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt @@ -416,9 +416,7 @@ data class SuggestedSignature( .distinct() .singleOrNull() ?: return null } - val intLikeAssignment = if (intLikeAnchor == null) { - null - } else { + val intLikeAssignment = intLikeAnchor?.let { val solver = IntLikeAnchorSolver() val signatures = softSignatures.asSequence().map { it to false } + hardSignatures.asSequence().map { it to true } diff --git a/src/main/kotlin/util/lists.kt b/src/main/kotlin/util/lists.kt deleted file mode 100644 index 4eae5cabe..000000000 --- a/src/main/kotlin/util/lists.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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 - -fun sharedPrefixLength(lists: List>): Int { - if (lists.isEmpty()) { - return 0 - } - lists.singleOrNull()?.let { return it.size } - - val firstList = lists.first() - - for (i in firstList.indices) { - val targetElement = firstList[i] - - if (lists.any { i >= it.size || it[i] != targetElement }) { - return i - } - } - - return firstList.size -} - diff --git a/src/main/kotlin/util/sequenced-collections.kt b/src/main/kotlin/util/sequenced-collections.kt index 516aa1567..1844bf034 100644 --- a/src/main/kotlin/util/sequenced-collections.kt +++ b/src/main/kotlin/util/sequenced-collections.kt @@ -20,8 +20,6 @@ package com.demonwav.mcdev.util -import java.util.Collections - @Suppress("JavaDefaultMethodsNotOverriddenByDelegation") @JvmInline value class SequencedSet(private val set: MutableSequencedSet) : Set by set diff --git a/src/main/kotlin/util/sequences.kt b/src/main/kotlin/util/sequences.kt index d2b167078..1c6468e95 100644 --- a/src/main/kotlin/util/sequences.kt +++ b/src/main/kotlin/util/sequences.kt @@ -62,14 +62,6 @@ 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 } -inline fun Grouping.mapReduceFallible( - mapper: (T) -> R, - operation: (key: K, accumulator: R, element: R) -> R? -): Map? = - fold({ _, it -> mapper(it) }) { key, accumulator, element -> - operation(key, accumulator, mapper(element)) ?: return null - } - fun Sequence>.interleaved(): Sequence> = sequence { val iterators = this@interleaved.mapIndexedTo(LinkedList()) { i, it -> IndexedValue(i, it.iterator()) } while (iterators.isNotEmpty()) { From dda0b6f418d22c8993612895f104b4e3f316f848 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Sat, 26 Sep 2026 01:37:55 +0100 Subject: [PATCH 23/51] some docs and fixes --- .../inspection/injector/SuggestedSignature.kt | 184 ++++++++++++------ .../mixin/util/IntLikeAnchorSolver.kt | 15 ++ 2 files changed, 139 insertions(+), 60 deletions(-) diff --git a/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt index c37602582..6f4e0ba4d 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt @@ -63,7 +63,7 @@ data class SuggestedReturnType( override val params: Nothing? get() = null private fun intersectCoerce(other: SuggestedReturnType, manager: PsiManager): SuggestedReturnType? { - val (returnType, returnTypeIsIntLike, coerceReturnType) = getSupertype( + val (returnType, returnTypeIsIntLike, coerceReturnType) = mergeTypes( manager, this.returnType, other.returnType, @@ -79,13 +79,16 @@ data class SuggestedReturnType( } companion object { - fun exact(signature: MethodSignature): SuggestedReturnType { + private fun exact(signature: MethodSignature): SuggestedReturnType { return SuggestedReturnType( signature.returnType, MethodSignature.TypePosition.Return in signature.intLikeTypes, ) } + /** + * 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 } @@ -173,46 +176,72 @@ data class SuggestedReturnType( } /** - * Returns null iff the signature can be intersected with all types of its kind. Otherwise, returns the possible - * return types that this signature can support. + * Returns `null` iff the signature's return type can be intersected with all types of its kind. Otherwise, + * returns, in order of preference, the possible return types that this signature can support. + * + * **Precondition:** The parameter list is valid according to the signature. */ private fun MethodSignature.forcedReturnTypes(parameterList: PsiParameterList): SequencedSet? { if (MethodSignature.TypePosition.Return in intLikeTypes) { val params = parameterList.parameters val anchor = (intLikeTypes.first() as? MethodSignature.TypePosition.Param) - ?.let { params[it.index]?.type } + ?.let { params[it.index].type } return when (anchor) { - PsiTypes.intType() -> null + PsiTypes.intType() -> { + // Can be coerced to any int-like type + null + } null -> { + // The return type itself is the anchor val intLikeParams = intLikeTypes.asSequence() .mapNotNull { it.getParam(params) } .groupBy { it.type } when { - intLikeParams.isEmpty() -> null - PsiTypes.intType() in intLikeParams -> sequencedSetOf(PsiTypes.intType()) - intLikeParams.size == 1 -> sequencedSetOfNotNull( - intLikeParams.keys.single(), - PsiTypes.intType() - .takeIf { intLikeParams.values.single().all { it.hasAnnotation(COERCE) } }, - ) + intLikeParams.isEmpty() -> { + // Only the return type is int-like, free choice + null + } + PsiTypes.intType() in intLikeParams -> { + // Only int can be coerced to int (can also be coerced to anything else we found) + sequencedSetOf(PsiTypes.intType()) + } + 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. + sequencedSetOfNotNull( + intLikeParams.keys.single(), + PsiTypes.intType() + .takeIf { intLikeParams.values.single().all { it.hasAnnotation(COERCE) } }, + ) + } else -> { + // Only int can be coerced to multiple types for ((type, params) in intLikeParams) { + // We double-check that the parameters are valid as the caller promised check(type == PsiTypes.intType() || params.all { it.hasAnnotation(COERCE) }) } sequencedSetOf(PsiTypes.intType()) } } } - else -> sequencedSetOf(anchor) + else -> { + // Anchor is a leaf type which can only be coerced to itself + sequencedSetOf(anchor) + } } } if (TypeKind.of(returnType) == TypeKind.INT_LIKE && returnType != PsiTypes.intType()) { + // Leaf types cannot be intersected with any other type return sequencedSetOf(returnType) } return sequencedSetOf(returnType).takeUnless { allowCoerceRequired } } + /** + * Returns the most specific suggested return type that satisfies all the constraints, or `null` if no such type + * exists. + */ private fun intersectCoerce( context: PsiElement, types: List, @@ -242,7 +271,7 @@ data class SuggestedSignature( private fun intersectCoerce(other: SuggestedSignature, manager: PsiManager): SuggestedSignature? { val intLikeTypes = linkedSetOf() - val (returnType, returnTypeIsIntLike, coerceReturnType) = getSupertype( + val (returnType, returnTypeIsIntLike, coerceReturnType) = mergeTypes( manager, this.returnType, other.returnType, @@ -257,7 +286,7 @@ data class SuggestedSignature( return SuggestedSignature( params.withIndex().zip(other.params) { (index, a), b -> val pos = MethodSignature.TypePosition.Param(index) - val (type, isIntLike, coerce) = getSupertype( + val (type, isIntLike, coerce) = mergeTypes( manager, a.type, b.type, @@ -277,7 +306,7 @@ data class SuggestedSignature( } companion object { - fun exact( + private fun exact( signature: MethodSignature, takeTrailing: Int = 0, intLikeAssignment: PsiType? = null, @@ -307,6 +336,11 @@ data class SuggestedSignature( ) } + /** + * 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) @@ -328,34 +362,53 @@ data class SuggestedSignature( 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 intersectCoerce( + 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 = intersectCoerce( + val longForm = intersect( annotation, signatures.map { it.longSignature }, numParams = { it.requiredParams.size + localsToUse }, ) - return longForm ?: intersectCoerce( + 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 } @@ -372,12 +425,9 @@ data class SuggestedSignature( continue } - val (soft, hard) = candidates.partition { it.allowCoerceRequired } - val intersected = intersect( annotation, - softSignatures = soft, - hardSignatures = hard, + candidates, numParams = { numParams }, ) ?: continue @@ -387,48 +437,51 @@ data class SuggestedSignature( return null } - private fun intersectCoerce( - context: PsiElement, - signatures: List, - numParams: (MethodSignature) -> Int, - ): SuggestedSignature? { - return intersect( - context, - softSignatures = signatures, - hardSignatures = emptyList(), - numParams = numParams, - ) - } - + /** + * 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, - softSignatures: List, - hardSignatures: List, + signatures: List, numParams: (MethodSignature) -> Int, ): SuggestedSignature? { - val allSignatures = softSignatures.asSequence() + hardSignatures - val paramsToUse = allSignatures.map { numParams(it) }.distinct().singleOrNull() ?: return null + val paramsToUse = signatures + .map { sig -> + numParams(sig).also { + if (it !in sig.requiredParams.size..sig.requiredParams.size + sig.trailingParams.size) { + return null + } + } + } + .asSequence() + .distinct() + .singleOrNull() + ?: return null val intLikeAnchor = run { - allSignatures.mapNotNull { it.intLikeTypes.firstOrNull() }.toList() + signatures.mapNotNull { it.intLikeTypes.firstOrNull() }.toList() .ifEmpty { return@run null } .asSequence() .distinct() - .singleOrNull() ?: return null + // With the currently available signature shapes, there can only ever be 1 anchor. + // This logic will need revisiting if that changes. + .single() } val intLikeAssignment = intLikeAnchor?.let { val solver = IntLikeAnchorSolver() - val signatures = - softSignatures.asSequence().map { it to false } + hardSignatures.asSequence().map { it to true } - for ((signature, isHard) in signatures) { + for (signature in signatures) { for (pos in signature.allPositions(paramsToUse)) { if (pos in signature.intLikeTypes) { continue } + val isTrailingParam = pos is MethodSignature.TypePosition.Param && + pos.index > signature.requiredParams.lastIndex val isValid = solver.constrain( pos.getType(signature), isAnchor = pos == intLikeAnchor, - isHard = isHard, + isHard = if (isTrailingParam) signature.allowCoerceTrailing else signature.allowCoerceRequired, ) if (!isValid) { return null @@ -438,7 +491,7 @@ data class SuggestedSignature( solver.solve() } - val suggestedSignatures = allSignatures.map { + val suggestedSignatures = signatures.map { exact(it, takeTrailing = paramsToUse - it.requiredParams.size, intLikeAssignment = intLikeAssignment) }.toList() @@ -450,8 +503,15 @@ data class SuggestedSignature( 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 { - hardSignatures.all { it.matches(intersected) } + signatures.all { + it.allowCoerceRequired && (intersected.params.size <= it.requiredParams.size || it.allowCoerceTrailing) + || it.matches(intersected) + } } } } @@ -462,7 +522,14 @@ private fun kindsMatch(a: SuggestedSignature, b: SuggestedSignature) = && a.params.size == b.params.size && a.params.indices.all { TypeKind.of(a.params[it].type) == TypeKind.of(b.params[it].type) } -private fun getSupertype( +/** + * Merges the two types with regards to `@Coerce` behaviour. 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, @@ -481,9 +548,10 @@ private fun getSupertype( when { aIsIntLike && bIsIntLike -> TypeMergeResult(a, isIntLike = true, coerce = false) aIsIntLike || bIsIntLike -> error("Int-like type should have been forced") - else -> mergeIntTypes(a, b)?.let { (type, coerce) -> - TypeMergeResult(type, isIntLike = false, coerce = coerce) - } + 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 } } @@ -495,13 +563,9 @@ private fun getSupertype( private data class TypeMergeResult(val type: PsiType, val isIntLike: Boolean, val coerce: Boolean) -private fun mergeIntTypes(a: PsiType, b: PsiType): Pair? = when { - a == b -> a to false - a == PsiTypes.intType() -> b to true - b == PsiTypes.intType() -> a to true - else -> null -} - +/** + * 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 diff --git a/src/main/kotlin/platform/mixin/util/IntLikeAnchorSolver.kt b/src/main/kotlin/platform/mixin/util/IntLikeAnchorSolver.kt index 6ce5d3659..35dccc6ba 100644 --- a/src/main/kotlin/platform/mixin/util/IntLikeAnchorSolver.kt +++ b/src/main/kotlin/platform/mixin/util/IntLikeAnchorSolver.kt @@ -24,11 +24,17 @@ 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 { @@ -43,17 +49,26 @@ class IntLikeAnchorSolver { 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 From 57204e12990e7b16449ee6494959fc48df2faa88 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Sat, 26 Sep 2026 17:51:48 +0100 Subject: [PATCH 24/51] fix return type solving --- .../inspection/injector/MethodSignature.kt | 20 +- .../inspection/injector/MethodSignatures.kt | 4 +- .../inspection/injector/SuggestedSignature.kt | 274 ++++-------------- .../platform/mixin/util/ReturnTypeSolver.kt | 232 +++++++++++++++ src/main/kotlin/util/PrioritySet.kt | 45 +++ 5 files changed, 342 insertions(+), 233 deletions(-) create mode 100644 src/main/kotlin/platform/mixin/util/ReturnTypeSolver.kt create mode 100644 src/main/kotlin/util/PrioritySet.kt diff --git a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt index 637b7b48a..cedb1799c 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt @@ -40,7 +40,7 @@ data class MethodSignature( val allowCoerceRequired: Boolean, val trailingParams: List = emptyList(), val allowCoerceTrailing: Boolean = true, - val intLikeTypes: SequencedSet = emptySequencedSet(), + val intLikePositions: SequencedSet = emptySequencedSet(), ) { sealed interface TypePosition { fun getElement(method: PsiMethod): PsiTypeElement? @@ -105,7 +105,7 @@ data class MethodSignature( this.returnType, returnType, coerce = allowCoerceRequired && hasCoerce, - isIntLike = TypePosition.Return in intLikeTypes, + isIntLike = TypePosition.Return in intLikePositions, ) private fun matches( @@ -115,12 +115,12 @@ data class MethodSignature( paramType: (ParamT) -> PsiType, paramCoerce: (ParamT) -> Boolean, ): Boolean { - val intLikeAssignment = when (val anchor = intLikeTypes.firstOrNull()) { + 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 intLikeTypes) intLikeAssignment!! else returnType + val transformedReturnType = if (TypePosition.Return in intLikePositions) intLikeAssignment!! else returnType return matchesReturnType(transformedReturnType, returnCoerce) && matchesParams( params, @@ -141,11 +141,11 @@ data class MethodSignature( } val intLikeAssignment = knownIntLikeAssignment - ?: (intLikeTypes.firstOrNull() as? TypePosition.Param)?.let { paramType(params[it.index]) } + ?: (intLikePositions.firstOrNull() as? TypePosition.Param)?.let { paramType(params[it.index]) } - if (intLikeAssignment == null && intLikeTypes.isNotEmpty()) { + 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 = intLikeTypes.mapNotNull { (it as? TypePosition.Param)?.index } + 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] @@ -169,7 +169,7 @@ data class MethodSignature( matchType( expected.type, paramType(actual), - isIntLike = intLikeAssignment == null && TypePosition.Param(index) in intLikeTypes, + isIntLike = intLikeAssignment == null && TypePosition.Param(index) in intLikePositions, coerce = allowCoerce && paramCoerce(actual), ) } @@ -179,7 +179,7 @@ data class MethodSignature( requiredParams } else { requiredParams.mapIndexed { i, param -> - if (TypePosition.Param(i) in intLikeTypes) { + if (TypePosition.Param(i) in intLikePositions) { param.copy(type = intLikeAssignment) } else { param @@ -187,7 +187,7 @@ data class MethodSignature( } } - return intLikeTypes.asSequence().filterIsInstance().mapNotNull { (index) -> + return intLikePositions.asSequence().filterIsInstance().mapNotNull { (index) -> params.getOrNull(index)?.let(paramType) }.allEqual() && matchParams(transformedRequiredParams, allowCoerceRequired, 0) diff --git a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt index be77111df..c30bfffe8 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt @@ -106,7 +106,7 @@ class OperationWrapperSignatures private constructor( returnType, allowCoerceRequired = true, trailingParams = trailingParams, - intLikeTypes = intLikePositions, + intLikePositions = intLikePositions, ) override val options = listOf(signature) @@ -152,7 +152,7 @@ data class GeneralSignatures( returnTypeOptions.getValue(returnKind), allowCoerceRequired = allowCoerce, trailingParams = trailingParams, - intLikeTypes = if (returnKind == TypeKind.INT_LIKE) { + intLikePositions = if (returnKind == TypeKind.INT_LIKE) { intLikePositions } else { intLikePositions - MethodSignature.TypePosition.Return diff --git a/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt index 6f4e0ba4d..52cffd817 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt @@ -21,18 +21,14 @@ package com.demonwav.mcdev.platform.mixin.inspection.injector import com.demonwav.mcdev.platform.mixin.util.IntLikeAnchorSolver -import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Annotations.COERCE +import com.demonwav.mcdev.platform.mixin.util.ReturnTypeSolver import com.demonwav.mcdev.platform.mixin.util.TypeKind -import com.demonwav.mcdev.util.MutableSequencedSet import com.demonwav.mcdev.util.SequencedSet -import com.demonwav.mcdev.util.allEqual import com.demonwav.mcdev.util.descriptor import com.demonwav.mcdev.util.emptySequencedSet -import com.demonwav.mcdev.util.interleaved import com.demonwav.mcdev.util.normalize import com.demonwav.mcdev.util.reduceFallible import com.demonwav.mcdev.util.sequencedSetOf -import com.demonwav.mcdev.util.sequencedSetOfNotNull import com.intellij.psi.GenericsUtil import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiElement @@ -42,7 +38,6 @@ import com.intellij.psi.PsiParameterList import com.intellij.psi.PsiType import com.intellij.psi.PsiTypes import com.intellij.psi.util.parentOfType -import java.util.IdentityHashMap import org.objectweb.asm.Type sealed interface SignatureSuggestion { @@ -56,13 +51,14 @@ sealed interface SignatureSuggestion { data class SuggestedReturnType( override val returnType: PsiType, - val returnTypeIsIntLike: Boolean, + val returnTypeIsIntLike: Boolean = false, override val coerceReturnType: Boolean = false, ) : SignatureSuggestion { - override val intLikeTypes = if (returnTypeIsIntLike) sequencedSetOf(MethodSignature.TypePosition.Return) else emptySequencedSet() - override val params: Nothing? get() = null + override val intLikeTypes = + if (returnTypeIsIntLike) sequencedSetOf(MethodSignature.TypePosition.Return) else emptySequencedSet() + override val params get() = null - private fun intersectCoerce(other: SuggestedReturnType, manager: PsiManager): SuggestedReturnType? { + fun intersectCoerce(other: SuggestedReturnType, manager: PsiManager): SuggestedReturnType? { val (returnType, returnTypeIsIntLike, coerceReturnType) = mergeTypes( manager, this.returnType, @@ -79,13 +75,6 @@ data class SuggestedReturnType( } companion object { - private fun exact(signature: MethodSignature): SuggestedReturnType { - return SuggestedReturnType( - signature.returnType, - MethodSignature.TypePosition.Return in signature.intLikeTypes, - ) - } - /** * Returns a valid suggested return type iff one is possible while making no changes to the existing parameters. */ @@ -94,170 +83,13 @@ data class SuggestedReturnType( expected.options.filter { it.matchesParams(params) }.ifEmpty { return null } } - val forcedReturnTypesBySig = validSignatures.asSequence() - .flatten() - .mapNotNull { sig -> sig.forcedReturnTypes(params)?.let { sig to it } } - .toMap(IdentityHashMap()) - - val constrainedSignatures = mutableMapOf>() - val unconstrainedSignatures = mutableMapOf>() - val typesByKind = mutableMapOf>() - val preferences = linkedSetOf() - - for ((index, signature) in validSignatures.asSequence().interleaved()) { - val forcedReturnTypes = forcedReturnTypesBySig[signature] - if (forcedReturnTypes != null) { - for (forcedReturnType in forcedReturnTypes) { - val normalized = forcedReturnType.normalize() - constrainedSignatures.getOrPut(normalized, ::mutableMapOf).putIfAbsent(index, signature) - typesByKind.getOrPut(TypeKind.of(normalized), ::linkedSetOf).add(normalized) - preferences.add(ReturnTypePreference.Constrained(normalized)) - } - } else { - val kind = TypeKind.of(signature.returnType) - unconstrainedSignatures.getOrPut(kind, ::mutableMapOf).putIfAbsent(index, signature) - preferences.add(ReturnTypePreference.Unconstrained(kind)) - } - } - - val visitedReturnTypes = mutableSetOf() - - fun tryReturnType(option: PsiType): SuggestedReturnType? { - if (!visitedReturnTypes.add(option)) { - return null - } - val unconstrained = unconstrainedSignatures[TypeKind.of(option)].orEmpty() - val constrained = constrainedSignatures.getValue(option) - val combined = unconstrained + constrained - if (combined.size != signatures.size) { - return null - } - val intersected = intersectCoerce( - params, - combined.values.map { sig -> - val forcedReturnTypes = forcedReturnTypesBySig[sig] - if (forcedReturnTypes == null) { - exact(sig) - } else { - SuggestedReturnType( - forcedReturnTypes.first { it.normalize() == option }, - returnTypeIsIntLike = false, - ) - } - }, - ) ?: return null - - return intersected.takeIf { it.returnType.normalize() == option } - } - - for (preference in preferences) { - when (preference) { - is ReturnTypePreference.Constrained -> { - tryReturnType(preference.type)?.let { return it } - } - is ReturnTypePreference.Unconstrained -> { - val unconstrained = unconstrainedSignatures.getValue(preference.kind) - if (unconstrained.size == signatures.size) { - // Will definitely work - return intersectCoerce( - params, - signatures.indices.map { exact(unconstrained.getValue(it)) }, - )!! - } - val returnTypeOptions = typesByKind[preference.kind] ?: continue - for (option in returnTypeOptions) { - tryReturnType(option)?.let { return it } - } - } - } - } - - return null - } - - /** - * Returns `null` iff the signature's return type can be intersected with all types of its kind. Otherwise, - * returns, in order of preference, the possible return types that this signature can support. - * - * **Precondition:** The parameter list is valid according to the signature. - */ - private fun MethodSignature.forcedReturnTypes(parameterList: PsiParameterList): SequencedSet? { - if (MethodSignature.TypePosition.Return in intLikeTypes) { - val params = parameterList.parameters - val anchor = (intLikeTypes.first() as? MethodSignature.TypePosition.Param) - ?.let { params[it.index].type } - return when (anchor) { - PsiTypes.intType() -> { - // Can be coerced to any int-like type - null - } - null -> { - // The return type itself is the anchor - val intLikeParams = intLikeTypes.asSequence() - .mapNotNull { it.getParam(params) } - .groupBy { it.type } - when { - intLikeParams.isEmpty() -> { - // Only the return type is int-like, free choice - null - } - PsiTypes.intType() in intLikeParams -> { - // Only int can be coerced to int (can also be coerced to anything else we found) - sequencedSetOf(PsiTypes.intType()) - } - 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. - sequencedSetOfNotNull( - intLikeParams.keys.single(), - PsiTypes.intType() - .takeIf { intLikeParams.values.single().all { it.hasAnnotation(COERCE) } }, - ) - } - else -> { - // Only int can be coerced to multiple types - for ((type, params) in intLikeParams) { - // We double-check that the parameters are valid as the caller promised - check(type == PsiTypes.intType() || params.all { it.hasAnnotation(COERCE) }) - } - sequencedSetOf(PsiTypes.intType()) - } - } - } - else -> { - // Anchor is a leaf type which can only be coerced to itself - sequencedSetOf(anchor) - } - } - } - if (TypeKind.of(returnType) == TypeKind.INT_LIKE && returnType != PsiTypes.intType()) { - // Leaf types cannot be intersected with any other type - return sequencedSetOf(returnType) - } - - return sequencedSetOf(returnType).takeUnless { allowCoerceRequired } - } + val solver = ReturnTypeSolver(params) - /** - * Returns the most specific suggested return type that satisfies all the constraints, or `null` if no such type - * exists. - */ - private fun intersectCoerce( - context: PsiElement, - types: List, - ): SuggestedReturnType? { - if (!types.asSequence().map { TypeKind.of(it.returnType) }.allEqual()) { - return null + for (expected in validSignatures) { + solver.addExpected(expected) } - val manager = PsiManager.getInstance(context.project) - return types.asSequence().reduceFallible { acc, it -> acc.intersectCoerce(it, manager) } - } - - private sealed interface ReturnTypePreference { - data class Constrained(val type: PsiType) : ReturnTypePreference - - data class Unconstrained(val kind: TypeKind) : ReturnTypePreference + return solver.solve() } } } @@ -320,7 +152,7 @@ data class SuggestedSignature( ) }.toMutableList() if (intLikeAssignment != null) { - for (pos in signature.intLikeTypes) { + for (pos in signature.intLikePositions) { when (pos) { MethodSignature.TypePosition.Return -> returnType = intLikeAssignment is MethodSignature.TypePosition.Param -> { @@ -332,7 +164,7 @@ data class SuggestedSignature( return SuggestedSignature( params, returnType, - if (intLikeAssignment == null) signature.intLikeTypes else emptySequencedSet(), + if (intLikeAssignment == null) signature.intLikePositions else emptySequencedSet(), ) } @@ -461,7 +293,7 @@ data class SuggestedSignature( ?: return null val intLikeAnchor = run { - signatures.mapNotNull { it.intLikeTypes.firstOrNull() }.toList() + signatures.mapNotNull { it.intLikePositions.firstOrNull() }.toList() .ifEmpty { return@run null } .asSequence() .distinct() @@ -473,7 +305,7 @@ data class SuggestedSignature( val solver = IntLikeAnchorSolver() for (signature in signatures) { for (pos in signature.allPositions(paramsToUse)) { - if (pos in signature.intLikeTypes) { + if (pos in signature.intLikePositions) { continue } val isTrailingParam = pos is MethodSignature.TypePosition.Param && @@ -514,14 +346,48 @@ data class SuggestedSignature( } } } + + 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++ + } + } } } -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) } - /** * Merges the two types with regards to `@Coerce` behaviour. Returns the merged type, whether the merged type is a * free int-like type, and whether the merge **newly** requires `@Coerce`. @@ -562,37 +428,3 @@ private fun mergeTypes( } private data class TypeMergeResult(val type: PsiType, val isIntLike: Boolean, val coerce: Boolean) - -/** - * 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++ - } -} 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..da01a1ad7 --- /dev/null +++ b/src/main/kotlin/platform/mixin/util/ReturnTypeSolver.kt @@ -0,0 +1,232 @@ +/* + * 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.PrioritySet +import com.demonwav.mcdev.util.allEqual +import com.demonwav.mcdev.util.normalize +import com.demonwav.mcdev.util.reduceFallible +import com.intellij.psi.CommonClassNames +import com.intellij.psi.PsiArrayType +import com.intellij.psi.PsiManager +import com.intellij.psi.PsiParameterList +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 suggestionsByType = mutableMapOf>() + 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, ::mutableMapOf).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 { intersectCoerce(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() -> { + // Can be coerced to any int-like type + intReturnOptions + } + 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 (intLikeParams.values.single().all { it.hasAnnotation(COERCE) }) { + results.add( + SuggestedReturnType(PsiTypes.intType()) withPriority 1 + ) + } + results + } + else -> { + // Only int can be coerced to multiple types + for ((type, params) in intLikeParams) { + // We double-check that the parameters are valid as the caller promised + check(type == PsiTypes.intType() || 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 + TypeKind.of(returnType) == TypeKind.OBJECT -> objectReturnOptions(returnType).toList() + else -> listOf(SuggestedReturnType(returnType) withPriority 0) + } + } + + /** + * Returns the most specific suggested return type that satisfies all the constraints, or `null` if no such type + * exists. + */ + private fun intersectCoerce(types: Iterable): SuggestedReturnType? { + if (!types.asSequence().map { TypeKind.of(it.returnType) }.allEqual()) { + return null + } + + return types.asSequence().reduceFallible { acc, it -> acc.intersectCoerce(it, manager) } + } + + /** + * Yields all 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. [Object] is given maximum priority (unless [type] itself is + * [Object]) to express the fact that it is the least specific type. + */ + private fun objectReturnOptions(type: PsiType): Sequence> = sequence { + val queue = ArrayDeque(listOf(SuggestedReturnType(type) withPriority 0)) + val visited = hashSetOf(type.normalize()) + + while (queue.isNotEmpty()) { + val (next, priority) = queue.removeFirst().also { yield(it) } + for (directSuper in next.returnType.directSupertypes()) { + if (directSuper.equalsToText(CommonClassNames.JAVA_LANG_OBJECT)) { + // Will handle at the end + continue + } + if (visited.add(directSuper.normalize())) { + queue.addLast(SuggestedReturnType(directSuper, coerceReturnType = true) withPriority priority + 1) + } + } + } + + val javaLangObject = PsiType.getJavaLangObject(manager, parameterList.resolveScope) + if (visited.add(javaLangObject)) { + // Always the least specific option + yield(SuggestedReturnType(javaLangObject) withPriority Int.MAX_VALUE) + } + } + + private fun PsiType.directSupertypes(): List = when (this) { + is PsiArrayType -> superTypes.asList() + arraySuperTypes.map { + PsiType.getTypeByName(it, manager.project, parameterList.resolveScope) + } + else -> superTypes.asList() + } + + 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_LANG_OBJECT, + CommonClassNames.JAVA_IO_SERIALIZABLE, + CommonClassNames.JAVA_LANG_CLONEABLE, + ) + } +} + +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/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() +} From d49a8a4fcf26099e61e2dcb0e267d13beb46764a Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Sat, 26 Sep 2026 21:10:19 +0100 Subject: [PATCH 25/51] Cache walked supertypes and improve handling of Object as a supertype. --- .../platform/mixin/util/ReturnTypeSolver.kt | 69 +++++++++++-------- src/main/kotlin/util/sequences.kt | 17 ----- 2 files changed, 41 insertions(+), 45 deletions(-) diff --git a/src/main/kotlin/platform/mixin/util/ReturnTypeSolver.kt b/src/main/kotlin/platform/mixin/util/ReturnTypeSolver.kt index da01a1ad7..a7a36a8e8 100644 --- a/src/main/kotlin/platform/mixin/util/ReturnTypeSolver.kt +++ b/src/main/kotlin/platform/mixin/util/ReturnTypeSolver.kt @@ -23,6 +23,7 @@ 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.allEqual import com.demonwav.mcdev.util.normalize @@ -31,6 +32,7 @@ import com.intellij.psi.CommonClassNames 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 @@ -39,7 +41,8 @@ import com.intellij.psi.PsiTypes */ class ReturnTypeSolver(private val parameterList: PsiParameterList) { private val manager = PsiManager.getInstance(parameterList.project) - private val suggestionsByType = mutableMapOf>() + private val objectReturnOptionsCache = hashMapOf>>() + private val suggestionsByType = hashMapOf>() private val typeOptions = PrioritySet() private var numExpected = 0 @@ -52,7 +55,7 @@ class ReturnTypeSolver(private val parameterList: PsiParameterList) { for ((option, priority) in signature.returnTypeOptions()) { val key = TypeKey.of(option) typeOptions.add(key, priority) - suggestionsByType.getOrPut(key, ::mutableMapOf).putIfAbsent(expectedIndex, option) + suggestionsByType.getOrPut(key, ::linkedMapOf).putIfAbsent(expectedIndex, option) } } } @@ -135,7 +138,7 @@ class ReturnTypeSolver(private val parameterList: PsiParameterList) { } return when { returnType == PsiTypes.intType() -> intReturnOptions - TypeKind.of(returnType) == TypeKind.OBJECT -> objectReturnOptions(returnType).toList() + TypeKind.of(returnType) == TypeKind.OBJECT -> objectReturnOptions(returnType) else -> listOf(SuggestedReturnType(returnType) withPriority 0) } } @@ -154,38 +157,49 @@ class ReturnTypeSolver(private val parameterList: PsiParameterList) { /** * Yields all 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. [Object] is given maximum priority (unless [type] itself is - * [Object]) to express the fact that it is the least specific type. + * of traversal steps required to reach the supertype. */ - private fun objectReturnOptions(type: PsiType): Sequence> = sequence { - val queue = ArrayDeque(listOf(SuggestedReturnType(type) withPriority 0)) - val visited = hashSetOf(type.normalize()) - - while (queue.isNotEmpty()) { - val (next, priority) = queue.removeFirst().also { yield(it) } - for (directSuper in next.returnType.directSupertypes()) { - if (directSuper.equalsToText(CommonClassNames.JAVA_LANG_OBJECT)) { - // Will handle at the end - continue - } - if (visited.add(directSuper.normalize())) { - queue.addLast(SuggestedReturnType(directSuper, coerceReturnType = true) withPriority priority + 1) + private fun objectReturnOptions(type: PsiType): List> = + 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 + ) + } } } - } - val javaLangObject = PsiType.getJavaLangObject(manager, parameterList.resolveScope) - if (visited.add(javaLangObject)) { - // Always the least specific option - yield(SuggestedReturnType(javaLangObject) withPriority Int.MAX_VALUE) + result } - } private fun PsiType.directSupertypes(): List = when (this) { - is PsiArrayType -> superTypes.asList() + arraySuperTypes.map { - PsiType.getTypeByName(it, manager.project, parameterList.resolveScope) + 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) } + } } - else -> superTypes.asList() } private companion object { @@ -206,7 +220,6 @@ class ReturnTypeSolver(private val parameterList: PsiParameterList) { } private val arraySuperTypes = listOf( - CommonClassNames.JAVA_LANG_OBJECT, CommonClassNames.JAVA_IO_SERIALIZABLE, CommonClassNames.JAVA_LANG_CLONEABLE, ) diff --git a/src/main/kotlin/util/sequences.kt b/src/main/kotlin/util/sequences.kt index 1c6468e95..398ea3617 100644 --- a/src/main/kotlin/util/sequences.kt +++ b/src/main/kotlin/util/sequences.kt @@ -20,8 +20,6 @@ package com.demonwav.mcdev.util -import java.util.LinkedList - inline fun Sequence.toTypedArray(): Array { return toList().toTypedArray() } @@ -61,18 +59,3 @@ 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>.interleaved(): Sequence> = sequence { - val iterators = this@interleaved.mapIndexedTo(LinkedList()) { i, it -> IndexedValue(i, it.iterator()) } - while (iterators.isNotEmpty()) { - val outer = iterators.iterator() - while (outer.hasNext()) { - val (index, inner) = outer.next() - if (inner.hasNext()) { - yield(IndexedValue(index, inner.next())) - } else { - outer.remove() - } - } - } -} From fbbfabc4e0a36758dc828315d9912c28b2de3ba9 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Sat, 26 Sep 2026 21:59:33 +0100 Subject: [PATCH 26/51] Cleanup --- .../inspection/injector/SuggestedSignature.kt | 92 ++++++++----------- .../platform/mixin/util/ReturnTypeSolver.kt | 28 ++++-- src/main/kotlin/util/sequences.kt | 28 ++++++ 3 files changed, 84 insertions(+), 64 deletions(-) diff --git a/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt index 52cffd817..36a5a20db 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt @@ -29,6 +29,8 @@ 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 @@ -58,22 +60,6 @@ data class SuggestedReturnType( if (returnTypeIsIntLike) sequencedSetOf(MethodSignature.TypePosition.Return) else emptySequencedSet() override val params get() = null - fun intersectCoerce(other: SuggestedReturnType, manager: PsiManager): SuggestedReturnType? { - val (returnType, returnTypeIsIntLike, coerceReturnType) = mergeTypes( - manager, - this.returnType, - other.returnType, - this.returnTypeIsIntLike, - other.returnTypeIsIntLike, - ) ?: return null - - return SuggestedReturnType( - returnType, - returnTypeIsIntLike, - this.coerceReturnType || other.coerceReturnType || coerceReturnType, - ) - } - companion object { /** * Returns a valid suggested return type iff one is possible while making no changes to the existing parameters. @@ -187,7 +173,7 @@ data class SuggestedSignature( val chosenParams = forExistingReturnType?.takeIf { it.size == parameterOptions.size } ?: optionsByType.values.firstOrNull { it.size == parameterOptions.size } ?: return null - val name = chosenParams.asSequence().map { it.name }.distinct().singleOrNull() ?: "original" + 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 } @@ -288,18 +274,16 @@ data class SuggestedSignature( } } .asSequence() - .distinct() - .singleOrNull() + .singleDistinctOrNull() ?: return null val intLikeAnchor = run { signatures.mapNotNull { it.intLikePositions.firstOrNull() }.toList() .ifEmpty { return@run null } .asSequence() - .distinct() // With the currently available signature shapes, there can only ever be 1 anchor. // This logic will need revisiting if that changes. - .single() + .singleDistinct() } val intLikeAssignment = intLikeAnchor?.let { val solver = IntLikeAnchorSolver() @@ -346,45 +330,45 @@ data class SuggestedSignature( } } } + } +} - 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) } +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 - } +/** + * 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 - } - } + if (candidateKind == TypeKind.INT_LIKE && candidate != PsiTypes.intType()) { + when (forcedIntType) { + null -> forcedIntType = candidate + candidate -> {} + else -> return i } - - i++ } } + + i++ } } diff --git a/src/main/kotlin/platform/mixin/util/ReturnTypeSolver.kt b/src/main/kotlin/platform/mixin/util/ReturnTypeSolver.kt index a7a36a8e8..a14120218 100644 --- a/src/main/kotlin/platform/mixin/util/ReturnTypeSolver.kt +++ b/src/main/kotlin/platform/mixin/util/ReturnTypeSolver.kt @@ -25,10 +25,10 @@ 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.allEqual import com.demonwav.mcdev.util.normalize -import com.demonwav.mcdev.util.reduceFallible +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 @@ -66,7 +66,7 @@ class ReturnTypeSolver(private val parameterList: PsiParameterList) { fun solve(): SuggestedReturnType? { val suggestions = typeOptions.asSequence() .map { suggestionsByType.getValue(it) }.firstOrNull { it.size == numExpected }?.values - return suggestions?.let { intersectCoerce(it)!! } + return suggestions?.let { intersect(it) } } /** @@ -144,15 +144,23 @@ class ReturnTypeSolver(private val parameterList: PsiParameterList) { } /** - * Returns the most specific suggested return type that satisfies all the constraints, or `null` if no such type - * exists. + * 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 intersectCoerce(types: Iterable): SuggestedReturnType? { - if (!types.asSequence().map { TypeKind.of(it.returnType) }.allEqual()) { - return null + 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 types.asSequence().reduceFallible { acc, it -> acc.intersectCoerce(it, manager) } + return SuggestedReturnType( + mergedType, + returnTypeIsIntLike = suggestions.asSequence().map { it.returnTypeIsIntLike }.singleDistinct(), + coerceReturnType = suggestions.any { it.coerceReturnType }, + ) } /** diff --git a/src/main/kotlin/util/sequences.kt b/src/main/kotlin/util/sequences.kt index 398ea3617..0baef085e 100644 --- a/src/main/kotlin/util/sequences.kt +++ b/src/main/kotlin/util/sequences.kt @@ -59,3 +59,31 @@ 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 (element != first) { + 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 (element != first) { + return null + } + } + return first +} From 2bff39a4efc4914d0828ea8bb7864105d8820230 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Sat, 26 Sep 2026 22:33:32 +0100 Subject: [PATCH 27/51] Cleanup --- src/main/kotlin/platform/mixin/handlers/ModifyArgsHandler.kt | 2 +- .../platform/mixin/handlers/mixinextras/WrapMethodHandler.kt | 2 +- .../platform/mixin/inspection/injector/MethodSignatures.kt | 2 +- src/main/kotlin/util/sequenced-collections.kt | 4 ---- src/main/kotlin/util/utils.kt | 1 - 5 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyArgsHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyArgsHandler.kt index 2157606ed..5d7541d64 100644 --- a/src/main/kotlin/platform/mixin/handlers/ModifyArgsHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/ModifyArgsHandler.kt @@ -50,7 +50,7 @@ class ModifyArgsHandler : InsnInjectorAnnotationHandler() { targetClass: ClassNode, targetMethod: MethodNode, targetInsn: TargetInsn, - ): ExpectedSignatures<*> { + ): ExpectedSignatures { val argsType = JavaPsiFacade.getElementFactory(annotation.project) .createTypeByFQClassName(ARGS, annotation.resolveScope) val shortParams = listOf(Parameter("args", argsType)) diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt index 8df023c30..9d60b578b 100644 --- a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt @@ -41,7 +41,7 @@ class WrapMethodHandler : InjectorAnnotationHandler() { annotation: PsiAnnotation, targets: List, mode: CollectVisitor.Mode, - ): List> { + ): List> { return targets.map { (targetClass, targetMethod) -> val returnType = targetMethod.getGenericReturnType(targetClass, annotation.project) diff --git a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt index c30bfffe8..69029032d 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt @@ -52,7 +52,7 @@ sealed interface ExpectedSignatures { } } -inline fun List>.collectSignatures(): List? = +inline fun List>.collectSignatures(): List? = mapNotNull { when (it) { ExpectedSignatures.Unknown -> null diff --git a/src/main/kotlin/util/sequenced-collections.kt b/src/main/kotlin/util/sequenced-collections.kt index 1844bf034..bc30ff97f 100644 --- a/src/main/kotlin/util/sequenced-collections.kt +++ b/src/main/kotlin/util/sequenced-collections.kt @@ -32,10 +32,6 @@ fun emptySequencedSet(): SequencedSet = EMPTY_SEQUENCED_SET fun sequencedSetOf(vararg elements: T): SequencedSet = SequencedSet(linkedSetOf(*elements)) -fun sequencedSetOfNotNull(vararg elements: T?): SequencedSet { - return SequencedSet(elements.filterNotNullTo(linkedSetOf())) -} - inline fun buildSequencedSet(action: MutableSequencedSet.() -> Unit): SequencedSet = SequencedSet(linkedSetOf().apply(action)) diff --git a/src/main/kotlin/util/utils.kt b/src/main/kotlin/util/utils.kt index 7c4612507..b2676cf6d 100644 --- a/src/main/kotlin/util/utils.kt +++ b/src/main/kotlin/util/utils.kt @@ -47,7 +47,6 @@ 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 From b89aeb77e75f78d1d7e9a552f60fbd7334d30dab Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Sat, 26 Sep 2026 23:50:43 +0100 Subject: [PATCH 28/51] Small fixes --- .../mixin/inspection/injector/SuggestedSignature.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt index 36a5a20db..86a2157d4 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt @@ -294,10 +294,12 @@ data class SuggestedSignature( } val isTrailingParam = pos is MethodSignature.TypePosition.Param && pos.index > signature.requiredParams.lastIndex + val allowCoerce = + if (isTrailingParam) signature.allowCoerceTrailing else signature.allowCoerceRequired val isValid = solver.constrain( pos.getType(signature), isAnchor = pos == intLikeAnchor, - isHard = if (isTrailingParam) signature.allowCoerceTrailing else signature.allowCoerceRequired, + isHard = !allowCoerce, ) if (!isValid) { return null @@ -309,7 +311,7 @@ data class SuggestedSignature( val suggestedSignatures = signatures.map { exact(it, takeTrailing = paramsToUse - it.requiredParams.size, intLikeAssignment = intLikeAssignment) - }.toList() + } if (suggestedSignatures.asSequence().zipWithNext { a, b -> !kindsMatch(a, b) }.any { it }) { return null From ecefb0d67945182f8152f4695caae53cbbdac5e5 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Sun, 27 Sep 2026 00:11:46 +0100 Subject: [PATCH 29/51] Small fixes --- .../mixin/util/IntLikeAnchorSolver.kt | 6 +- .../platform/mixin/util/ReturnTypeSolver.kt | 66 +++++++++++-------- src/main/kotlin/util/sequences.kt | 4 +- 3 files changed, 42 insertions(+), 34 deletions(-) diff --git a/src/main/kotlin/platform/mixin/util/IntLikeAnchorSolver.kt b/src/main/kotlin/platform/mixin/util/IntLikeAnchorSolver.kt index 35dccc6ba..814bdde71 100644 --- a/src/main/kotlin/platform/mixin/util/IntLikeAnchorSolver.kt +++ b/src/main/kotlin/platform/mixin/util/IntLikeAnchorSolver.kt @@ -38,11 +38,11 @@ class IntLikeAnchorSolver { fun constrain(desiredType: PsiType, isAnchor: Boolean, isHard: Boolean): Boolean { val type = IntType.of(desiredType) ?: return false when { - isHard && (isAnchor || type == IntType.INT) -> candidates.retainAll(setOf(type)) + isHard && (isAnchor || type == IntType.INT) -> candidates.retainAll(EnumSet.of(type)) type == IntType.INT -> hasIntPreference = true - isAnchor -> candidates.retainAll(setOf(type)) + isAnchor -> candidates.retainAll(EnumSet.of(type)) else -> { - candidates.retainAll(setOf(type, IntType.INT)) + candidates.retainAll(EnumSet.of(type, IntType.INT)) hasLeafPreference = true } } diff --git a/src/main/kotlin/platform/mixin/util/ReturnTypeSolver.kt b/src/main/kotlin/platform/mixin/util/ReturnTypeSolver.kt index a14120218..0f8674ffe 100644 --- a/src/main/kotlin/platform/mixin/util/ReturnTypeSolver.kt +++ b/src/main/kotlin/platform/mixin/util/ReturnTypeSolver.kt @@ -81,8 +81,7 @@ class ReturnTypeSolver(private val parameterList: PsiParameterList) { ?.let { params[it.index].type } return when (anchor) { PsiTypes.intType() -> { - // Can be coerced to any int-like type - intReturnOptions + intReturnOptions(allowCoerceRequired) } null -> { // The return type itself is the anchor @@ -113,7 +112,7 @@ class ReturnTypeSolver(private val parameterList: PsiParameterList) { val results = mutableListOf( SuggestedReturnType(leaf) withPriority 0 ) - if (intLikeParams.values.single().all { it.hasAnnotation(COERCE) }) { + if (allowCoerceRequired && intLikeParams.values.single().all { it.hasAnnotation(COERCE) }) { results.add( SuggestedReturnType(PsiTypes.intType()) withPriority 1 ) @@ -122,9 +121,11 @@ class ReturnTypeSolver(private val parameterList: PsiParameterList) { } else -> { // Only int can be coerced to multiple types - for ((type, params) in intLikeParams) { - // We double-check that the parameters are valid as the caller promised - check(type == PsiTypes.intType() || params.all { it.hasAnnotation(COERCE) }) + 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) } @@ -137,8 +138,8 @@ class ReturnTypeSolver(private val parameterList: PsiParameterList) { } } return when { - returnType == PsiTypes.intType() -> intReturnOptions - TypeKind.of(returnType) == TypeKind.OBJECT -> objectReturnOptions(returnType) + returnType == PsiTypes.intType() -> intReturnOptions(allowCoerceRequired) + TypeKind.of(returnType) == TypeKind.OBJECT -> objectReturnOptions(returnType, allowCoerceRequired) else -> listOf(SuggestedReturnType(returnType) withPriority 0) } } @@ -164,31 +165,35 @@ class ReturnTypeSolver(private val parameterList: PsiParameterList) { } /** - * Yields all 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. + * 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): List> = - 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 - ) + 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 + result + } } private fun PsiType.directSupertypes(): List = when (this) { @@ -231,6 +236,9 @@ class ReturnTypeSolver(private val parameterList: PsiParameterList) { CommonClassNames.JAVA_IO_SERIALIZABLE, CommonClassNames.JAVA_LANG_CLONEABLE, ) + + private fun intReturnOptions(allowCoerce: Boolean): List> = + if (allowCoerce) intReturnOptions else listOf(SuggestedReturnType(PsiTypes.intType()) withPriority 0) } } diff --git a/src/main/kotlin/util/sequences.kt b/src/main/kotlin/util/sequences.kt index 0baef085e..b02c72ec9 100644 --- a/src/main/kotlin/util/sequences.kt +++ b/src/main/kotlin/util/sequences.kt @@ -67,7 +67,7 @@ fun Sequence.singleDistinct(): T { } val first = iter.next() for (element in iter) { - if (element != first) { + if (first != element) { throw IllegalArgumentException("Sequence has more than one distinct element.") } } @@ -81,7 +81,7 @@ fun Sequence.singleDistinctOrNull(): T? { } val first = iter.next() for (element in iter) { - if (element != first) { + if (first != element) { return null } } From a0c6698f50062d48f5aea9d9b88c9dc842fb09ec Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Sun, 27 Sep 2026 22:22:35 +0100 Subject: [PATCH 30/51] Fix: Suggest "ci" for coerced callbackinfo name --- .../InvalidInjectorMethodSignatureInspection.kt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt b/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt index 0520ad991..2c94bd86c 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt @@ -27,6 +27,7 @@ import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.CollectVisitor import com.demonwav.mcdev.platform.mixin.inspection.MixinInspection 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.isConstructor @@ -68,6 +69,7 @@ import com.intellij.psi.PsiMethod import com.intellij.psi.PsiModifier import com.intellij.psi.PsiNameHelper import com.intellij.psi.PsiParameterList +import com.intellij.psi.PsiType import com.intellij.psi.codeStyle.VariableKind import com.intellij.psi.util.PsiTreeUtil import com.intellij.psi.util.PsiUtil @@ -268,8 +270,9 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() { val languageLevel = PsiUtil.getLanguageLevel(parameters) val usedNames = mutableSetOf() - val newParams = suggestedParams.mapIndexedTo(mutableListOf()) { i, p -> + 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) @@ -364,6 +367,13 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() { } return builder.buildInlineTemplate() } + + private companion object { + fun suggestedParamNames(type: PsiType): List = when { + type.equalsToText(CALLBACK_INFO) -> listOf("ci") + else -> emptyList() + } + } } } From 385c821d94b32baa5d9bbc233dee897b2af6c0df Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Sun, 27 Sep 2026 23:03:44 +0100 Subject: [PATCH 31/51] Make highlight pos consistent --- .../InvalidInjectorMethodSignatureInspection.kt | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt b/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt index 2c94bd86c..f535c28b4 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt @@ -188,24 +188,28 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() { val signatureSuggestion = signatureOptions?.let { SuggestedReturnType.forParams(parameters, it) } ?: handler.suggestedMethodSignature(annotation, targetMethods) + val declarationStart = (method.returnTypeElement ?: identifier).startOffsetInParent + val declarationEnd = method.parameterList.textRangeInParent.endOffset + if (signatureSuggestion == null) { holder.registerProblem( - parameters, + 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) - val declarationStart = (method.returnTypeElement ?: identifier).startOffsetInParent - val declarationEnd = method.parameterList.textRangeInParent.endOffset + holder.registerProblem( method, description, ProblemHighlightType.GENERIC_ERROR_OR_WARNING, TextRange.create(declarationStart, declarationEnd), - quickFix + quickFix, ) } } From d99a252d3cd4b64ffaa2f0deec5400874f568515 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Sun, 27 Sep 2026 23:19:57 +0100 Subject: [PATCH 32/51] Switch to fabric mixin for tests --- gradle/libs.versions.toml | 2 +- src/test/kotlin/platform/mixin/BaseMixinTest.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/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") } From 4137742ff8a3afe3633f4579a81303b43a656446 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Sun, 27 Sep 2026 23:38:22 +0100 Subject: [PATCH 33/51] Some tests --- .../MixedInComplex.java | 49 ++++++ .../MixedInModifyArg.java | 38 +++++ .../MixedInModifyVariable.java | 41 +++++ src/test/kotlin/framework/test-util.kt | 30 ++++ .../InvalidInjectorMethodSignatureFixTest.kt | 62 ++++--- ...idInjectorMethodSignatureInspectionTest.kt | 156 ++++++++++++++++++ .../{ => inject}/genericCase.after.java | 0 .../{ => inject}/genericCase.java | 0 .../genericCaseComplexReturnType.after.java | 0 .../genericCaseComplexReturnType.java | 0 .../innerCtorWithLocals.after.java | 0 .../{ => inject}/innerCtorWithLocals.java | 0 .../inject/multiTarget.after.java | 43 +++++ .../inject/multiTarget.java | 41 +++++ .../{ => inject}/simpleCase.after.java | 0 .../{ => inject}/simpleCase.java | 0 .../simpleCaseWithMixinExtrasSugar.after.java | 0 .../simpleCaseWithMixinExtrasSugar.java | 0 .../{ => inject}/simpleInnerCtor.after.java | 0 .../{ => inject}/simpleInnerCtor.java | 0 .../simpleMethodWithInnerType.after.java | 0 .../simpleMethodWithInnerType.java | 0 .../withCapturedLocals.after.java | 0 .../{ => inject}/withCapturedLocals.java | 0 .../withoutCI.after.java} | 0 .../withoutCI.java} | 0 .../modifyArg.after.java | 42 +++++ .../modifyArg.java | 36 ++++ .../modifyVariable.after.java | 39 +++++ .../modifyVariable.java | 33 ++++ .../modifyargs/returnTypeOnly.after.java | 15 ++ .../modifyargs/returnTypeOnly.java | 15 ++ .../simple.after.java} | 0 .../simple.java} | 0 34 files changed, 620 insertions(+), 20 deletions(-) create mode 100644 mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInComplex.java create mode 100644 mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInModifyArg.java create mode 100644 mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInModifyVariable.java rename src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/{ => inject}/genericCase.after.java (100%) rename src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/{ => inject}/genericCase.java (100%) rename src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/{ => inject}/genericCaseComplexReturnType.after.java (100%) rename src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/{ => inject}/genericCaseComplexReturnType.java (100%) rename src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/{ => inject}/innerCtorWithLocals.after.java (100%) rename src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/{ => inject}/innerCtorWithLocals.java (100%) create mode 100644 src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/inject/multiTarget.after.java create mode 100644 src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/inject/multiTarget.java rename src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/{ => inject}/simpleCase.after.java (100%) rename src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/{ => inject}/simpleCase.java (100%) rename src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/{ => inject}/simpleCaseWithMixinExtrasSugar.after.java (100%) rename src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/{ => inject}/simpleCaseWithMixinExtrasSugar.java (100%) rename src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/{ => inject}/simpleInnerCtor.after.java (100%) rename src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/{ => inject}/simpleInnerCtor.java (100%) rename src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/{ => inject}/simpleMethodWithInnerType.after.java (100%) rename src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/{ => inject}/simpleMethodWithInnerType.java (100%) rename src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/{ => inject}/withCapturedLocals.after.java (100%) rename src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/{ => inject}/withCapturedLocals.java (100%) rename src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/{injectWithoutCI.after.java => inject/withoutCI.after.java} (100%) rename src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/{injectWithoutCI.java => inject/withoutCI.java} (100%) create mode 100644 src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/modifyArg.after.java create mode 100644 src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/modifyArg.java create mode 100644 src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/modifyVariable.after.java create mode 100644 src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/modifyVariable.java create mode 100644 src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/modifyargs/returnTypeOnly.after.java create mode 100644 src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/modifyargs/returnTypeOnly.java rename src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/{modifyArgs.after.java => modifyargs/simple.after.java} (100%) rename src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/{modifyArgs.java => modifyargs/simple.java} (100%) 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..1f10da037 --- /dev/null +++ b/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInComplex.java @@ -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.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; + } +} 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/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/src/test/kotlin/framework/test-util.kt b/src/test/kotlin/framework/test-util.kt index 64dae6275..ca96f16be 100644 --- a/src/test/kotlin/framework/test-util.kt +++ b/src/test/kotlin/framework/test-util.kt @@ -28,6 +28,7 @@ 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 +129,32 @@ 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) + + 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 + + fixture.launchAction(fix) + } + + Assertions.assertTrue(lastFixedRange != null, "No fixes applied") + + fixture.checkResult(expected) +} diff --git a/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt index b15be00e7..fdab8387c 100644 --- a/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt +++ b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt @@ -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,69 @@ 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") - fun modifyArgs() = doTest("modifyArgs") + @DisplayName("ModifyArgs return type only") + fun modifyArgsReturnTypeOnly() = doSingleTest("modifyargs/returnTypeOnly") @Test - @DisplayName("Generic method") - fun genericCase() = doTest("genericCase") + @DisplayName("Inject multi-target") + fun injectMultiTarget() = doMultiTest("inject/multiTarget") @Test - @DisplayName("Generic method complex return type") - fun genericCaseComplexReturnType() = doTest("genericCaseComplexReturnType") + @DisplayName("ModifyArg") + fun modifyArg() = doMultiTest("modifyArg") @Test - @DisplayName("Simple method with inner type") - fun simpleMethodWithInnerType() = doTest("simpleMethodWithInnerType") + @DisplayName("ModifyVariable") + fun modifyVariable() = doMultiTest("modifyVariable") } diff --git a/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt index 2f210197e..5d46584fb 100644 --- a/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt +++ b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt @@ -163,4 +163,160 @@ 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; + } + } + """, + ) + } } 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..bee209756 --- /dev/null +++ b/src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/inject/multiTarget.after.java @@ -0,0 +1,43 @@ +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) +public 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 = {"method1", "method3"}, at = @At("RETURN")) + private void inject7(@Coerce Object arg, @Coerce 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..2474df5ab --- /dev/null +++ b/src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/inject/multiTarget.java @@ -0,0 +1,41 @@ +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; + +import java.io.Serializable; + +@Mixin(MixedInComplex.class) +public 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 = {"method1", "method3"}, at = @At("RETURN")) + private String inject7(@Coerce Object arg, @Coerce 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 100% 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 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 100% 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 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 100% 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 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..6a3d0f9bb --- /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) +public 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..e1535947b --- /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) +public 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/modifyVariable.after.java b/src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/modifyVariable.after.java new file mode 100644 index 000000000..2e8049fb9 --- /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) +public 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..b72fd01b3 --- /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) +public 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 100% 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 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 100% 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 From a96a8f84f2fb3a8510f34547a0bb0af5c9f25069 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Mon, 28 Sep 2026 14:34:39 +0100 Subject: [PATCH 34/51] Assert no issues after multi-fix tests --- src/test/kotlin/framework/test-util.kt | 4 ++++ .../inject/multiTarget.after.java | 2 +- .../invalidInjectorMethodSignature/inject/multiTarget.java | 2 +- .../mixin/invalidInjectorMethodSignature/modifyArg.after.java | 2 +- .../mixin/invalidInjectorMethodSignature/modifyArg.java | 2 +- .../invalidInjectorMethodSignature/modifyVariable.after.java | 2 +- .../mixin/invalidInjectorMethodSignature/modifyVariable.java | 2 +- 7 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/test/kotlin/framework/test-util.kt b/src/test/kotlin/framework/test-util.kt index ca96f16be..e98783fd2 100644 --- a/src/test/kotlin/framework/test-util.kt +++ b/src/test/kotlin/framework/test-util.kt @@ -23,6 +23,7 @@ package com.demonwav.mcdev.framework 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 @@ -154,6 +155,9 @@ fun testAllInspectionFixes(fixture: JavaCodeInsightTestFixture, basePath: String fixture.launchAction(fix) } + 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/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 index bee209756..42b8379af 100644 --- 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 @@ -12,7 +12,7 @@ import java.io.Serializable; @Mixin(MixedInComplex.class) -public class TestMixin { +class TestMixin { @Inject(method = {"method1", "method2"}, at = @At("RETURN")) private void inject1(CallbackInfo ci) { } 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 index 2474df5ab..3d1770529 100644 --- 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 @@ -10,7 +10,7 @@ import java.io.Serializable; @Mixin(MixedInComplex.class) -public class TestMixin { +class TestMixin { @Inject(method = {"method1", "method2"}, at = @At("RETURN")) private void inject1() { } 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 index 6a3d0f9bb..785275771 100644 --- 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 @@ -6,7 +6,7 @@ import org.spongepowered.asm.mixin.injection.ModifyArg; @Mixin(MixedInModifyArg.class) -public class TestMixin { +class TestMixin { @ModifyArg(method = "caller", at = @At(value = "INVOKE", target = "callee1")) private String test1(String x) { return x; 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 index e1535947b..1359d438c 100644 --- a/src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/modifyArg.java +++ b/src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/modifyArg.java @@ -6,7 +6,7 @@ import org.spongepowered.asm.mixin.injection.ModifyArg; @Mixin(MixedInModifyArg.class) -public class TestMixin { +class TestMixin { @ModifyArg(method = "caller", at = @At(value = "INVOKE", target = "callee1")) private void test1() { } 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 index 2e8049fb9..b3b35d44a 100644 --- 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 @@ -6,7 +6,7 @@ import org.spongepowered.asm.mixin.injection.ModifyVariable; @Mixin(MixedInModifyVariable.class) -public class TestMixin { +class TestMixin { @ModifyVariable(method = "method1", at = @At("RETURN")) private String test1(String arg) { return arg; 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 index b72fd01b3..dd9f1e2cc 100644 --- a/src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/modifyVariable.java +++ b/src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/modifyVariable.java @@ -6,7 +6,7 @@ import org.spongepowered.asm.mixin.injection.ModifyVariable; @Mixin(MixedInModifyVariable.class) -public class TestMixin { +class TestMixin { @ModifyVariable(method = "method1", at = @At("RETURN")) private void test1() { } From 825104199db2b5079b8ccb67f3b667fd62bea7e0 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Mon, 28 Sep 2026 14:38:01 +0100 Subject: [PATCH 35/51] Remove `allowCoerceTrailing`. We always allow this. --- .../platform/mixin/inspection/injector/MethodSignature.kt | 3 +-- .../mixin/inspection/injector/SuggestedSignature.kt | 6 ++---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt index cedb1799c..42d3cd34c 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt @@ -39,7 +39,6 @@ data class MethodSignature( val returnType: PsiType, val allowCoerceRequired: Boolean, val trailingParams: List = emptyList(), - val allowCoerceTrailing: Boolean = true, val intLikePositions: SequencedSet = emptySequencedSet(), ) { sealed interface TypePosition { @@ -191,7 +190,7 @@ data class MethodSignature( params.getOrNull(index)?.let(paramType) }.allEqual() && matchParams(transformedRequiredParams, allowCoerceRequired, 0) - && matchParams(trailingParams, allowCoerceTrailing, requiredParams.size) + && matchParams(trailingParams, true, requiredParams.size) } private companion object { diff --git a/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt index 86a2157d4..e9be0a100 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt @@ -294,8 +294,7 @@ data class SuggestedSignature( } val isTrailingParam = pos is MethodSignature.TypePosition.Param && pos.index > signature.requiredParams.lastIndex - val allowCoerce = - if (isTrailingParam) signature.allowCoerceTrailing else signature.allowCoerceRequired + val allowCoerce = isTrailingParam || signature.allowCoerceRequired val isValid = solver.constrain( pos.getType(signature), isAnchor = pos == intLikeAnchor, @@ -327,8 +326,7 @@ data class SuggestedSignature( // check will fail: return intersected.takeIf { signatures.all { - it.allowCoerceRequired && (intersected.params.size <= it.requiredParams.size || it.allowCoerceTrailing) - || it.matches(intersected) + it.allowCoerceRequired || it.matches(intersected) } } } From 18b7d9dffd35203d99d4dcffbd889f17360fa531 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Mon, 28 Sep 2026 15:09:43 +0100 Subject: [PATCH 36/51] Fix @Coerce return type being added to wrong psi element. --- .../injector/InvalidInjectorMethodSignatureInspection.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt b/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt index f535c28b4..4f55bfc33 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt @@ -322,7 +322,7 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() { existingCoerce == null && needsCoerce -> { val annotation = JavaPsiFacade.getElementFactory(project) .createAnnotationFromText("@$COERCE", returnTypeElement); - { returnTypeElement.addBefore(annotation, returnTypeElement.firstChild) } + { method.modifierList.add(annotation) } } else -> return From b80064933da0c123dc874537afd44aa60f8b011d Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Mon, 28 Sep 2026 15:21:05 +0100 Subject: [PATCH 37/51] Fix: WrapMethod cannot always be static. --- .../platform/mixin/handlers/mixinextras/WrapMethodHandler.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt index 9d60b578b..06f5ffc27 100644 --- a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt @@ -32,6 +32,7 @@ import com.demonwav.mcdev.platform.mixin.util.findSourceElement import com.demonwav.mcdev.platform.mixin.util.getGenericReturnType import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiElement +import com.intellij.psi.PsiMethod import com.intellij.psi.search.GlobalSearchScope import org.objectweb.asm.tree.ClassNode import org.objectweb.asm.tree.MethodNode @@ -87,4 +88,6 @@ class WrapMethodHandler : InjectorAnnotationHandler() { canDecompile = true )?.let(::listOf).orEmpty() } + + override fun canAlwaysBeStatic(method: PsiMethod) = false } From afd6a955714e2aa4fc269d97afd87ce1cff08e2e Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Mon, 28 Sep 2026 15:44:14 +0100 Subject: [PATCH 38/51] WrapMethod tests --- .../MixedInWrapMethod.java | 54 +++++++++++++++++++ .../InvalidInjectorMethodSignatureFixTest.kt | 4 ++ ...idInjectorMethodSignatureInspectionTest.kt | 26 +++++++++ .../wrapMethod.after.java | 34 ++++++++++++ .../wrapMethod.java | 30 +++++++++++ 5 files changed, 148 insertions(+) create mode 100644 mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInWrapMethod.java create mode 100644 src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/wrapMethod.after.java create mode 100644 src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/wrapMethod.java 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/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt index fdab8387c..2e8904afc 100644 --- a/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt +++ b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt @@ -97,4 +97,8 @@ class InvalidInjectorMethodSignatureFixTest : BaseMixinTest() { @Test @DisplayName("ModifyVariable") fun modifyVariable() = doMultiTest("modifyVariable") + + @Test + @DisplayName("WrapMethod") + fun wrapMethod() = doMultiTest("wrapMethod") } diff --git a/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt index 5d46584fb..ff4dd8a42 100644 --- a/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt +++ b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt @@ -319,4 +319,30 @@ class InvalidInjectorMethodSignatureInspectionTest : BaseMixinTest() { """, ) } + + @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() { + } + } + """, + ) + } } 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() { + } +} From 9b72bb9f74914470d1e564096bd444267bab69eb Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Mon, 28 Sep 2026 16:02:07 +0100 Subject: [PATCH 39/51] ModifyReturnValue tests --- .../MixedInModifyReturnValue.java | 43 +++++++++++++++++++ .../InvalidInjectorMethodSignatureFixTest.kt | 4 ++ ...idInjectorMethodSignatureInspectionTest.kt | 22 ++++++++++ .../modifyReturnValue.after.java | 42 ++++++++++++++++++ .../modifyReturnValue.java | 34 +++++++++++++++ 5 files changed, 145 insertions(+) create mode 100644 mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInModifyReturnValue.java create mode 100644 src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/modifyReturnValue.after.java create mode 100644 src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/modifyReturnValue.java 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/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt index 2e8904afc..399d23b6f 100644 --- a/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt +++ b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt @@ -101,4 +101,8 @@ class InvalidInjectorMethodSignatureFixTest : BaseMixinTest() { @Test @DisplayName("WrapMethod") fun wrapMethod() = doMultiTest("wrapMethod") + + @Test + @DisplayName("ModifyReturnValue") + fun modifyReturnValue() = doMultiTest("modifyReturnValue") } diff --git a/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt index ff4dd8a42..1e94df24d 100644 --- a/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt +++ b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt @@ -345,4 +345,26 @@ class InvalidInjectorMethodSignatureInspectionTest : BaseMixinTest() { """, ) } + + @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() { + } + } + """, + ) + } } 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) { + } +} From fcabc88d84929091373c812b3455dd17204409f9 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Mon, 28 Sep 2026 17:00:28 +0100 Subject: [PATCH 40/51] Fix: Remove vestigial and overly strict int-like params check. --- .../platform/mixin/inspection/injector/MethodSignature.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt index 42d3cd34c..04c2fdd62 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt @@ -186,10 +186,7 @@ data class MethodSignature( } } - return intLikePositions.asSequence().filterIsInstance().mapNotNull { (index) -> - params.getOrNull(index)?.let(paramType) - }.allEqual() - && matchParams(transformedRequiredParams, allowCoerceRequired, 0) + return matchParams(transformedRequiredParams, allowCoerceRequired, 0) && matchParams(trailingParams, true, requiredParams.size) } From 01db6ccd9add36bcf51d1eeb8300735ad0897b06 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Mon, 28 Sep 2026 17:23:17 +0100 Subject: [PATCH 41/51] ModifyExpressionValue tests. --- .../MixedInModifyExpressionValue.java | 57 +++++++++++++ src/test/kotlin/framework/test-util.kt | 20 +++++ .../InvalidInjectorMethodSignatureFixTest.kt | 4 + ...idInjectorMethodSignatureInspectionTest.kt | 32 +++++++ .../modifyExpressionValue.after.java | 84 +++++++++++++++++++ .../modifyExpressionValue.java | 74 ++++++++++++++++ 6 files changed, 271 insertions(+) create mode 100644 mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInModifyExpressionValue.java create mode 100644 src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/modifyExpressionValue.after.java create mode 100644 src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/modifyExpressionValue.java 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/src/test/kotlin/framework/test-util.kt b/src/test/kotlin/framework/test-util.kt index e98783fd2..cf081bb47 100644 --- a/src/test/kotlin/framework/test-util.kt +++ b/src/test/kotlin/framework/test-util.kt @@ -22,9 +22,12 @@ 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.actionSystem.IdeActions import com.intellij.openapi.project.Project import com.intellij.openapi.roots.OrderRootType import com.intellij.openapi.roots.libraries.Library @@ -140,6 +143,7 @@ fun testAllInspectionFixes(fixture: JavaCodeInsightTestFixture, basePath: String ?.joinToString("\n") ?: Assertions.fail("no expected data") fixture.configureByText(JavaFileType.INSTANCE, original) + TemplateManagerImpl.setTemplateTesting(fixture.testRootDisposable) var lastFixedRange: TextRange? = null while (true) { @@ -152,7 +156,23 @@ fun testAllInspectionFixes(fixture: JavaCodeInsightTestFixture, basePath: String 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 } diff --git a/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt index 399d23b6f..527f575e9 100644 --- a/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt +++ b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt @@ -105,4 +105,8 @@ class InvalidInjectorMethodSignatureFixTest : BaseMixinTest() { @Test @DisplayName("ModifyReturnValue") fun modifyReturnValue() = doMultiTest("modifyReturnValue") + + @Test + @DisplayName("ModifyExpressionValue") + fun modifyExpressionValue() = doMultiTest("modifyExpressionValue") } diff --git a/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt index 1e94df24d..835921e13 100644 --- a/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt +++ b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt @@ -367,4 +367,36 @@ class InvalidInjectorMethodSignatureInspectionTest : BaseMixinTest() { """, ) } + + @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() { + } + } + """, + ) + } } 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 + } +} From bf68947bdd7c07d4df44c5bc248fc71badbbee30 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Mon, 28 Sep 2026 17:35:18 +0100 Subject: [PATCH 42/51] More @Inject test cases --- .../MixedInComplex.java | 17 +++++++++++++++++ .../inject/multiTarget.after.java | 10 +++++++++- .../inject/multiTarget.java | 10 +++++++++- 3 files changed, 35 insertions(+), 2 deletions(-) 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 index 1f10da037..4dab24dd6 100644 --- 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 @@ -45,5 +45,22 @@ public void method5(CharSequence arg) { 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/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 index 42b8379af..38275ec1f 100644 --- 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 @@ -37,7 +37,15 @@ private void inject5(CallbackInfo ci) { 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 inject7(@Coerce Object arg, @Coerce CallbackInfo ci) { + 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 index 3d1770529..d79926ca0 100644 --- 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 @@ -35,7 +35,15 @@ private void inject5() { 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 inject7(@Coerce Object arg, @Coerce CallbackInfo ci) { + private String inject8(@Coerce Object arg, @Coerce CallbackInfo ci) { + } + + @Inject(method = {"method7", "method8"}, at = @At("HEAD")) + private void inject9(@Coerce int a, CallbackInfo ci) { } } From 702610dffde359d6776fb186c17ceb00eb3bd9de Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Mon, 28 Sep 2026 17:57:44 +0100 Subject: [PATCH 43/51] ModifyConstant tests --- .../MixedInModifyConstant.java | 37 +++++++++++++ .../InvalidInjectorMethodSignatureFixTest.kt | 4 ++ ...idInjectorMethodSignatureInspectionTest.kt | 51 ++++++++++++++++++ .../modifyConstant.after.java | 53 +++++++++++++++++++ .../modifyConstant.java | 46 ++++++++++++++++ 5 files changed, 191 insertions(+) create mode 100644 mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInModifyConstant.java create mode 100644 src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/modifyConstant.after.java create mode 100644 src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/modifyConstant.java 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/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt index 527f575e9..de86930cb 100644 --- a/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt +++ b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt @@ -109,4 +109,8 @@ class InvalidInjectorMethodSignatureFixTest : BaseMixinTest() { @Test @DisplayName("ModifyExpressionValue") fun modifyExpressionValue() = doMultiTest("modifyExpressionValue") + + @Test + @DisplayName("ModifyConstant") + fun modifyConstant() = doMultiTest("modifyConstant") } diff --git a/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt index 835921e13..68448f640 100644 --- a/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt +++ b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt @@ -399,4 +399,55 @@ class InvalidInjectorMethodSignatureInspectionTest : BaseMixinTest() { """, ) } + + @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() { + } + } + """, + ) + } } 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) { + } +} From f8f1d2bf7a72f8ac682b2707fa23a851b504877b Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Mon, 28 Sep 2026 19:02:34 +0100 Subject: [PATCH 44/51] ModifyReceiver tests --- .../MixedInModifyReceiver.java | 45 ++++++++++++++++ .../InvalidInjectorMethodSignatureFixTest.kt | 4 ++ ...idInjectorMethodSignatureInspectionTest.kt | 24 +++++++++ .../modifyReceiver.after.java | 54 +++++++++++++++++++ .../modifyReceiver.java | 46 ++++++++++++++++ 5 files changed, 173 insertions(+) create mode 100644 mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInModifyReceiver.java create mode 100644 src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/modifyReceiver.after.java create mode 100644 src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/modifyReceiver.java 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/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt index de86930cb..f721bbc54 100644 --- a/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt +++ b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt @@ -113,4 +113,8 @@ class InvalidInjectorMethodSignatureFixTest : BaseMixinTest() { @Test @DisplayName("ModifyConstant") fun modifyConstant() = doMultiTest("modifyConstant") + + @Test + @DisplayName("ModifyReceiver") + fun modifyReceiver() = doMultiTest("modifyReceiver") } diff --git a/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt index 68448f640..2bbec1395 100644 --- a/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt +++ b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt @@ -450,4 +450,28 @@ class InvalidInjectorMethodSignatureInspectionTest : BaseMixinTest() { """, ) } + + @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() { + } + } + """, + ) + } } 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() { + } +} From 8a35dd6043e6b462202ed7161341bda5ad13bd7f Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Mon, 28 Sep 2026 19:13:08 +0100 Subject: [PATCH 45/51] WrapWithCondition tests --- .../MixedInWrapWithCondition.java | 44 ++++++++++++++++++ .../InvalidInjectorMethodSignatureFixTest.kt | 4 ++ ...idInjectorMethodSignatureInspectionTest.kt | 24 ++++++++++ .../wrapWithCondition.after.java | 46 +++++++++++++++++++ .../wrapWithCondition.java | 39 ++++++++++++++++ 5 files changed, 157 insertions(+) create mode 100644 mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInWrapWithCondition.java create mode 100644 src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/wrapWithCondition.after.java create mode 100644 src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/wrapWithCondition.java 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/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt index f721bbc54..7401f5e5b 100644 --- a/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt +++ b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt @@ -117,4 +117,8 @@ class InvalidInjectorMethodSignatureFixTest : BaseMixinTest() { @Test @DisplayName("ModifyReceiver") fun modifyReceiver() = doMultiTest("modifyReceiver") + + @Test + @DisplayName("WrapWithCondition") + fun wrapWithCondition() = doMultiTest("wrapWithCondition") } diff --git a/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt index 2bbec1395..69726df5b 100644 --- a/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt +++ b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt @@ -474,4 +474,28 @@ class InvalidInjectorMethodSignatureInspectionTest : BaseMixinTest() { """, ) } + + @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() { + } + } + """, + ) + } } 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() { + } +} From 97d88b8049ac88f3468ee0e3e9b0adb623477ae6 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Mon, 28 Sep 2026 19:26:59 +0100 Subject: [PATCH 46/51] Redirect tests --- .../MixedInRedirect.java | 60 +++++++++++++++++ .../InvalidInjectorMethodSignatureFixTest.kt | 4 ++ ...idInjectorMethodSignatureInspectionTest.kt | 30 +++++++++ .../redirect.after.java | 64 +++++++++++++++++++ .../redirect.java | 59 +++++++++++++++++ 5 files changed, 217 insertions(+) create mode 100644 mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInRedirect.java create mode 100644 src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/redirect.after.java create mode 100644 src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/redirect.java 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/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt index 7401f5e5b..6e486328e 100644 --- a/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt +++ b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt @@ -121,4 +121,8 @@ class InvalidInjectorMethodSignatureFixTest : BaseMixinTest() { @Test @DisplayName("WrapWithCondition") fun wrapWithCondition() = doMultiTest("wrapWithCondition") + + @Test + @DisplayName("Redirect") + fun redirect() = doMultiTest("redirect") } diff --git a/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt index 69726df5b..5adf3431b 100644 --- a/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt +++ b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt @@ -498,4 +498,34 @@ class InvalidInjectorMethodSignatureInspectionTest : BaseMixinTest() { """, ) } + + @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() { + } + } + """, + ) + } } 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) { + } +} From d220cf37fa632235a6a966071290e2abc828c749 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Mon, 28 Sep 2026 20:02:03 +0100 Subject: [PATCH 47/51] Fix: Only constrain int-like anchor for types that overlap with int-like types. --- .../mixin/inspection/injector/SuggestedSignature.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt index e9be0a100..f488931eb 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt @@ -287,9 +287,13 @@ data class SuggestedSignature( } 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) { + if (pos in signature.intLikePositions || pos !in allIntLikePositions) { continue } val isTrailingParam = pos is MethodSignature.TypePosition.Param && @@ -305,6 +309,7 @@ data class SuggestedSignature( } } } + solver.solve() } From b01151fe73cc4054f94d462a13ce2454d3a0d9e3 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Mon, 28 Sep 2026 20:26:30 +0100 Subject: [PATCH 48/51] WrapOperation tests --- .../MixedInWrapOperation.java | 82 ++++++++++++++++++ .../InvalidInjectorMethodSignatureFixTest.kt | 6 +- ...idInjectorMethodSignatureInspectionTest.kt | 50 ++++++++++- .../wrapOperation.after.java | 86 +++++++++++++++++++ .../wrapOperation.java | 78 +++++++++++++++++ 5 files changed, 300 insertions(+), 2 deletions(-) create mode 100644 mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureFix/MixedInWrapOperation.java create mode 100644 src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/wrapOperation.after.java create mode 100644 src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/wrapOperation.java 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/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt index 6e486328e..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 @@ -125,4 +125,8 @@ class InvalidInjectorMethodSignatureFixTest : BaseMixinTest() { @Test @DisplayName("Redirect") fun redirect() = doMultiTest("redirect") + + @Test + @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 5adf3431b..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 @@ -528,4 +528,52 @@ class InvalidInjectorMethodSignatureInspectionTest : BaseMixinTest() { """, ) } + + @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/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..ad8d50edc --- /dev/null +++ b/src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/wrapOperation.java @@ -0,0 +1,78 @@ +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() { + } + + @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 From 3b0b08d49d7f68655c69ca17feee946f21978a45 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Mon, 28 Sep 2026 21:13:39 +0100 Subject: [PATCH 49/51] Make InjectSignatures ctor private --- .../platform/mixin/inspection/injector/MethodSignatures.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt index 69029032d..2cd138490 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt @@ -160,7 +160,7 @@ data class GeneralSignatures( ) } -class InjectSignatures( +class InjectSignatures private constructor( val params: List, val locals: List, ciParam: Parameter, From 99a894affa0eb4af9ba38c33708e5d012ddb1594 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Mon, 28 Sep 2026 23:05:53 +0100 Subject: [PATCH 50/51] Unused imports --- src/test/kotlin/framework/test-util.kt | 3 +-- .../invalidInjectorMethodSignature/inject/multiTarget.java | 2 -- .../mixin/invalidInjectorMethodSignature/inject/withoutCI.java | 1 - .../invalidInjectorMethodSignature/modifyargs/simple.java | 1 - .../mixin/invalidInjectorMethodSignature/wrapOperation.java | 2 -- 5 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/test/kotlin/framework/test-util.kt b/src/test/kotlin/framework/test-util.kt index cf081bb47..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 @@ -27,7 +27,6 @@ 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.actionSystem.IdeActions import com.intellij.openapi.project.Project import com.intellij.openapi.roots.OrderRootType import com.intellij.openapi.roots.libraries.Library 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 index d79926ca0..8230c63ea 100644 --- 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 @@ -7,8 +7,6 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.LocalCapture; -import java.io.Serializable; - @Mixin(MixedInComplex.class) class TestMixin { @Inject(method = {"method1", "method2"}, at = @At("RETURN")) diff --git a/src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/inject/withoutCI.java b/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/inject/withoutCI.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/modifyargs/simple.java b/src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/modifyargs/simple.java index e6ff5982d..7502f56ba 100644 --- a/src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/modifyargs/simple.java +++ b/src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/modifyargs/simple.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.invoke.arg.Args; @Mixin(MixedInSimple.class) public class TestMixin { 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 index ad8d50edc..a2e16683a 100644 --- a/src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/wrapOperation.java +++ b/src/test/resources/com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/wrapOperation.java @@ -9,8 +9,6 @@ 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")) From f0489353769cb7c62410c8e1399dbc023d462c56 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Wed, 7 Oct 2026 23:44:56 +0100 Subject: [PATCH 51/51] Address review comments. --- .../kotlin/platform/mixin/handlers/InjectAnnotationHandler.kt | 2 +- .../platform/mixin/handlers/mixinextras/WrapMethodHandler.kt | 2 +- .../mixin/handlers/mixinextras/WrapOperationHandler.kt | 2 +- .../platform/mixin/inspection/injector/MethodSignatures.kt | 4 ++-- .../platform/mixin/inspection/injector/SuggestedSignature.kt | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/platform/mixin/handlers/InjectAnnotationHandler.kt b/src/main/kotlin/platform/mixin/handlers/InjectAnnotationHandler.kt index a8bed6027..eda238ce8 100644 --- a/src/main/kotlin/platform/mixin/handlers/InjectAnnotationHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/InjectAnnotationHandler.kt @@ -82,7 +82,7 @@ class InjectAnnotationHandler : InsnInjectorAnnotationHandler() { } return ExpectedSignatures.Valid( - InjectSignatures( + InjectSignatures.create( annotation, targetParams, returnType, diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt index 06f5ffc27..1d150c1fa 100644 --- a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt @@ -47,7 +47,7 @@ class WrapMethodHandler : InjectorAnnotationHandler() { val returnType = targetMethod.getGenericReturnType(targetClass, annotation.project) ExpectedSignatures.Valid( - OperationWrapperSignatures( + OperationWrapperSignatures.create( annotation, collectTargetMethodParameters(annotation.project, targetClass, targetMethod), returnType, diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt index b072cf390..c8f5289b4 100644 --- a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt @@ -58,7 +58,7 @@ class WrapOperationHandler : MixinExtrasInjectorAnnotationHandler() { ): OperationWrapperSignatures? { val params = getParameterTypes(target, targetClass, annotation) ?: return null val returnType = getReturnType(target, annotation) ?: return null - return OperationWrapperSignatures( + return OperationWrapperSignatures.create( annotation, params, returnType, diff --git a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt index 2cd138490..86c9acab8 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignatures.kt @@ -112,7 +112,7 @@ class OperationWrapperSignatures private constructor( override val options = listOf(signature) companion object { - operator fun invoke( + fun create( context: PsiElement, params: List, returnType: PsiType, @@ -185,7 +185,7 @@ class InjectSignatures private constructor( override val options = listOfNotNull(shortSignature, longSignature) companion object { - operator fun invoke( + fun create( context: PsiElement, params: List, returnType: PsiType, diff --git a/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt index f488931eb..94bfc0c91 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/SuggestedSignature.kt @@ -378,7 +378,7 @@ private fun coerciblePrefixLength(types: Sequence>): Int { } /** - * Merges the two types with regards to `@Coerce` behaviour. Returns the merged type, whether the merged type is a + * 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