From 4da3f08b012b56ad7b89729a40d5937d35305726 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Wed, 2 Sep 2026 17:22:16 +0100 Subject: [PATCH 1/8] Refactor: Split MemberInfo and MemberReference. Mixin MemberInfo strings are only tangentially related to concrete MemberReferences. We also introduce a MemberMatcher superinterface for resolving things. --- .../action/CopyMixinTargetReferenceAction.kt | 3 +- .../expression/MEDefinitionFoldingBuilder.kt | 10 +- .../mixin/expression/MEExpressionMatchUtil.kt | 8 +- .../mixin/expression/MESourceMatchContext.kt | 16 +- .../ConstantStringMethodInjectionPoint.kt | 4 +- .../injectionPoint/FieldInjectionPoint.kt | 4 +- .../InvokeAssignInjectionPoint.kt | 4 +- .../injectionPoint/InvokeInjectionPoint.kt | 6 +- .../injectionPoint/NewInsnInjectionPoint.kt | 12 +- .../mixinextras/ExpressionInjectionPoint.kt | 8 +- .../reference/AmbiguousReferenceInspection.kt | 2 +- ...ssaryQualifiedMemberReferenceInspection.kt | 7 +- .../reference/AbstractMethodReference.kt | 12 +- .../mixin/reference/MixinSelectors.kt | 159 +-------------- .../reference/target/DefinitionReferences.kt | 19 +- .../kotlin/platform/mixin/util/AsmUtil.kt | 10 +- .../kotlin/platform/mixin/util/MemberInfo.kt | 181 ++++++++++++++++++ src/main/kotlin/util/MemberMatcher.kt | 151 +++++++++++++++ src/main/kotlin/util/MemberReference.kt | 95 ++------- 19 files changed, 416 insertions(+), 295 deletions(-) create mode 100644 src/main/kotlin/platform/mixin/util/MemberInfo.kt create mode 100644 src/main/kotlin/util/MemberMatcher.kt diff --git a/src/main/kotlin/platform/mixin/action/CopyMixinTargetReferenceAction.kt b/src/main/kotlin/platform/mixin/action/CopyMixinTargetReferenceAction.kt index 46513badc..02f543024 100644 --- a/src/main/kotlin/platform/mixin/action/CopyMixinTargetReferenceAction.kt +++ b/src/main/kotlin/platform/mixin/action/CopyMixinTargetReferenceAction.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.action import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.QualifiedMember -import com.demonwav.mcdev.platform.mixin.reference.toMixinString import com.demonwav.mcdev.util.findReferencedMember import com.demonwav.mcdev.util.getQualifiedMemberReference import com.demonwav.mcdev.util.qualifiedMemberReference diff --git a/src/main/kotlin/platform/mixin/expression/MEDefinitionFoldingBuilder.kt b/src/main/kotlin/platform/mixin/expression/MEDefinitionFoldingBuilder.kt index 760c83ba7..157772412 100644 --- a/src/main/kotlin/platform/mixin/expression/MEDefinitionFoldingBuilder.kt +++ b/src/main/kotlin/platform/mixin/expression/MEDefinitionFoldingBuilder.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 @@ -24,8 +24,8 @@ import com.demonwav.mcdev.platform.mixin.MixinModuleType import com.demonwav.mcdev.platform.mixin.folding.MixinFoldingSettings import com.demonwav.mcdev.platform.mixin.reference.target.FieldDefinitionReference import com.demonwav.mcdev.platform.mixin.reference.target.MethodDefinitionReference +import com.demonwav.mcdev.platform.mixin.util.MemberInfo import com.demonwav.mcdev.platform.mixin.util.MixinConstants -import com.demonwav.mcdev.util.MemberReference import com.intellij.lang.ASTNode import com.intellij.lang.folding.CustomFoldingBuilder import com.intellij.lang.folding.FoldingDescriptor @@ -49,8 +49,8 @@ class MEDefinitionFoldingBuilder : CustomFoldingBuilder() { val psi = node.psi if (psi is PsiLiteralExpression) { val value = psi.value as? String ?: return "..." - val memberReference = MemberReference.parse(value) ?: return "..." - return memberReference.presentableText + val memberInfo = MemberInfo.parse(value) ?: return "..." + return memberInfo.presentableText } return "..." } @@ -108,7 +108,7 @@ class MEDefinitionFoldingBuilder : CustomFoldingBuilder() { if (FieldDefinitionReference.ELEMENT_PATTERN.accepts(expression) || MethodDefinitionReference.ELEMENT_PATTERN.accepts(expression) ) { - if (MemberReference.parse(expression.value as String) != null) { + if (MemberInfo.parse(expression.value as String) != null) { descriptors.add(FoldingDescriptor(expression.node, expression.textRange)) } } diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionMatchUtil.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionMatchUtil.kt index b8f2e0c83..5982f9562 100644 --- a/src/main/kotlin/platform/mixin/expression/MEExpressionMatchUtil.kt +++ b/src/main/kotlin/platform/mixin/expression/MEExpressionMatchUtil.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 @@ -24,8 +24,8 @@ import com.demonwav.mcdev.platform.mixin.handlers.InjectorAnnotationHandler 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 +import com.demonwav.mcdev.platform.mixin.util.MemberInfo import com.demonwav.mcdev.platform.mixin.util.MixinConstants -import com.demonwav.mcdev.util.MemberReference import com.demonwav.mcdev.util.computeStringArray import com.demonwav.mcdev.util.constantStringValue import com.demonwav.mcdev.util.descriptor @@ -172,7 +172,7 @@ object MEExpressionMatchUtil { val fields = annotation.findDeclaredAttributeValue("field")?.computeStringArray() ?: emptyList() for (field in fields) { - val fieldRef = MemberReference.parse(field) ?: continue + val fieldRef = MemberInfo.parse(field) ?: continue pool.addMember( definitionId, SimpleMemberDefinition { @@ -183,7 +183,7 @@ object MEExpressionMatchUtil { val methods = annotation.findDeclaredAttributeValue("method")?.computeStringArray() ?: emptyList() for (method in methods) { - val methodRef = MemberReference.parse(method) ?: continue + val methodRef = MemberInfo.parse(method) ?: continue pool.addMember( definitionId, object : SimpleMemberDefinition { diff --git a/src/main/kotlin/platform/mixin/expression/MESourceMatchContext.kt b/src/main/kotlin/platform/mixin/expression/MESourceMatchContext.kt index 92673ef5d..1601567be 100644 --- a/src/main/kotlin/platform/mixin/expression/MESourceMatchContext.kt +++ b/src/main/kotlin/platform/mixin/expression/MESourceMatchContext.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,7 @@ package com.demonwav.mcdev.platform.mixin.expression import com.demonwav.mcdev.platform.mixin.util.LocalInfo -import com.demonwav.mcdev.util.MemberReference +import com.demonwav.mcdev.platform.mixin.util.MemberInfo import com.intellij.openapi.project.Project import com.intellij.psi.PsiElement @@ -32,8 +32,8 @@ class MESourceMatchContext(val project: Project) { val captures: List get() = capturesInternal private val types = mutableMapOf>() - private val fields = mutableMapOf>() - private val methods = mutableMapOf>() + private val fields = mutableMapOf>() + private val methods = mutableMapOf>() private val localInfos = mutableMapOf>() init { @@ -57,15 +57,15 @@ class MESourceMatchContext(val project: Project) { types.getOrPut(key, ::mutableListOf) += desc } - fun getFields(key: String): List = fields[key] ?: emptyList() + fun getFields(key: String): List = fields[key] ?: emptyList() - fun addField(key: String, field: MemberReference) { + fun addField(key: String, field: MemberInfo) { fields.getOrPut(key, ::mutableListOf) += field } - fun getMethods(key: String): List = methods[key] ?: emptyList() + fun getMethods(key: String): List = methods[key] ?: emptyList() - fun addMethod(key: String, method: MemberReference) { + fun addMethod(key: String, method: MemberInfo) { methods.getOrPut(key, ::mutableListOf) += method } diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantStringMethodInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantStringMethodInjectionPoint.kt index fb770d71e..ec810874f 100644 --- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantStringMethodInjectionPoint.kt +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantStringMethodInjectionPoint.kt @@ -22,10 +22,10 @@ package com.demonwav.mcdev.platform.mixin.handlers.injectionPoint import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler import com.demonwav.mcdev.platform.mixin.reference.MixinSelector +import com.demonwav.mcdev.platform.mixin.util.MemberInfo import com.demonwav.mcdev.platform.mixin.util.MethodTargetMember import com.demonwav.mcdev.platform.mixin.util.fakeResolve import com.demonwav.mcdev.platform.mixin.util.findOrConstructSourceMethod -import com.demonwav.mcdev.util.MemberReference import com.demonwav.mcdev.util.constantStringValue import com.demonwav.mcdev.util.createLiteralExpression import com.demonwav.mcdev.util.toTypedArray @@ -148,7 +148,7 @@ class ConstantStringMethodInjectionPoint : AbstractMethodInjectionPoint() { mode: CollectVisitor.Mode, ): CollectVisitor? { if (mode == CollectVisitor.Mode.COMPLETION) { - return MyCollectVisitor(mode, at.project, MemberReference(""), null) + return MyCollectVisitor(mode, at.project, MemberInfo(""), null) } return target?.let { MyCollectVisitor(mode, at.project, it, AtResolver.getArgs(at)["ldc"]) } } diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/FieldInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/FieldInjectionPoint.kt index 798af8d8f..1e3461272 100644 --- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/FieldInjectionPoint.kt +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/FieldInjectionPoint.kt @@ -21,9 +21,9 @@ package com.demonwav.mcdev.platform.mixin.handlers.injectionPoint import com.demonwav.mcdev.platform.mixin.reference.MixinSelector +import com.demonwav.mcdev.platform.mixin.util.MemberInfo import com.demonwav.mcdev.platform.mixin.util.fakeResolve import com.demonwav.mcdev.platform.mixin.util.findOrConstructSourceField -import com.demonwav.mcdev.util.MemberReference import com.demonwav.mcdev.util.constantValue import com.intellij.codeInsight.completion.JavaLookupElementBuilder import com.intellij.codeInsight.lookup.LookupElementBuilder @@ -97,7 +97,7 @@ class FieldInjectionPoint : QualifiedInjectionPoint() { mode: CollectVisitor.Mode, ): CollectVisitor? { if (mode == CollectVisitor.Mode.COMPLETION) { - return MyCollectVisitor(mode, at.project, MemberReference(""), -1, null, 8) + return MyCollectVisitor(mode, at.project, MemberInfo(""), -1, null, 8) } val opcode = (at.findDeclaredAttributeValue("opcode")?.constantValue as? Int) ?.takeIf { it in Const.VALID_OPCODES } ?: -1 diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeAssignInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeAssignInjectionPoint.kt index 92d1d78a5..14dace415 100644 --- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeAssignInjectionPoint.kt +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeAssignInjectionPoint.kt @@ -21,7 +21,7 @@ package com.demonwav.mcdev.platform.mixin.handlers.injectionPoint import com.demonwav.mcdev.platform.mixin.reference.MixinSelector -import com.demonwav.mcdev.util.MemberReference +import com.demonwav.mcdev.platform.mixin.util.MemberInfo import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project import com.intellij.psi.CommonClassNames @@ -105,7 +105,7 @@ class InvokeAssignInjectionPoint : AbstractMethodInjectionPoint() { val skip = args["skip"]?.let { parseSkip(it) } ?: Const.DEFAULT_SKIP if (mode == CollectVisitor.Mode.COMPLETION) { - return MyCollectVisitor(mode, at.project, MemberReference(""), fuzz, skip) + 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 546eb6227..45c4382af 100644 --- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeInjectionPoint.kt +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeInjectionPoint.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,7 @@ package com.demonwav.mcdev.platform.mixin.handlers.injectionPoint import com.demonwav.mcdev.platform.mixin.reference.MixinSelector -import com.demonwav.mcdev.util.MemberReference +import com.demonwav.mcdev.platform.mixin.util.MemberInfo import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project import com.intellij.psi.CommonClassNames @@ -64,7 +64,7 @@ class InvokeInjectionPoint : AbstractMethodInjectionPoint() { mode: CollectVisitor.Mode, ): CollectVisitor? { if (mode == CollectVisitor.Mode.COMPLETION) { - return MyCollectVisitor(mode, at.project, MemberReference("")) + return MyCollectVisitor(mode, at.project, MemberInfo("")) } return target?.let { MyCollectVisitor(mode, at.project, it) } } diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/NewInsnInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/NewInsnInjectionPoint.kt index 757d0edeb..23400c63c 100644 --- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/NewInsnInjectionPoint.kt +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/NewInsnInjectionPoint.kt @@ -23,11 +23,11 @@ package com.demonwav.mcdev.platform.mixin.handlers.injectionPoint import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler import com.demonwav.mcdev.platform.mixin.reference.MixinSelector import com.demonwav.mcdev.platform.mixin.reference.MixinSelectorParser +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.util.MemberReference import com.demonwav.mcdev.util.constantStringValue import com.demonwav.mcdev.util.fullQualifiedName import com.demonwav.mcdev.util.internalName @@ -84,7 +84,7 @@ class NewInsnInjectionPoint : InjectionPoint() { return target } val clazz = AtResolver.getArgs(at)["class"] ?: return null - return classToMemberReference(clazz) + return classToMemberInfo(clazz) } override fun createNavigationVisitor( @@ -102,7 +102,7 @@ class NewInsnInjectionPoint : InjectionPoint() { mode: CollectVisitor.Mode, ): CollectVisitor? { if (mode == CollectVisitor.Mode.COMPLETION) { - return MyCollectVisitor(mode, at.project, MemberReference("")) + return MyCollectVisitor(mode, at.project, MemberInfo("")) } return getTarget(at, target)?.let { MyCollectVisitor(mode, at.project, it) } } @@ -235,7 +235,6 @@ private class NewInsnTypeSelector( override val fieldDescriptor = null override val methodDescriptor = null - override val displayName = owner } private class NewInsnDescriptorSelector( @@ -256,14 +255,13 @@ private class NewInsnDescriptorSelector( override val owner = null override val fieldDescriptor = null - override val displayName = methodDescriptor } -private fun classToMemberReference(value: String): MemberReference? { +private fun classToMemberInfo(value: String): MemberInfo? { val fqn = value.replace('/', '.') if (fqn.isNotEmpty() && !fqn.startsWith('.') && !fqn.endsWith('.') && !fqn.contains("..")) { if (StringUtil.isJavaIdentifier(fqn.replace('.', '_'))) { - return MemberReference("", owner = fqn) + return MemberInfo("", owner = fqn) } } diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/ExpressionInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/ExpressionInjectionPoint.kt index 30411da04..6701b5797 100644 --- a/src/main/kotlin/platform/mixin/handlers/mixinextras/ExpressionInjectionPoint.kt +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/ExpressionInjectionPoint.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 @@ -34,8 +34,8 @@ import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.InjectionPoint import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.NavigationVisitor import com.demonwav.mcdev.platform.mixin.reference.MixinSelector import com.demonwav.mcdev.platform.mixin.util.LocalInfo +import com.demonwav.mcdev.platform.mixin.util.MemberInfo import com.demonwav.mcdev.platform.mixin.util.MixinConstants -import com.demonwav.mcdev.util.MemberReference import com.demonwav.mcdev.util.cached import com.demonwav.mcdev.util.childrenOfType import com.demonwav.mcdev.util.computeStringArray @@ -154,13 +154,13 @@ class ExpressionInjectionPoint : InjectionPoint() { val fields = annotation.findDeclaredAttributeValue("field")?.computeStringArray() ?: emptyList() for (field in fields) { - val fieldRef = MemberReference.parse(field) ?: continue + val fieldRef = MemberInfo.parse(field) ?: continue matchContext.addField(definitionId, fieldRef) } val methods = annotation.findDeclaredAttributeValue("method")?.computeStringArray() ?: emptyList() for (method in methods) { - val methodRef = MemberReference.parse(method) ?: continue + val methodRef = MemberInfo.parse(method) ?: continue matchContext.addMethod(definitionId, methodRef) } diff --git a/src/main/kotlin/platform/mixin/inspection/reference/AmbiguousReferenceInspection.kt b/src/main/kotlin/platform/mixin/inspection/reference/AmbiguousReferenceInspection.kt index a3fdd8248..abbff266e 100644 --- a/src/main/kotlin/platform/mixin/inspection/reference/AmbiguousReferenceInspection.kt +++ b/src/main/kotlin/platform/mixin/inspection/reference/AmbiguousReferenceInspection.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 diff --git a/src/main/kotlin/platform/mixin/inspection/reference/UnnecessaryQualifiedMemberReferenceInspection.kt b/src/main/kotlin/platform/mixin/inspection/reference/UnnecessaryQualifiedMemberReferenceInspection.kt index 0571a15f7..8e25f4fdd 100644 --- a/src/main/kotlin/platform/mixin/inspection/reference/UnnecessaryQualifiedMemberReferenceInspection.kt +++ b/src/main/kotlin/platform/mixin/inspection/reference/UnnecessaryQualifiedMemberReferenceInspection.kt @@ -24,8 +24,7 @@ import com.demonwav.mcdev.platform.mixin.handlers.InjectorAnnotationHandler import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler import com.demonwav.mcdev.platform.mixin.inspection.MixinAnnotationAttributeInspection import com.demonwav.mcdev.platform.mixin.reference.parseMixinSelector -import com.demonwav.mcdev.platform.mixin.reference.toMixinString -import com.demonwav.mcdev.util.MemberReference +import com.demonwav.mcdev.platform.mixin.util.MemberInfo import com.intellij.codeInspection.LocalQuickFix import com.intellij.codeInspection.ProblemDescriptor import com.intellij.codeInspection.ProblemsHolder @@ -56,10 +55,10 @@ class UnnecessaryQualifiedMemberReferenceInspection : MixinAnnotationAttributeIn private fun checkMemberReference(value: PsiAnnotationMemberValue, holder: ProblemsHolder) { val selector = parseMixinSelector(value) ?: return - if (selector is MemberReference && selector.qualified) { + if (selector is MemberInfo && selector.qualified) { holder.registerProblem( value, - "Unnecessary qualified reference to '${selector.displayName}' in target class", + "Unnecessary qualified reference to '${selector.name}' in target class", QuickFix(selector.withoutOwner.toMixinString()), ) } diff --git a/src/main/kotlin/platform/mixin/reference/AbstractMethodReference.kt b/src/main/kotlin/platform/mixin/reference/AbstractMethodReference.kt index 9c359711e..9196c8258 100644 --- a/src/main/kotlin/platform/mixin/reference/AbstractMethodReference.kt +++ b/src/main/kotlin/platform/mixin/reference/AbstractMethodReference.kt @@ -21,9 +21,9 @@ package com.demonwav.mcdev.platform.mixin.reference import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler -import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.InjectionPoint import com.demonwav.mcdev.platform.mixin.reference.target.TargetReference import com.demonwav.mcdev.platform.mixin.util.ClassAndMethodNode +import com.demonwav.mcdev.platform.mixin.util.MemberInfo import com.demonwav.mcdev.platform.mixin.util.bytecode import com.demonwav.mcdev.platform.mixin.util.findMethods import com.demonwav.mcdev.platform.mixin.util.findOrConstructSourceMethod @@ -87,8 +87,8 @@ abstract class AbstractMethodReference : PolyReferenceResolver(), MixinReference }.any() } - fun getReferenceIfAmbiguous(context: PsiElement): MemberReference? { - val targetReference = parseSelector(context) as? MemberReference ?: return null + fun getReferenceIfAmbiguous(context: PsiElement): MemberInfo? { + val targetReference = parseSelector(context) as? MemberInfo ?: return null if (targetReference.descriptor != null) { return null } @@ -97,8 +97,8 @@ abstract class AbstractMethodReference : PolyReferenceResolver(), MixinReference return if (isAmbiguous(targets, targetReference)) targetReference else null } - private fun isAmbiguous(targets: Collection, targetReference: MemberReference): Boolean { - if (targetReference.matchAllNames) { + private fun isAmbiguous(targets: Collection, targetReference: MemberInfo): Boolean { + if (targetReference.name == null) { return targets.any { val methods = it.methods methods != null && methods.size > 1 @@ -141,7 +141,7 @@ abstract class AbstractMethodReference : PolyReferenceResolver(), MixinReference return targetedMethods.asSequence().flatMap { method -> val targetReference = parseSelector(method, context) ?: return@flatMap emptySequence() - if (targetReference is MemberReference && targetReference.descriptor == null && isAmbiguous( + if (targetReference is MemberInfo && targetReference.descriptor == null && isAmbiguous( targets, targetReference, ) diff --git a/src/main/kotlin/platform/mixin/reference/MixinSelectors.kt b/src/main/kotlin/platform/mixin/reference/MixinSelectors.kt index 0904625c8..cc381d573 100644 --- a/src/main/kotlin/platform/mixin/reference/MixinSelectors.kt +++ b/src/main/kotlin/platform/mixin/reference/MixinSelectors.kt @@ -20,28 +20,19 @@ package com.demonwav.mcdev.platform.mixin.reference -import com.demonwav.mcdev.platform.mixin.util.FieldTargetMember -import com.demonwav.mcdev.platform.mixin.util.MethodTargetMember +import com.demonwav.mcdev.platform.mixin.util.MemberInfo import com.demonwav.mcdev.platform.mixin.util.MixinConstants import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Annotations.DESC import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Annotations.SLICE -import com.demonwav.mcdev.platform.mixin.util.MixinTargetMember -import com.demonwav.mcdev.platform.mixin.util.bytecode -import com.demonwav.mcdev.platform.mixin.util.findField -import com.demonwav.mcdev.platform.mixin.util.findMethod import com.demonwav.mcdev.platform.mixin.util.mixinTargets -import com.demonwav.mcdev.util.MemberReference +import com.demonwav.mcdev.util.MemberMatcher import com.demonwav.mcdev.util.cached import com.demonwav.mcdev.util.constantStringValue import com.demonwav.mcdev.util.descriptor import com.demonwav.mcdev.util.findAnnotation import com.demonwav.mcdev.util.findContainingClass import com.demonwav.mcdev.util.findContainingModifierList -import com.demonwav.mcdev.util.findField -import com.demonwav.mcdev.util.findMethods -import com.demonwav.mcdev.util.findQualifiedClass import com.demonwav.mcdev.util.fullQualifiedName -import com.demonwav.mcdev.util.internalName import com.demonwav.mcdev.util.mapToArray import com.demonwav.mcdev.util.resolveClass import com.demonwav.mcdev.util.resolveType @@ -49,16 +40,12 @@ import com.demonwav.mcdev.util.resolveTypeArray import com.intellij.openapi.editor.Editor import com.intellij.openapi.extensions.ExtensionPointName import com.intellij.openapi.project.Project -import com.intellij.openapi.util.RecursionManager -import com.intellij.psi.CommonClassNames import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiCallExpression import com.intellij.psi.PsiClass import com.intellij.psi.PsiElement -import com.intellij.psi.PsiField import com.intellij.psi.PsiLiteral -import com.intellij.psi.PsiMember import com.intellij.psi.PsiMethod import com.intellij.psi.PsiModifierList import com.intellij.psi.PsiNameValuePair @@ -75,9 +62,6 @@ import com.intellij.psi.util.parentOfType import java.util.Locale import java.util.regex.PatternSyntaxException import org.objectweb.asm.Type -import org.objectweb.asm.tree.ClassNode -import org.objectweb.asm.tree.FieldNode -import org.objectweb.asm.tree.MethodNode fun parseMixinSelector(element: PsiElement): MixinSelector? { val stringValue = element.constantStringValue ?: return null @@ -104,142 +88,12 @@ interface MixinSelectorParser { } /** - * An interface which matches members, that's it really. + * An interface which represents Mixin target selectors. */ -interface MixinSelector { - fun matchField(owner: String, name: String, desc: String): Boolean - fun matchMethod(owner: String, name: String, desc: String): Boolean - - fun matchField(field: PsiField, qualifier: PsiClass): Boolean { - if (!canEverMatch(field.name)) { - return false - } - val fqn = qualifier.fullQualifiedName ?: return false - val desc = field.descriptor ?: return false - return matchField(fqn.replace('.', '/'), field.name, desc) - } - - fun matchField(field: FieldNode, qualifier: ClassNode): Boolean { - return matchField(qualifier.name, field.name, field.desc) - } - - fun matchMethod(method: PsiMethod, qualifier: PsiClass): Boolean { - if (!canEverMatch(method.internalName)) { - return false - } - val fqn = qualifier.fullQualifiedName ?: return false - val desc = method.descriptor ?: return false - return matchMethod(fqn.replace('.', '/'), method.internalName, desc) - } - - fun matchMethod(method: MethodNode, qualifier: ClassNode): Boolean { - return matchMethod(qualifier.name, method.name, method.desc) - } - - fun getCustomOwner(owner: ClassNode): ClassNode { - return owner - } - - /** - * Implement this to return false for early-out optimizations, so you don't need to resolve the member in the - * navigation visitor - */ - fun canEverMatch(name: String): Boolean { - return true - } - - val owner: String? - val methodDescriptor: String? - val fieldDescriptor: String? - val qualified - get() = owner != null - - val displayName: String - - fun resolve( - project: Project, - scope: GlobalSearchScope = GlobalSearchScope.allScope(project), - ): Pair? { - return resolve(project, scope, ::Pair) - } - - fun resolveMember(project: Project, scope: GlobalSearchScope = GlobalSearchScope.allScope(project)): PsiMember? { - return resolve(project, scope) { _, member -> member } - } - - fun resolveAsm( - project: Project, - scope: GlobalSearchScope = GlobalSearchScope.allScope(project), - ): MixinTargetMember? { - val owner = this.owner ?: return null - - fun doFind(owner: String): MixinTargetMember? { - if (owner == CommonClassNames.JAVA_LANG_OBJECT) { - return null - } - return RecursionManager.doPreventingRecursion(owner, false) { - val classNode = findQualifiedClass(project, owner, scope)?.bytecode ?: return@doPreventingRecursion null - - classNode.findMethod(this)?.let { - return@doPreventingRecursion MethodTargetMember(classNode, it) - } - - classNode.findField(this)?.let { - return@doPreventingRecursion FieldTargetMember(classNode, it) - } - - classNode.superName?.let { doFind(it.replace('/', '.')) }?.let { return@doPreventingRecursion it } - - classNode.interfaces?.let { interfaces -> - for (itf in interfaces) { - doFind(itf.replace('/', '.'))?.let { return@doPreventingRecursion it } - } - } - - null - } - } - - return doFind(owner) - } - - private inline fun resolve(project: Project, scope: GlobalSearchScope, ret: (PsiClass, PsiMember) -> R): R? { - val owner = this.owner ?: return null - - val psiClass = findQualifiedClass(project, owner, scope) ?: return null - - val field = psiClass.findField(this, checkBases = true) - return if (field != null) { - ret(psiClass, field) - } else { - psiClass.findMethods(this, checkBases = true).firstOrNull()?.let { ret(psiClass, it) } - } - } -} - -// Member reference - -fun MemberReference.toMixinString(): String { - return buildString { - if (owner != null) { - append('L').append(owner.replace('.', '/')).append(';') - } - - append(if (matchAllNames) "*" else name) - - descriptor?.let { descriptor -> - if (!descriptor.startsWith('(')) { - // Field descriptor - append(':') - } - - append(descriptor) - } - } -} +interface MixinSelector : MemberMatcher class MixinMemberParser : MixinSelectorParser { - override fun parse(value: String, context: PsiElement) = MemberReference.parse(value) + override fun parse(value: String, context: PsiElement) = MemberInfo.parse(value) } // Regex reference @@ -334,8 +188,6 @@ private class MixinRegexSelector( override val methodDescriptor = descriptor?.takeIf { it.contains("(") } override val fieldDescriptor = descriptor?.takeUnless { it.contains("(") } - override val displayName: String - get() = namePattern.pattern } // Dynamic selectors @@ -627,5 +479,4 @@ data class DescSelector( override val owner = owners.singleOrNull() override val fieldDescriptor = methodDescriptor.substringBefore('(') - override val displayName = name } diff --git a/src/main/kotlin/platform/mixin/reference/target/DefinitionReferences.kt b/src/main/kotlin/platform/mixin/reference/target/DefinitionReferences.kt index d47ba972c..684b3a770 100644 --- a/src/main/kotlin/platform/mixin/reference/target/DefinitionReferences.kt +++ b/src/main/kotlin/platform/mixin/reference/target/DefinitionReferences.kt @@ -23,6 +23,7 @@ package com.demonwav.mcdev.platform.mixin.reference.target import com.demonwav.mcdev.platform.mixin.expression.MEExpressionMatchUtil import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler import com.demonwav.mcdev.platform.mixin.reference.MixinReference +import com.demonwav.mcdev.platform.mixin.util.MemberInfo import com.demonwav.mcdev.platform.mixin.util.MethodTargetMember import com.demonwav.mcdev.platform.mixin.util.MixinConstants import com.demonwav.mcdev.util.MemberReference @@ -55,7 +56,7 @@ import org.objectweb.asm.tree.FieldInsnNode import org.objectweb.asm.tree.MethodInsnNode abstract class AbstractDefinitionReference : PolyReferenceResolver(), MixinReference { - abstract fun getFullReferenceIfMatches(memberReference: MemberReference, node: FlowValue): MemberReference? + abstract fun getFullReferenceIfMatches(memberInfo: MemberInfo, node: FlowValue): MemberReference? abstract fun getMatchesInClass(memberReference: MemberReference, clazz: PsiClass): Sequence abstract fun referenceToString(memberReference: MemberReference): String @@ -82,7 +83,7 @@ abstract class AbstractDefinitionReference : PolyReferenceResolver(), MixinRefer override fun collectVariants(context: PsiElement) = resolveInBytecode( context, - MemberReference("*", null, null, matchAllNames = true, matchAllDescs = true) + MemberInfo("*", null, null, matchAllNames = true, matchAllDescs = true) ).mapToArray { LookupElementBuilder.create(referenceToString(it)) .withPresentableText(it.presentableText) @@ -90,11 +91,11 @@ abstract class AbstractDefinitionReference : PolyReferenceResolver(), MixinRefer } fun resolveInBytecode(context: PsiElement): List { - val memberReference = context.constantStringValue?.let(MemberReference::parse) ?: return emptyList() + val memberReference = context.constantStringValue?.let(MemberInfo::parse) ?: return emptyList() return resolveInBytecode(context, memberReference) } - private fun resolveInBytecode(context: PsiElement, memberReference: MemberReference): List { + private fun resolveInBytecode(context: PsiElement, memberInfo: MemberInfo): List { val project = context.project val modifierList = context.findContainingModifierList() ?: return emptyList() val annotation = modifierList.annotations.firstOrNull { @@ -119,7 +120,7 @@ abstract class AbstractDefinitionReference : PolyReferenceResolver(), MixinRefer ) ?: continue for (node in flow.values) { - val fullReference = getFullReferenceIfMatches(memberReference, node) ?: continue + val fullReference = getFullReferenceIfMatches(memberInfo, node) ?: continue result += fullReference } } @@ -132,9 +133,9 @@ object FieldDefinitionReference : AbstractDefinitionReference() { val ELEMENT_PATTERN: PsiJavaElementPattern.Capture = PsiJavaPatterns.psiLiteral(StandardPatterns.string()) .insideAnnotationAttribute(MixinConstants.MixinExtras.DEFINITION, "field") - override fun getFullReferenceIfMatches(memberReference: MemberReference, node: FlowValue): MemberReference? { + override fun getFullReferenceIfMatches(memberInfo: MemberInfo, node: FlowValue): MemberReference? { val insn = node.insn - if (insn !is FieldInsnNode || !memberReference.matchField(insn.owner, insn.name, insn.desc)) { + if (insn !is FieldInsnNode || !memberInfo.matchField(insn.owner, insn.name, insn.desc)) { return null } @@ -154,7 +155,7 @@ object MethodDefinitionReference : AbstractDefinitionReference() { val ELEMENT_PATTERN: PsiJavaElementPattern.Capture = PsiJavaPatterns.psiLiteral(StandardPatterns.string()) .insideAnnotationAttribute(MixinConstants.MixinExtras.DEFINITION, "method") - override fun getFullReferenceIfMatches(memberReference: MemberReference, node: FlowValue): MemberReference? { + override fun getFullReferenceIfMatches(memberInfo: MemberInfo, node: FlowValue): MemberReference? { val info = node.getDecoration(FlowDecorations.LMF_INFO) val insn = node.insn val (owner, name, desc) = when { @@ -164,7 +165,7 @@ object MethodDefinitionReference : AbstractDefinitionReference() { insn is MethodInsnNode -> Triple(insn.owner, insn.name, insn.desc) else -> return null } - if (!memberReference.matchMethod(owner, name, desc)) { + if (!memberInfo.matchMethod(owner, name, desc)) { return null } diff --git a/src/main/kotlin/platform/mixin/util/AsmUtil.kt b/src/main/kotlin/platform/mixin/util/AsmUtil.kt index a1da87ed3..60d927f3b 100644 --- a/src/main/kotlin/platform/mixin/util/AsmUtil.kt +++ b/src/main/kotlin/platform/mixin/util/AsmUtil.kt @@ -20,7 +20,7 @@ package com.demonwav.mcdev.platform.mixin.util -import com.demonwav.mcdev.platform.mixin.reference.MixinSelector +import com.demonwav.mcdev.util.MemberMatcher import com.demonwav.mcdev.util.MemberReference import com.demonwav.mcdev.util.anonymousClasses import com.demonwav.mcdev.util.cached @@ -462,19 +462,19 @@ fun ClassNode.findFieldByName(name: String): FieldNode? { return fields?.firstOrNull { it.name == name } } -fun ClassNode.findFields(ref: MixinSelector): Sequence { +fun ClassNode.findFields(ref: MemberMatcher): Sequence { return fields?.asSequence()?.filter { ref.matchField(it, this) } ?: emptySequence() } -fun ClassNode.findField(ref: MixinSelector): FieldNode? { +fun ClassNode.findField(ref: MemberMatcher): FieldNode? { return findFields(ref).firstOrNull() } -fun ClassNode.findMethods(ref: MixinSelector): Sequence { +fun ClassNode.findMethods(ref: MemberMatcher): Sequence { return methods?.asSequence()?.filter { ref.matchMethod(it, this) } ?: emptySequence() } -fun ClassNode.findMethod(ref: MixinSelector): MethodNode? { +fun ClassNode.findMethod(ref: MemberMatcher): MethodNode? { return findMethods(ref).firstOrNull() } diff --git a/src/main/kotlin/platform/mixin/util/MemberInfo.kt b/src/main/kotlin/platform/mixin/util/MemberInfo.kt new file mode 100644 index 000000000..cfd5eef4d --- /dev/null +++ b/src/main/kotlin/platform/mixin/util/MemberInfo.kt @@ -0,0 +1,181 @@ +/* + * 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.reference.MixinSelector +import com.intellij.openapi.util.text.StringUtil +import org.objectweb.asm.Type + +/** + * Represents a Mixin MemberInfo. + */ +data class MemberInfo( + val name: String, + val descriptor: String? = null, + override val owner: String? = null, + val matchAllNames: Boolean = false, + val matchAllDescs: Boolean = false, +) : MixinSelector { + + init { + assert(owner?.contains('/') != true) + } + + val withoutDescriptor + get() = if (this.descriptor == null) { + this + } else { + copy(descriptor = null) + } + + val withoutOwner + get() = if (this.owner == null) { + this + } else { + copy(owner = null) + } + + override val methodDescriptor = descriptor?.takeIf { it.contains("(") } + override val fieldDescriptor = descriptor?.takeUnless { it.contains("(") } + + val presentableText: String get() = buildString { + if (owner != null) { + append(owner.substringAfterLast('.')) + append('.') + } + append(name) + if (descriptor != null && descriptor.startsWith("(")) { + append('(') + append(Type.getArgumentTypes(descriptor).joinToString { it.className.substringAfterLast('.') }) + append(')') + } + } + + override fun canEverMatch(name: String): Boolean { + return matchAllNames || this.name == name + } + + private fun matchOwner(clazz: String): Boolean { + assert(!clazz.contains('.')) + return this.owner == null || this.owner == clazz.replace('/', '.') + } + + override fun matchField(owner: String, name: String, desc: String): Boolean { + assert(!owner.contains('.')) + return (this.matchAllNames || this.name == name) && + matchOwner(owner) && + (this.descriptor == null || this.descriptor == desc) + } + + override fun matchMethod(owner: String, name: String, desc: String): Boolean { + assert(!owner.contains('.')) + return (this.matchAllNames || this.name == name) && + matchOwner(owner) && + (this.descriptor == null || this.descriptor == desc) + } + + fun toMixinString(): String { + return buildString { + if (owner != null) { + append('L').append(owner.replace('.', '/')).append(';') + } + + append(if (matchAllNames) "*" else name) + + descriptor?.let { descriptor -> + if (!descriptor.startsWith('(')) { + // Field descriptor + append(':') + } + + append(descriptor) + } + } + } + + companion object { + fun parse(value: String): MemberInfo? { + val reference = value.replace(" ", "") + val owner: String? + + var pos = reference.lastIndexOf('.') + if (pos != -1) { + // Everything before the dot is the qualifier/owner + owner = reference.substring(0, pos).replace('/', '.') + } else { + pos = reference.indexOf(';') + if (pos != -1 && reference.startsWith('L')) { + val internalOwner = reference.substring(1, pos) + if (!StringUtil.isJavaIdentifier(internalOwner.replace('/', '_'))) { + // Invalid: Qualifier should only contain slashes + return null + } + + owner = internalOwner.replace('/', '.') + + // if owner is all there is to the selector, match anything with the owner + if (pos == reference.length - 1) { + return MemberInfo("", null, owner, matchAllNames = true, matchAllDescs = true) + } + } else { + // No owner/qualifier specified + pos = -1 + owner = null + } + } + + val descriptor: String? + val name: String + val matchAllNames = reference.getOrNull(pos + 1) == '*' + val matchAllDescs: Boolean + + // Find descriptor separator + val methodDescPos = reference.indexOf('(', pos + 1) + if (methodDescPos != -1) { + // Method descriptor + descriptor = reference.substring(methodDescPos) + name = reference.substring(pos + 1, methodDescPos) + matchAllDescs = false + } else { + val fieldDescPos = reference.indexOf(':', pos + 1) + if (fieldDescPos != -1) { + descriptor = reference.substring(fieldDescPos + 1) + name = reference.substring(pos + 1, fieldDescPos) + matchAllDescs = false + } else { + descriptor = null + matchAllDescs = reference.endsWith('*') + name = if (matchAllDescs) { + reference.substring(pos + 1, reference.lastIndex) + } else { + reference.substring(pos + 1) + } + } + } + + if (!matchAllNames && !StringUtil.isJavaIdentifier(name) && name != "" && name != "") { + return null + } + + return MemberInfo(if (matchAllNames) "*" else name, descriptor, owner, matchAllNames, matchAllDescs) + } + } +} diff --git a/src/main/kotlin/util/MemberMatcher.kt b/src/main/kotlin/util/MemberMatcher.kt new file mode 100644 index 000000000..39cb336c3 --- /dev/null +++ b/src/main/kotlin/util/MemberMatcher.kt @@ -0,0 +1,151 @@ +/* + * 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 com.demonwav.mcdev.platform.mixin.util.FieldTargetMember +import com.demonwav.mcdev.platform.mixin.util.MethodTargetMember +import com.demonwav.mcdev.platform.mixin.util.MixinTargetMember +import com.demonwav.mcdev.platform.mixin.util.bytecode +import com.demonwav.mcdev.platform.mixin.util.findField +import com.demonwav.mcdev.platform.mixin.util.findMethod +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.RecursionManager +import com.intellij.psi.CommonClassNames +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiField +import com.intellij.psi.PsiMember +import com.intellij.psi.PsiMethod +import com.intellij.psi.search.GlobalSearchScope +import org.objectweb.asm.tree.ClassNode +import org.objectweb.asm.tree.FieldNode +import org.objectweb.asm.tree.MethodNode + +/** + * An interface which matches members, that's it really. + */ +interface MemberMatcher { + fun matchField(owner: String, name: String, desc: String): Boolean + fun matchMethod(owner: String, name: String, desc: String): Boolean + + fun matchField(field: PsiField, qualifier: PsiClass): Boolean { + if (!canEverMatch(field.name)) { + return false + } + val fqn = qualifier.fullQualifiedName ?: return false + val desc = field.descriptor ?: return false + return matchField(fqn.replace('.', '/'), field.name, desc) + } + + fun matchField(field: FieldNode, qualifier: ClassNode): Boolean { + return matchField(qualifier.name, field.name, field.desc) + } + + fun matchMethod(method: PsiMethod, qualifier: PsiClass): Boolean { + if (!canEverMatch(method.internalName)) { + return false + } + val fqn = qualifier.fullQualifiedName ?: return false + val desc = method.descriptor ?: return false + return matchMethod(fqn.replace('.', '/'), method.internalName, desc) + } + + fun matchMethod(method: MethodNode, qualifier: ClassNode): Boolean { + return matchMethod(qualifier.name, method.name, method.desc) + } + + fun getCustomOwner(owner: ClassNode): ClassNode { + return owner + } + + /** + * Implement this to return false for early-out optimizations, so you don't need to resolve the member in the + * navigation visitor + */ + fun canEverMatch(name: String): Boolean { + return true + } + + val owner: String? + val methodDescriptor: String? + val fieldDescriptor: String? + val qualified + get() = owner != null + + fun resolve( + project: Project, + scope: GlobalSearchScope = GlobalSearchScope.allScope(project), + ): Pair? { + return resolve(project, scope, ::Pair) + } + + fun resolveMember(project: Project, scope: GlobalSearchScope = GlobalSearchScope.allScope(project)): PsiMember? { + return resolve(project, scope) { _, member -> member } + } + + fun resolveAsm( + project: Project, + scope: GlobalSearchScope = GlobalSearchScope.allScope(project), + ): MixinTargetMember? { + val owner = this.owner ?: return null + + fun doFind(owner: String): MixinTargetMember? { + if (owner == CommonClassNames.JAVA_LANG_OBJECT) { + return null + } + return RecursionManager.doPreventingRecursion(owner, false) { + val classNode = findQualifiedClass(project, owner, scope)?.bytecode ?: return@doPreventingRecursion null + + classNode.findMethod(this)?.let { + return@doPreventingRecursion MethodTargetMember(classNode, it) + } + + classNode.findField(this)?.let { + return@doPreventingRecursion FieldTargetMember(classNode, it) + } + + classNode.superName?.let { doFind(it.replace('/', '.')) }?.let { return@doPreventingRecursion it } + + classNode.interfaces?.let { interfaces -> + for (itf in interfaces) { + doFind(itf.replace('/', '.'))?.let { return@doPreventingRecursion it } + } + } + + null + } + } + + return doFind(owner) + } + + private inline fun resolve(project: Project, scope: GlobalSearchScope, ret: (PsiClass, PsiMember) -> R): R? { + val owner = this.owner ?: return null + + val psiClass = findQualifiedClass(project, owner, scope) ?: return null + + val field = psiClass.findField(this, checkBases = true) + return if (field != null) { + ret(psiClass, field) + } else { + psiClass.findMethods(this, checkBases = true).firstOrNull()?.let { ret(psiClass, it) } + } + } +} diff --git a/src/main/kotlin/util/MemberReference.kt b/src/main/kotlin/util/MemberReference.kt index 85564c98c..44ac190d8 100644 --- a/src/main/kotlin/util/MemberReference.kt +++ b/src/main/kotlin/util/MemberReference.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,26 +20,19 @@ package com.demonwav.mcdev.util -import com.demonwav.mcdev.platform.mixin.reference.MixinSelector -import com.intellij.openapi.util.text.StringUtil import com.intellij.psi.PsiClass import com.intellij.psi.PsiField import com.intellij.psi.PsiMethod -import java.io.Serializable import org.objectweb.asm.Type /** - * Represents a reference to a class member (a method or a field). It may - * resolve to multiple members if [matchAllNames] or [matchAllDescs] is set or if the member is - * not full qualified. + * Represents a reference to a class member (a method or a field). */ data class MemberReference( val name: String, val descriptor: String? = null, override val owner: String? = null, - val matchAllNames: Boolean = false, - val matchAllDescs: Boolean = false, -) : Serializable, MixinSelector { +) : MemberMatcher { init { assert(owner?.contains('/') != true) @@ -61,7 +54,6 @@ data class MemberReference( override val methodDescriptor = descriptor?.takeIf { it.contains("(") } override val fieldDescriptor = descriptor?.takeUnless { it.contains("(") } - override val displayName = name val presentableText: String get() = buildString { if (owner != null) { @@ -77,7 +69,7 @@ data class MemberReference( } override fun canEverMatch(name: String): Boolean { - return matchAllNames || this.name == name + return this.name == name } private fun matchOwner(clazz: String): Boolean { @@ -87,90 +79,39 @@ data class MemberReference( override fun matchField(owner: String, name: String, desc: String): Boolean { assert(!owner.contains('.')) - return (this.matchAllNames || this.name == name) && + return canEverMatch(name) && matchOwner(owner) && (this.descriptor == null || this.descriptor == desc) } override fun matchMethod(owner: String, name: String, desc: String): Boolean { assert(!owner.contains('.')) - return (this.matchAllNames || this.name == name) && + return canEverMatch(name) && matchOwner(owner) && (this.descriptor == null || this.descriptor == desc) } - companion object { - fun parse(value: String): MemberReference? { - val reference = value.replace(" ", "") - val owner: String? - - var pos = reference.lastIndexOf('.') - if (pos != -1) { - // Everything before the dot is the qualifier/owner - owner = reference.substring(0, pos).replace('/', '.') - } else { - pos = reference.indexOf(';') - if (pos != -1 && reference.startsWith('L')) { - val internalOwner = reference.substring(1, pos) - if (!StringUtil.isJavaIdentifier(internalOwner.replace('/', '_'))) { - // Invalid: Qualifier should only contain slashes - return null - } - - owner = internalOwner.replace('/', '.') - - // if owner is all there is to the selector, match anything with the owner - if (pos == reference.length - 1) { - return MemberReference("", null, owner, matchAllNames = true, matchAllDescs = true) - } - } else { - // No owner/qualifier specified - pos = -1 - owner = null - } - } + fun toMixinString() = buildString { + if (owner != null) { + append('L').append(owner.replace('.', '/')).append(';') + } - val descriptor: String? - val name: String - val matchAllNames = reference.getOrNull(pos + 1) == '*' - val matchAllDescs: Boolean - - // Find descriptor separator - val methodDescPos = reference.indexOf('(', pos + 1) - if (methodDescPos != -1) { - // Method descriptor - descriptor = reference.substring(methodDescPos) - name = reference.substring(pos + 1, methodDescPos) - matchAllDescs = false - } else { - val fieldDescPos = reference.indexOf(':', pos + 1) - if (fieldDescPos != -1) { - descriptor = reference.substring(fieldDescPos + 1) - name = reference.substring(pos + 1, fieldDescPos) - matchAllDescs = false - } else { - descriptor = null - matchAllDescs = reference.endsWith('*') - name = if (matchAllDescs) { - reference.substring(pos + 1, reference.lastIndex) - } else { - reference.substring(pos + 1) - } - } - } + append(name) - if (!matchAllNames && !StringUtil.isJavaIdentifier(name) && name != "" && name != "") { - return null + descriptor?.let { descriptor -> + if (!descriptor.startsWith('(')) { + // Field descriptor + append(':') } - return MemberReference(if (matchAllNames) "*" else name, descriptor, owner, matchAllNames, matchAllDescs) + append(descriptor) } } } // Class -fun PsiClass.findMethods(member: MixinSelector, checkBases: Boolean = false): Sequence { +fun PsiClass.findMethods(member: MemberMatcher, checkBases: Boolean = false): Sequence { val methods = if (checkBases) { allMethods.asSequence() } else { @@ -179,7 +120,7 @@ fun PsiClass.findMethods(member: MixinSelector, checkBases: Boolean = false): Se return methods.filter { member.matchMethod(it, this) } } -fun PsiClass.findField(selector: MixinSelector, checkBases: Boolean = false): PsiField? { +fun PsiClass.findField(selector: MemberMatcher, checkBases: Boolean = false): PsiField? { val fields = if (checkBases) { allFields.toList() } else { From 88a5825546be9c2f8ed5551887c5a846be2ca922 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Wed, 2 Sep 2026 17:28:56 +0100 Subject: [PATCH 2/8] Refactor: Parse MemberInfos properly and represent Quantifiers. --- .../expression/MEDefinitionFoldingBuilder.kt | 3 +- .../ConstantStringMethodInjectionPoint.kt | 2 +- .../injectionPoint/FieldInjectionPoint.kt | 2 +- .../InvokeAssignInjectionPoint.kt | 2 +- .../injectionPoint/InvokeInjectionPoint.kt | 2 +- .../injectionPoint/NewInsnInjectionPoint.kt | 2 +- .../reference/AmbiguousReferenceInspection.kt | 3 +- .../reference/target/DefinitionReferences.kt | 2 +- .../kotlin/platform/mixin/util/MemberInfo.kt | 138 +++++++----------- src/main/kotlin/util/Quantifier.kt | 112 ++++++++++++++ 10 files changed, 177 insertions(+), 91 deletions(-) create mode 100644 src/main/kotlin/util/Quantifier.kt diff --git a/src/main/kotlin/platform/mixin/expression/MEDefinitionFoldingBuilder.kt b/src/main/kotlin/platform/mixin/expression/MEDefinitionFoldingBuilder.kt index 157772412..84219f904 100644 --- a/src/main/kotlin/platform/mixin/expression/MEDefinitionFoldingBuilder.kt +++ b/src/main/kotlin/platform/mixin/expression/MEDefinitionFoldingBuilder.kt @@ -26,6 +26,7 @@ import com.demonwav.mcdev.platform.mixin.reference.target.FieldDefinitionReferen import com.demonwav.mcdev.platform.mixin.reference.target.MethodDefinitionReference import com.demonwav.mcdev.platform.mixin.util.MemberInfo import com.demonwav.mcdev.platform.mixin.util.MixinConstants +import com.demonwav.mcdev.util.MemberReference import com.intellij.lang.ASTNode import com.intellij.lang.folding.CustomFoldingBuilder import com.intellij.lang.folding.FoldingDescriptor @@ -50,7 +51,7 @@ class MEDefinitionFoldingBuilder : CustomFoldingBuilder() { if (psi is PsiLiteralExpression) { val value = psi.value as? String ?: return "..." val memberInfo = MemberInfo.parse(value) ?: return "..." - return memberInfo.presentableText + return MemberReference(memberInfo.name ?: "*", memberInfo.descriptor, memberInfo.owner).presentableText } return "..." } diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantStringMethodInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantStringMethodInjectionPoint.kt index ec810874f..bca4d9997 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() { mode: CollectVisitor.Mode, ): CollectVisitor? { if (mode == CollectVisitor.Mode.COMPLETION) { - return MyCollectVisitor(mode, at.project, MemberInfo(""), null) + return MyCollectVisitor(mode, at.project, MemberInfo(), null) } return target?.let { MyCollectVisitor(mode, at.project, it, AtResolver.getArgs(at)["ldc"]) } } diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/FieldInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/FieldInjectionPoint.kt index 1e3461272..e29242961 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() { mode: CollectVisitor.Mode, ): CollectVisitor? { if (mode == CollectVisitor.Mode.COMPLETION) { - return MyCollectVisitor(mode, at.project, MemberInfo(""), -1, null, 8) + return MyCollectVisitor(mode, at.project, MemberInfo(), -1, null, 8) } val opcode = (at.findDeclaredAttributeValue("opcode")?.constantValue as? Int) ?.takeIf { it in Const.VALID_OPCODES } ?: -1 diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeAssignInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeAssignInjectionPoint.kt index 14dace415..d406528b2 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 skip = args["skip"]?.let { parseSkip(it) } ?: Const.DEFAULT_SKIP if (mode == CollectVisitor.Mode.COMPLETION) { - return MyCollectVisitor(mode, at.project, MemberInfo(""), fuzz, skip) + 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 45c4382af..be0c904ea 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() { mode: CollectVisitor.Mode, ): CollectVisitor? { if (mode == CollectVisitor.Mode.COMPLETION) { - return MyCollectVisitor(mode, at.project, MemberInfo("")) + return MyCollectVisitor(mode, at.project, MemberInfo()) } return target?.let { MyCollectVisitor(mode, at.project, it) } } diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/NewInsnInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/NewInsnInjectionPoint.kt index 23400c63c..97a35bb7e 100644 --- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/NewInsnInjectionPoint.kt +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/NewInsnInjectionPoint.kt @@ -102,7 +102,7 @@ class NewInsnInjectionPoint : InjectionPoint() { mode: CollectVisitor.Mode, ): CollectVisitor? { if (mode == CollectVisitor.Mode.COMPLETION) { - return MyCollectVisitor(mode, at.project, MemberInfo("")) + return MyCollectVisitor(mode, at.project, MemberInfo()) } return getTarget(at, target)?.let { MyCollectVisitor(mode, at.project, it) } } diff --git a/src/main/kotlin/platform/mixin/inspection/reference/AmbiguousReferenceInspection.kt b/src/main/kotlin/platform/mixin/inspection/reference/AmbiguousReferenceInspection.kt index abbff266e..cfaba0a1c 100644 --- a/src/main/kotlin/platform/mixin/inspection/reference/AmbiguousReferenceInspection.kt +++ b/src/main/kotlin/platform/mixin/inspection/reference/AmbiguousReferenceInspection.kt @@ -24,6 +24,7 @@ import com.demonwav.mcdev.platform.mixin.handlers.InjectorAnnotationHandler import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler import com.demonwav.mcdev.platform.mixin.inspection.MixinAnnotationAttributeInspection import com.demonwav.mcdev.platform.mixin.reference.MethodReference +import com.demonwav.mcdev.util.Quantifier import com.demonwav.mcdev.util.constantStringValue import com.intellij.codeInspection.LocalQuickFix import com.intellij.codeInspection.ProblemDescriptor @@ -58,7 +59,7 @@ class AmbiguousReferenceInspection : MixinAnnotationAttributeInspection("method" private fun checkMember(value: PsiAnnotationMemberValue, holder: ProblemsHolder) { val ambiguousReference = MethodReference.getReferenceIfAmbiguous(value) ?: return - if (ambiguousReference.matchAllNames || ambiguousReference.matchAllDescs) { + if (ambiguousReference.name == null || ambiguousReference.quantifier == Quantifier.Any) { // the intent of ambiguity is clear return } diff --git a/src/main/kotlin/platform/mixin/reference/target/DefinitionReferences.kt b/src/main/kotlin/platform/mixin/reference/target/DefinitionReferences.kt index 684b3a770..bccd279de 100644 --- a/src/main/kotlin/platform/mixin/reference/target/DefinitionReferences.kt +++ b/src/main/kotlin/platform/mixin/reference/target/DefinitionReferences.kt @@ -83,7 +83,7 @@ abstract class AbstractDefinitionReference : PolyReferenceResolver(), MixinRefer override fun collectVariants(context: PsiElement) = resolveInBytecode( context, - MemberInfo("*", null, null, matchAllNames = true, matchAllDescs = true) + MemberInfo() ).mapToArray { LookupElementBuilder.create(referenceToString(it)) .withPresentableText(it.presentableText) diff --git a/src/main/kotlin/platform/mixin/util/MemberInfo.kt b/src/main/kotlin/platform/mixin/util/MemberInfo.kt index cfd5eef4d..c7507c9a5 100644 --- a/src/main/kotlin/platform/mixin/util/MemberInfo.kt +++ b/src/main/kotlin/platform/mixin/util/MemberInfo.kt @@ -21,6 +21,7 @@ package com.demonwav.mcdev.platform.mixin.util import com.demonwav.mcdev.platform.mixin.reference.MixinSelector +import com.demonwav.mcdev.util.Quantifier import com.intellij.openapi.util.text.StringUtil import org.objectweb.asm.Type @@ -28,24 +29,16 @@ import org.objectweb.asm.Type * Represents a Mixin MemberInfo. */ data class MemberInfo( - val name: String, + val name: String? = null, val descriptor: String? = null, override val owner: String? = null, - val matchAllNames: Boolean = false, - val matchAllDescs: Boolean = false, + val quantifier: Quantifier = Quantifier.Default, ) : MixinSelector { init { assert(owner?.contains('/') != true) } - val withoutDescriptor - get() = if (this.descriptor == null) { - this - } else { - copy(descriptor = null) - } - val withoutOwner get() = if (this.owner == null) { this @@ -56,21 +49,8 @@ data class MemberInfo( override val methodDescriptor = descriptor?.takeIf { it.contains("(") } override val fieldDescriptor = descriptor?.takeUnless { it.contains("(") } - val presentableText: String get() = buildString { - if (owner != null) { - append(owner.substringAfterLast('.')) - append('.') - } - append(name) - if (descriptor != null && descriptor.startsWith("(")) { - append('(') - append(Type.getArgumentTypes(descriptor).joinToString { it.className.substringAfterLast('.') }) - append(')') - } - } - override fun canEverMatch(name: String): Boolean { - return matchAllNames || this.name == name + return this.name == null || this.name == name } private fun matchOwner(clazz: String): Boolean { @@ -80,14 +60,14 @@ data class MemberInfo( override fun matchField(owner: String, name: String, desc: String): Boolean { assert(!owner.contains('.')) - return (this.matchAllNames || this.name == name) && + return canEverMatch(name) && matchOwner(owner) && (this.descriptor == null || this.descriptor == desc) } override fun matchMethod(owner: String, name: String, desc: String): Boolean { assert(!owner.contains('.')) - return (this.matchAllNames || this.name == name) && + return canEverMatch(name) && matchOwner(owner) && (this.descriptor == null || this.descriptor == desc) } @@ -98,7 +78,8 @@ data class MemberInfo( append('L').append(owner.replace('.', '/')).append(';') } - append(if (matchAllNames) "*" else name) + name?.let(::append) + append(quantifier) descriptor?.let { descriptor -> if (!descriptor.startsWith('(')) { @@ -112,70 +93,61 @@ data class MemberInfo( } companion object { - fun parse(value: String): MemberInfo? { - val reference = value.replace(" ", "") - val owner: String? - - var pos = reference.lastIndexOf('.') - if (pos != -1) { - // Everything before the dot is the qualifier/owner - owner = reference.substring(0, pos).replace('/', '.') - } else { - pos = reference.indexOf(';') - if (pos != -1 && reference.startsWith('L')) { - val internalOwner = reference.substring(1, pos) - if (!StringUtil.isJavaIdentifier(internalOwner.replace('/', '_'))) { - // Invalid: Qualifier should only contain slashes - return null - } - - owner = internalOwner.replace('/', '.') - - // if owner is all there is to the selector, match anything with the owner - if (pos == reference.length - 1) { - return MemberInfo("", null, owner, matchAllNames = true, matchAllDescs = true) - } - } else { - // No owner/qualifier specified - pos = -1 - owner = null - } + fun parse(input: String): MemberInfo? { + var desc: String? = null + var owner: String? = null + var name: String = input.trim() + + val parenPos = name.indexOf('(') + val colonPos = name.indexOf(':') + if (parenPos > -1) { + desc = name.substring(parenPos).trim() + name = name.substring(0, parenPos).trim() + } else if (colonPos > -1) { + desc = name.substring(colonPos + 1).trim() + name = name.substring(0, colonPos).trim() + } + + val lastDotPos = name.lastIndexOf('.') + val semiColonPos = name.indexOf(';') + if (lastDotPos > -1) { + owner = name.substring(0, lastDotPos).replace('/', '.').trim() + name = name.substring(lastDotPos + 1).trim() + } else if (semiColonPos > -1 && name.startsWith("L")) { + owner = name.substring(1, semiColonPos).replace('/', '.').trim() + name = name.substring(semiColonPos + 1).trim() } - val descriptor: String? - val name: String - val matchAllNames = reference.getOrNull(pos + 1) == '*' - val matchAllDescs: Boolean - - // Find descriptor separator - val methodDescPos = reference.indexOf('(', pos + 1) - if (methodDescPos != -1) { - // Method descriptor - descriptor = reference.substring(methodDescPos) - name = reference.substring(pos + 1, methodDescPos) - matchAllDescs = false - } else { - val fieldDescPos = reference.indexOf(':', pos + 1) - if (fieldDescPos != -1) { - descriptor = reference.substring(fieldDescPos + 1) - name = reference.substring(pos + 1, fieldDescPos) - matchAllDescs = false - } else { - descriptor = null - matchAllDescs = reference.endsWith('*') - name = if (matchAllDescs) { - reference.substring(pos + 1, reference.lastIndex) - } else { - reference.substring(pos + 1) - } + if ((name.contains('/') || name.contains('.')) && owner == null) { + owner = name.replace('/', '.') + name = "" + } + + var quantifier: Quantifier = Quantifier.Default + if (name.endsWith('*')) { + quantifier = Quantifier.Any + name = name.dropLast(1).trim() + } else if (name.endsWith('+')) { + quantifier = Quantifier.Plus + name = name.dropLast(1).trim() + } else if (name.endsWith('}')) { + val bracePos = name.indexOf('{') + if (bracePos >= 0) { + quantifier = Quantifier.parse(name.substring(bracePos, name.length)) ?: return null + name = name.substring(0, bracePos).trim() } + } else if (name.contains('{')) { + return null // Probably incomplete quantifier } - if (!matchAllNames && !StringUtil.isJavaIdentifier(name) && name != "" && name != "") { + if (owner != null && !StringUtil.isJavaIdentifier(owner.replace('.', '_'))) { + return null + } + if (name.isNotEmpty() && !StringUtil.isJavaIdentifier(name) && name != "" && name != "") { return null } - return MemberInfo(if (matchAllNames) "*" else name, descriptor, owner, matchAllNames, matchAllDescs) + return MemberInfo(name.takeIf { it.isNotEmpty() }, desc, owner, quantifier) } } } diff --git a/src/main/kotlin/util/Quantifier.kt b/src/main/kotlin/util/Quantifier.kt new file mode 100644 index 000000000..f5961231d --- /dev/null +++ b/src/main/kotlin/util/Quantifier.kt @@ -0,0 +1,112 @@ +/* + * 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 + +sealed interface Quantifier { + fun min(ctx: Context): Int + + fun max(ctx: Context): Int + + data object Default : Quantifier { + override fun min(ctx: Context) = when (ctx) { + Context.MEMBER, Context.INSTRUCTION -> 0 + } + + override fun max(ctx: Context) = when (ctx) { + Context.MEMBER -> 1 + Context.INSTRUCTION -> Int.MAX_VALUE + } + + override fun toString() = "" + } + + data class Exact(val min: Int, val max: Int) : Quantifier { + override fun min(ctx: Context) = min + + override fun max(ctx: Context) = max + + override fun toString() = when (this) { + Any -> "*" + Plus -> "+" + else -> buildString { + append('{') + if (min == max) { + append(min) + } else { + if (min != 0) { + append(min) + } + append(',') + if (max != Int.MAX_VALUE) { + append(max) + } + } + append('}') + } + } + } + + enum class Context { + MEMBER, INSTRUCTION + } + + companion object { + val Any: Quantifier = Exact(0, Int.MAX_VALUE) + val Plus: Quantifier = Exact(1, Int.MAX_VALUE) + + fun parse(stringIn: String): Quantifier? { + val string = stringIn.trim() + if (string.isEmpty()) { + return Default + } + + if (string == "*") { + return Any + } + + if (string == "+") { + return Plus + } + + if (!string.startsWith('{') || !string.endsWith('}') || string.length < 3) { + return null // malformed + } + + val inner = string.substring(1, string.length - 1).trim() + if (inner.isEmpty()) { + return null + } + + var strMin = inner + var strMax = inner + + val comma = inner.indexOf(',') + if (comma > -1) { + strMin = inner.substring(0, comma).trim() + strMax = inner.substring(comma + 1).trim() + } + + val min = if (strMin.isEmpty()) 0 else strMin.toIntOrNull()?.takeIf { it >= 0 } ?: return null + val max = if (strMax.isEmpty()) Int.MAX_VALUE else strMax.toIntOrNull()?.takeIf { it >= 0 } ?: return null + return Exact(min, max) + } + } +} From 472b9a70ddc398298e9c2e392bd4fdc0a55e307f Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Wed, 2 Sep 2026 20:34:03 +0100 Subject: [PATCH 3/8] Fix: Handle all cases properly in AmbiguousReferenceInspection. Really nothing is "ambiguous" (except perhaps to the reader), it's just that Mixin's default behaviour of matching the first method in a given class is unexpected, but I'll leave the terminology for now. Adding any explicit quantifier suppresses this inspection, including `{1}` if that's really what's desired. Additionally, adding a wildcard is almost never the correct fix. Choosing one of the available descriptors is what should be offered instead, but I'll leave that too for now. --- .../ambiguousReference/MixedIn.java | 5 +- .../reference/AmbiguousReferenceInspection.kt | 19 ++--- .../reference/AbstractMethodReference.kt | 11 +-- .../mixin/AmbiguousReferenceInspectionTest.kt | 75 ++++++++++++++++++- 4 files changed, 87 insertions(+), 23 deletions(-) diff --git a/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/ambiguousReference/MixedIn.java b/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/ambiguousReference/MixedIn.java index 6e4a97a4a..f0baf9281 100644 --- a/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/ambiguousReference/MixedIn.java +++ b/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/ambiguousReference/MixedIn.java @@ -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 @@ -29,4 +29,7 @@ public void method(String string) { public void uniqueMethod(String string) { } + + public void uniqueDescMethod(int i) { + } } diff --git a/src/main/kotlin/platform/mixin/inspection/reference/AmbiguousReferenceInspection.kt b/src/main/kotlin/platform/mixin/inspection/reference/AmbiguousReferenceInspection.kt index cfaba0a1c..0dae40c79 100644 --- a/src/main/kotlin/platform/mixin/inspection/reference/AmbiguousReferenceInspection.kt +++ b/src/main/kotlin/platform/mixin/inspection/reference/AmbiguousReferenceInspection.kt @@ -24,6 +24,7 @@ import com.demonwav.mcdev.platform.mixin.handlers.InjectorAnnotationHandler import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler import com.demonwav.mcdev.platform.mixin.inspection.MixinAnnotationAttributeInspection import com.demonwav.mcdev.platform.mixin.reference.MethodReference +import com.demonwav.mcdev.platform.mixin.util.MemberInfo import com.demonwav.mcdev.util.Quantifier import com.demonwav.mcdev.util.constantStringValue import com.intellij.codeInspection.LocalQuickFix @@ -59,13 +60,13 @@ class AmbiguousReferenceInspection : MixinAnnotationAttributeInspection("method" private fun checkMember(value: PsiAnnotationMemberValue, holder: ProblemsHolder) { val ambiguousReference = MethodReference.getReferenceIfAmbiguous(value) ?: return - if (ambiguousReference.name == null || ambiguousReference.quantifier == Quantifier.Any) { + if (ambiguousReference.quantifier != Quantifier.Default) { // the intent of ambiguity is clear return } holder.registerProblem( value, - "Ambiguous reference to method '${ambiguousReference.name}' in target class", + "Ambiguous reference to method in target class", QuickFix, ) } @@ -75,18 +76,14 @@ class AmbiguousReferenceInspection : MixinAnnotationAttributeInspection("method" override fun applyFix(project: Project, descriptor: ProblemDescriptor) { val element = descriptor.psiElement ?: return - val constantValue = element.constantStringValue + val constantValue = element.constantStringValue ?: return + val info = MemberInfo.parse(constantValue) ?: return + val newText = info.copy(quantifier = Quantifier.Any).toMixinString() val elementFactory = JavaPsiFacade.getElementFactory(project) - if (constantValue != null) { - val newLiteral = "\"${StringUtil.escapeStringCharacters("$constantValue*")}\"" - element.replace(elementFactory.createExpressionFromText(newLiteral, null)) - } else { - val replacement = elementFactory.createExpressionFromText("str + \"*\"", null) as PsiBinaryExpression - replacement.lOperand.replace(element) - element.replace(replacement) - } + val newLiteral = "\"${StringUtil.escapeStringCharacters(newText)}\"" + element.replace(elementFactory.createExpressionFromText(newLiteral, null)) } } } diff --git a/src/main/kotlin/platform/mixin/reference/AbstractMethodReference.kt b/src/main/kotlin/platform/mixin/reference/AbstractMethodReference.kt index 9196c8258..6e889874d 100644 --- a/src/main/kotlin/platform/mixin/reference/AbstractMethodReference.kt +++ b/src/main/kotlin/platform/mixin/reference/AbstractMethodReference.kt @@ -89,7 +89,8 @@ abstract class AbstractMethodReference : PolyReferenceResolver(), MixinReference fun getReferenceIfAmbiguous(context: PsiElement): MemberInfo? { val targetReference = parseSelector(context) as? MemberInfo ?: return null - if (targetReference.descriptor != null) { + if (targetReference.name != null && targetReference.descriptor != null) { + // Not ambiguous return null } @@ -98,13 +99,7 @@ abstract class AbstractMethodReference : PolyReferenceResolver(), MixinReference } private fun isAmbiguous(targets: Collection, targetReference: MemberInfo): Boolean { - if (targetReference.name == null) { - return targets.any { - val methods = it.methods - methods != null && methods.size > 1 - } - } - return targets.any { it.findMethods(MemberReference(targetReference.name)).count() > 1 } + return targets.any { it.findMethods(targetReference).drop(1).any() } } fun resolve(context: PsiElement): Sequence? { diff --git a/src/test/kotlin/platform/mixin/AmbiguousReferenceInspectionTest.kt b/src/test/kotlin/platform/mixin/AmbiguousReferenceInspectionTest.kt index 0a44f3357..b68bc20d5 100644 --- a/src/test/kotlin/platform/mixin/AmbiguousReferenceInspectionTest.kt +++ b/src/test/kotlin/platform/mixin/AmbiguousReferenceInspectionTest.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 @@ -57,7 +57,7 @@ class AmbiguousReferenceInspectionTest : BaseMixinTest() { @Mixin(MixedIn.class) class AmbiguousReferenceMixin { - @Inject(method = "method", at = @At("HEAD")) + @Inject(method = "method", at = @At("HEAD")) public void onMethod() { } } @@ -103,7 +103,7 @@ class AmbiguousReferenceInspectionTest : BaseMixinTest() { @Mixin(MixedIn.class) class AmbiguousReferenceMixin { - @Inject(method = {"method", "uniqueMethod"}, at = @At("HEAD")) + @Inject(method = {"method", "uniqueMethod"}, at = @At("HEAD")) public void onMethod() { } } @@ -156,4 +156,73 @@ class AmbiguousReferenceInspectionTest : BaseMixinTest() { """, ) } + + @Test + @DisplayName("Explicit single quantifier") + fun explicitSingleQuantifier() { + doTest( + """ + package test; + + import com.demonwav.mcdev.mixintestdata.ambiguousReference.MixedIn; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MixedIn.class) + class AmbiguousReferenceMixin { + + @Inject(method = {"method{1}", "uniqueMethod"}, at = @At("HEAD")) + public void onMethod() { + } + } + """, + ) + } + + @Test + @DisplayName("Ambiguous Reference Desc") + fun ambiguousReferenceDesc() { + doTest( + """ + package test; + + import com.demonwav.mcdev.mixintestdata.ambiguousReference.MixedIn; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MixedIn.class) + class AmbiguousReferenceMixin { + + @Inject(method = {"()V", "uniqueMethod"}, at = @At("HEAD")) + public void onMethod() { + } + } + """, + ) + } + + @Test + @DisplayName("No Ambiguous Reference Desc") + fun noAmbiguousReferenceDesc() { + doTest( + """ + package test; + + import com.demonwav.mcdev.mixintestdata.ambiguousReference.MixedIn; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MixedIn.class) + class AmbiguousReferenceMixin { + + @Inject(method = {"(I)V", "uniqueMethod"}, at = @At("HEAD")) + public void onMethod() { + } + } + """, + ) + } } From 0a1471f8cce8b0b7d9fee0ed4053d6d88c4e8ea9 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Wed, 2 Sep 2026 23:02:56 +0100 Subject: [PATCH 4/8] New: Handle maximum values in quantifiers. --- .../MixinTargetDescriptorFoldingBuilder.kt | 4 +- .../ConstantStringMethodInjectionPoint.kt | 4 + .../injectionPoint/FieldInjectionPoint.kt | 4 + .../handlers/injectionPoint/InjectionPoint.kt | 13 +++- .../InvokeAssignInjectionPoint.kt | 4 + .../injectionPoint/InvokeInjectionPoint.kt | 4 + .../injectionPoint/NewInsnInjectionPoint.kt | 31 ++++++-- ...nvalidInjectorMethodSignatureInspection.kt | 2 +- .../reference/AbstractMethodReference.kt | 25 +------ .../mixin/reference/MixinSelectors.kt | 25 ++++++- .../mixin/reference/target/TargetReference.kt | 27 +++++-- .../kotlin/platform/mixin/util/AsmUtil.kt | 11 ++- .../kotlin/platform/mixin/util/MemberInfo.kt | 4 +- src/main/kotlin/util/MemberMatcher.kt | 75 +------------------ src/main/kotlin/util/MemberReference.kt | 68 ++++++++++++++++- src/main/kotlin/util/sequences.kt | 18 +++++ 16 files changed, 199 insertions(+), 120 deletions(-) diff --git a/src/main/kotlin/platform/mixin/folding/MixinTargetDescriptorFoldingBuilder.kt b/src/main/kotlin/platform/mixin/folding/MixinTargetDescriptorFoldingBuilder.kt index d54bc6101..d1c2bb416 100644 --- a/src/main/kotlin/platform/mixin/folding/MixinTargetDescriptorFoldingBuilder.kt +++ b/src/main/kotlin/platform/mixin/folding/MixinTargetDescriptorFoldingBuilder.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 @@ -51,7 +51,7 @@ class MixinTargetDescriptorFoldingBuilder : CustomFoldingBuilder() { override fun getLanguagePlaceholderText(node: ASTNode, range: TextRange): String? { val element = node.psi - return TargetReference.resolveTarget(element)?.let { formatElement(it) } + return TargetReference.resolveTargets(element).singleOrNull()?.let { formatElement(it) } } private fun formatElement(element: PsiElement): String? { diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantStringMethodInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantStringMethodInjectionPoint.kt index bca4d9997..9c544f5c4 100644 --- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantStringMethodInjectionPoint.kt +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantStringMethodInjectionPoint.kt @@ -26,6 +26,7 @@ import com.demonwav.mcdev.platform.mixin.util.MemberInfo import com.demonwav.mcdev.platform.mixin.util.MethodTargetMember import com.demonwav.mcdev.platform.mixin.util.fakeResolve import com.demonwav.mcdev.platform.mixin.util.findOrConstructSourceMethod +import com.demonwav.mcdev.util.Quantifier import com.demonwav.mcdev.util.constantStringValue import com.demonwav.mcdev.util.createLiteralExpression import com.demonwav.mcdev.util.toTypedArray @@ -204,6 +205,9 @@ class ConstantStringMethodInjectionPoint : AbstractMethodInjectionPoint() { private val selector: MixinSelector, private val ldc: String?, ) : CollectVisitor(mode) { + override val quantifier: Quantifier + get() = selector.quantifier + override fun accept(methodNode: MethodNode) = sequence { val insns = methodNode.instructions ?: return@sequence var seenStringConstant: String? = null diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/FieldInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/FieldInjectionPoint.kt index e29242961..b81d3fffa 100644 --- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/FieldInjectionPoint.kt +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/FieldInjectionPoint.kt @@ -24,6 +24,7 @@ import com.demonwav.mcdev.platform.mixin.reference.MixinSelector import com.demonwav.mcdev.platform.mixin.util.MemberInfo import com.demonwav.mcdev.platform.mixin.util.fakeResolve import com.demonwav.mcdev.platform.mixin.util.findOrConstructSourceField +import com.demonwav.mcdev.util.Quantifier import com.demonwav.mcdev.util.constantValue import com.intellij.codeInsight.completion.JavaLookupElementBuilder import com.intellij.codeInsight.lookup.LookupElementBuilder @@ -197,6 +198,9 @@ class FieldInjectionPoint : QualifiedInjectionPoint() { private val arrayAccess: ArrayAccessType?, private val fuzz: Int, ) : CollectVisitor(mode) { + override val quantifier: Quantifier + get() = selector.quantifier + override fun accept(methodNode: MethodNode) = sequence { val insns = methodNode.instructions ?: return@sequence for (insn in insns) { diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.kt index ef65ec262..120616ef6 100644 --- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.kt +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.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 @@ -26,6 +26,7 @@ import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Annotations.SLICE import com.demonwav.mcdev.platform.mixin.util.SourceCodeLocationInfo import com.demonwav.mcdev.platform.mixin.util.fakeResolve import com.demonwav.mcdev.platform.mixin.util.findOrConstructSourceMethod +import com.demonwav.mcdev.util.Quantifier import com.demonwav.mcdev.util.constantStringValue import com.demonwav.mcdev.util.constantValue import com.demonwav.mcdev.util.createLiteralExpression @@ -162,6 +163,7 @@ abstract class InjectionPoint { // results are shown. if (mode != CollectVisitor.Mode.COMPLETION) { addOrdinalFilter(at, targetClass, collectVisitor) + addQuantifierFilter(at, targetClass, collectVisitor) addSpecifierFilter(at, targetClass, collectVisitor, defaultSpecifier) } } @@ -219,6 +221,13 @@ abstract class InjectionPoint { } } + protected open fun addQuantifierFilter(at: PsiAnnotation, targetClass: ClassNode, collectVisitor: CollectVisitor) { + val maxMatches = collectVisitor.quantifier.max(Quantifier.Context.INSTRUCTION) + collectVisitor.addResultFilter("quantifier") { results, _ -> + results.take(maxMatches) + } + } + protected open fun addSpecifierFilter( at: PsiAnnotation, targetClass: ClassNode, @@ -356,6 +365,8 @@ abstract class NavigationVisitor : JavaRecursiveElementVisitor() { } abstract class CollectVisitor(protected val mode: Mode) { + open val quantifier: Quantifier get() = Quantifier.Any + fun visit(methodNode: MethodNode): InsnResolutionInfo { val numRetained = IntArray(resultFilters.size + 1) var results = accept(methodNode).onEach { numRetained[0]++ } diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeAssignInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeAssignInjectionPoint.kt index d406528b2..453edb749 100644 --- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeAssignInjectionPoint.kt +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeAssignInjectionPoint.kt @@ -22,6 +22,7 @@ package com.demonwav.mcdev.platform.mixin.handlers.injectionPoint import com.demonwav.mcdev.platform.mixin.reference.MixinSelector import com.demonwav.mcdev.platform.mixin.util.MemberInfo +import com.demonwav.mcdev.util.Quantifier import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project import com.intellij.psi.CommonClassNames @@ -162,6 +163,9 @@ class InvokeAssignInjectionPoint : AbstractMethodInjectionPoint() { private val fuzz: Int, private val skip: Set, ) : CollectVisitor(mode) { + override val quantifier: Quantifier + get() = selector.quantifier + override fun accept(methodNode: MethodNode) = sequence { val insns = methodNode.instructions ?: return@sequence for (insn in insns) { diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeInjectionPoint.kt index be0c904ea..ea5a94aac 100644 --- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeInjectionPoint.kt +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeInjectionPoint.kt @@ -22,6 +22,7 @@ package com.demonwav.mcdev.platform.mixin.handlers.injectionPoint import com.demonwav.mcdev.platform.mixin.reference.MixinSelector import com.demonwav.mcdev.platform.mixin.util.MemberInfo +import com.demonwav.mcdev.util.Quantifier import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project import com.intellij.psi.CommonClassNames @@ -153,6 +154,9 @@ class InvokeInjectionPoint : AbstractMethodInjectionPoint() { private val project: Project, private val selector: MixinSelector, ) : CollectVisitor(mode) { + override val quantifier: Quantifier + get() = selector.quantifier + override fun accept(methodNode: MethodNode) = sequence { val insns = methodNode.instructions ?: return@sequence for (insn in insns) { diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/NewInsnInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/NewInsnInjectionPoint.kt index 97a35bb7e..185bccac5 100644 --- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/NewInsnInjectionPoint.kt +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/NewInsnInjectionPoint.kt @@ -28,6 +28,9 @@ 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 import com.demonwav.mcdev.util.internalName @@ -139,8 +142,11 @@ class NewInsnInjectionPoint : InjectionPoint() { val anonymousClass = expression.anonymousClass val anonymousName = anonymousClass?.fullQualifiedName?.replace('.', '/') if (anonymousName != null) { - val method = findClassNodeByPsiClass(anonymousClass)?.findMethod(selector) - if (method != null && selector.matchMethod(anonymousName, method.name, method.desc)) { + val methods = findClassNodeByPsiClass(anonymousClass) + ?.findMethods(selector.withQuantifier(Quantifier.Default)) + .orEmpty() + + if (methods.any { selector.matchMethod(anonymousName, it.name, it.desc) }) { addResult(expression) } } else { @@ -215,17 +221,21 @@ class NewInsnSelectorParser : MixinSelectorParser { if (!at.hasQualifiedName(AT)) return null if (at.findAttributeValue("value")?.constantStringValue != "NEW") return null - val strippedValue = value.replace(" ", "") - return if (strippedValue.startsWith('(')) { - NewInsnDescriptorSelector(strippedValue) + val parsed = MemberInfo.parse(value) ?: return null + return if (parsed.owner == null && parsed.name == null) { + NewInsnDescriptorSelector(parsed.descriptor ?: return null, parsed.quantifier) } else { - NewInsnTypeSelector(strippedValue.removeSurrounding("L", ";")) + if (parsed.name != null || parsed.descriptor != null) { + return null + } + NewInsnTypeSelector(parsed.owner?.replace('.', '/') ?: return null, parsed.quantifier) } } } -private class NewInsnTypeSelector( +private data class NewInsnTypeSelector( override val owner: String, + override val quantifier: Quantifier, ) : MixinSelector { override fun matchField(owner: String, name: String, desc: String) = false @@ -235,10 +245,13 @@ private class NewInsnTypeSelector( override val fieldDescriptor = null override val methodDescriptor = null + + override fun withQuantifier(quantifier: Quantifier) = copy(quantifier = quantifier) } -private class NewInsnDescriptorSelector( +private data class NewInsnDescriptorSelector( override val methodDescriptor: String, + override val quantifier: Quantifier, ) : MixinSelector { override fun matchField(owner: String, name: String, desc: String): Boolean = false @@ -255,6 +268,8 @@ private class NewInsnDescriptorSelector( override val owner = null override val fieldDescriptor = null + + override fun withQuantifier(quantifier: Quantifier) = copy(quantifier = quantifier) } private fun classToMemberInfo(value: String): MemberInfo? { diff --git a/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt b/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt index c5ecfeb78..dc22e5f53 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt @@ -98,7 +98,7 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() { val handler = MixinAnnotationHandler.forMixinAnnotation(annotation, annotation.project) as? InjectorAnnotationHandler ?: continue val methodAttribute = annotation.findDeclaredAttributeValue("method") ?: continue - val targetMethods = MethodReference.resolveAllIfNotAmbiguous(methodAttribute) ?: continue + val targetMethods = MethodReference.resolve(methodAttribute) ?: continue val hasDisallowedInsns = targetMethods.any { classAndMethod -> handler.resolveInstructions( diff --git a/src/main/kotlin/platform/mixin/reference/AbstractMethodReference.kt b/src/main/kotlin/platform/mixin/reference/AbstractMethodReference.kt index 6e889874d..42770309a 100644 --- a/src/main/kotlin/platform/mixin/reference/AbstractMethodReference.kt +++ b/src/main/kotlin/platform/mixin/reference/AbstractMethodReference.kt @@ -32,7 +32,9 @@ import com.demonwav.mcdev.platform.mixin.util.findUpstreamMixin import com.demonwav.mcdev.platform.mixin.util.memberReference import com.demonwav.mcdev.platform.mixin.util.mixinTargets import com.demonwav.mcdev.util.MemberReference +import com.demonwav.mcdev.util.Quantifier import com.demonwav.mcdev.util.constantStringValue +import com.demonwav.mcdev.util.countIs import com.demonwav.mcdev.util.findContainingClass import com.demonwav.mcdev.util.findContainingMethod import com.demonwav.mcdev.util.reference.PolyReferenceResolver @@ -99,7 +101,7 @@ abstract class AbstractMethodReference : PolyReferenceResolver(), MixinReference } private fun isAmbiguous(targets: Collection, targetReference: MemberInfo): Boolean { - return targets.any { it.findMethods(targetReference).drop(1).any() } + return targets.any { it.findMethods(targetReference.withQuantifier(Quantifier.Any)).countIs > 1 } } fun resolve(context: PsiElement): Sequence? { @@ -126,27 +128,6 @@ abstract class AbstractMethodReference : PolyReferenceResolver(), MixinReference } } - fun resolveAllIfNotAmbiguous(context: PsiElement): List? { - val targets = getTargets(context) ?: return null - - val targetedMethods = when (context) { - is PsiArrayInitializerMemberValue -> context.initializers.mapNotNull { it.constantStringValue } - else -> context.constantStringValue?.let { listOf(it) } ?: emptyList() - } - - return targetedMethods.asSequence().flatMap { method -> - val targetReference = parseSelector(method, context) ?: return@flatMap emptySequence() - if (targetReference is MemberInfo && targetReference.descriptor == null && isAmbiguous( - targets, - targetReference, - ) - ) { - return@flatMap emptySequence() - } - return@flatMap resolve(targets, targetReference) - }.toList() - } - fun resolveForNavigation(context: PsiElement): Array? { return resolve(context)?.mapNotNull { it.method.findSourceElement( diff --git a/src/main/kotlin/platform/mixin/reference/MixinSelectors.kt b/src/main/kotlin/platform/mixin/reference/MixinSelectors.kt index cc381d573..4bc7b8c28 100644 --- a/src/main/kotlin/platform/mixin/reference/MixinSelectors.kt +++ b/src/main/kotlin/platform/mixin/reference/MixinSelectors.kt @@ -26,8 +26,10 @@ import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Annotations.DESC import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Annotations.SLICE import com.demonwav.mcdev.platform.mixin.util.mixinTargets import com.demonwav.mcdev.util.MemberMatcher +import com.demonwav.mcdev.util.Quantifier import com.demonwav.mcdev.util.cached import com.demonwav.mcdev.util.constantStringValue +import com.demonwav.mcdev.util.constantValue import com.demonwav.mcdev.util.descriptor import com.demonwav.mcdev.util.findAnnotation import com.demonwav.mcdev.util.findContainingClass @@ -90,7 +92,11 @@ interface MixinSelectorParser { /** * An interface which represents Mixin target selectors. */ -interface MixinSelector : MemberMatcher +interface MixinSelector : MemberMatcher { + val quantifier: Quantifier + + fun withQuantifier(quantifier: Quantifier): MixinSelector +} class MixinMemberParser : MixinSelectorParser { override fun parse(value: String, context: PsiElement) = MemberInfo.parse(value) @@ -168,6 +174,7 @@ private class MixinRegexSelector( val descPattern: Regex, override val owner: String?, descriptor: String?, + override val quantifier: Quantifier = Quantifier.Any ) : MixinSelector { override fun matchField(owner: String, name: String, desc: String): Boolean { return ownerPattern.containsMatchIn(owner) && @@ -188,6 +195,9 @@ private class MixinRegexSelector( override val methodDescriptor = descriptor?.takeIf { it.contains("(") } override val fieldDescriptor = descriptor?.takeUnless { it.contains("(") } + override fun withQuantifier(quantifier: Quantifier) = MixinRegexSelector( + ownerPattern, namePattern, descPattern, owner, methodDescriptor ?: fieldDescriptor, quantifier + ) } // Dynamic selectors @@ -455,7 +465,15 @@ class DescSelectorParser : DynamicSelectorParser("Desc", "mixin:Desc") { *argTypes.mapToArray { Type.getType(it.descriptor) }, ) - return DescSelector(owners, name, desc) + val min = descAnnotation.findAttributeValue("min")?.constantValue as? Int + val max = descAnnotation.findAttributeValue("max")?.constantValue as? Int + val quantifier = if (min == null && max == null) { + Quantifier.Default + } else { + Quantifier.Exact(min ?: 0, max ?: Int.MAX_VALUE) + } + + return DescSelector(owners, name, desc, quantifier) } } } @@ -464,6 +482,7 @@ data class DescSelector( val owners: Set, val name: String, override val methodDescriptor: String, + override val quantifier: Quantifier, ) : MixinSelector { override fun matchField(owner: String, name: String, desc: String): Boolean { return this.owners.contains(owner) && this.name == name && this.fieldDescriptor.substringBefore("(") == desc @@ -479,4 +498,6 @@ data class DescSelector( override val owner = owners.singleOrNull() override val fieldDescriptor = methodDescriptor.substringBefore('(') + + override fun withQuantifier(quantifier: Quantifier) = copy(quantifier = quantifier) } diff --git a/src/main/kotlin/platform/mixin/reference/target/TargetReference.kt b/src/main/kotlin/platform/mixin/reference/target/TargetReference.kt index b49b1627b..451b7762d 100644 --- a/src/main/kotlin/platform/mixin/reference/target/TargetReference.kt +++ b/src/main/kotlin/platform/mixin/reference/target/TargetReference.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 @@ -28,10 +28,12 @@ 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.MixinConstants.Annotations.AT +import com.demonwav.mcdev.util.findQualifiedClass import com.demonwav.mcdev.util.ifEmpty import com.demonwav.mcdev.util.insideAnnotationAttribute import com.demonwav.mcdev.util.reference.PolyReferenceResolver import com.demonwav.mcdev.util.reference.completeToLiteral +import com.demonwav.mcdev.util.toTypedArray import com.intellij.openapi.project.Project import com.intellij.patterns.ElementPattern import com.intellij.patterns.PsiJavaPatterns @@ -42,6 +44,7 @@ import com.intellij.psi.PsiElementResolveResult import com.intellij.psi.PsiLiteral import com.intellij.psi.PsiMember import com.intellij.psi.ResolveResult +import com.intellij.psi.search.GlobalSearchScope import com.intellij.psi.util.parentOfType import com.intellij.util.ArrayUtilRt @@ -60,9 +63,22 @@ object TargetReference : PolyReferenceResolver(), MixinReference { override fun isValidAnnotation(name: String, project: Project) = name == AT - fun resolveTarget(context: PsiElement): PsiMember? { - val selector = parseMixinSelector(context) ?: return null - return selector.resolveMember(context.project) + fun resolveTargets(context: PsiElement): Sequence { + val selector = parseMixinSelector(context) ?: return emptySequence() + val owner = selector.owner ?: return emptySequence() + val project = context.project + val psiClass = findQualifiedClass(project, owner, GlobalSearchScope.allScope(project)) + ?: return emptySequence() + + var result = emptySequence() + if (selector.canMatchFields) { + result += psiClass.allFields.asSequence().filter { selector.matchField(it, psiClass) } + } + if (selector.canMatchMethods) { + result += psiClass.allMethods.asSequence().filter { selector.matchMethod(it, psiClass) } + } + + return result } /** @@ -101,8 +117,7 @@ object TargetReference : PolyReferenceResolver(), MixinReference { } override fun resolveReference(context: PsiElement): Array { - val result = resolveTarget(context) ?: return ResolveResult.EMPTY_ARRAY - return arrayOf(PsiElementResolveResult(result)) + return resolveTargets(context).map(::PsiElementResolveResult).toTypedArray() } override fun collectVariants(context: PsiElement): Array { diff --git a/src/main/kotlin/platform/mixin/util/AsmUtil.kt b/src/main/kotlin/platform/mixin/util/AsmUtil.kt index 60d927f3b..f1a1407e4 100644 --- a/src/main/kotlin/platform/mixin/util/AsmUtil.kt +++ b/src/main/kotlin/platform/mixin/util/AsmUtil.kt @@ -20,8 +20,10 @@ package com.demonwav.mcdev.platform.mixin.util +import com.demonwav.mcdev.platform.mixin.reference.MixinSelector import com.demonwav.mcdev.util.MemberMatcher import com.demonwav.mcdev.util.MemberReference +import com.demonwav.mcdev.util.Quantifier import com.demonwav.mcdev.util.anonymousClasses import com.demonwav.mcdev.util.cached import com.demonwav.mcdev.util.childrenOfType @@ -470,12 +472,13 @@ fun ClassNode.findField(ref: MemberMatcher): FieldNode? { return findFields(ref).firstOrNull() } -fun ClassNode.findMethods(ref: MemberMatcher): Sequence { - return methods?.asSequence()?.filter { ref.matchMethod(it, this) } ?: emptySequence() +fun ClassNode.findMethods(ref: MixinSelector): Sequence { + val maxMatches = ref.quantifier.max(Quantifier.Context.MEMBER) + return methods?.asSequence()?.filter { ref.matchMethod(it, this) }?.take(maxMatches).orEmpty() } -fun ClassNode.findMethod(ref: MemberMatcher): MethodNode? { - return findMethods(ref).firstOrNull() +fun ClassNode.findMethod(ref: MemberReference): MethodNode? { + return methods?.asSequence()?.firstOrNull { ref.matchMethod(it, this) } } private fun makeFakeClass(name: String): ClassNode { diff --git a/src/main/kotlin/platform/mixin/util/MemberInfo.kt b/src/main/kotlin/platform/mixin/util/MemberInfo.kt index c7507c9a5..6b5754ced 100644 --- a/src/main/kotlin/platform/mixin/util/MemberInfo.kt +++ b/src/main/kotlin/platform/mixin/util/MemberInfo.kt @@ -32,7 +32,7 @@ data class MemberInfo( val name: String? = null, val descriptor: String? = null, override val owner: String? = null, - val quantifier: Quantifier = Quantifier.Default, + override val quantifier: Quantifier = Quantifier.Default, ) : MixinSelector { init { @@ -92,6 +92,8 @@ data class MemberInfo( } } + override fun withQuantifier(quantifier: Quantifier) = copy(quantifier = quantifier) + companion object { fun parse(input: String): MemberInfo? { var desc: String? = null diff --git a/src/main/kotlin/util/MemberMatcher.kt b/src/main/kotlin/util/MemberMatcher.kt index 39cb336c3..f5e2a765e 100644 --- a/src/main/kotlin/util/MemberMatcher.kt +++ b/src/main/kotlin/util/MemberMatcher.kt @@ -20,20 +20,9 @@ package com.demonwav.mcdev.util -import com.demonwav.mcdev.platform.mixin.util.FieldTargetMember -import com.demonwav.mcdev.platform.mixin.util.MethodTargetMember -import com.demonwav.mcdev.platform.mixin.util.MixinTargetMember -import com.demonwav.mcdev.platform.mixin.util.bytecode -import com.demonwav.mcdev.platform.mixin.util.findField -import com.demonwav.mcdev.platform.mixin.util.findMethod -import com.intellij.openapi.project.Project -import com.intellij.openapi.util.RecursionManager -import com.intellij.psi.CommonClassNames import com.intellij.psi.PsiClass import com.intellij.psi.PsiField -import com.intellij.psi.PsiMember import com.intellij.psi.PsiMethod -import com.intellij.psi.search.GlobalSearchScope import org.objectweb.asm.tree.ClassNode import org.objectweb.asm.tree.FieldNode import org.objectweb.asm.tree.MethodNode @@ -88,64 +77,8 @@ interface MemberMatcher { val fieldDescriptor: String? val qualified get() = owner != null - - fun resolve( - project: Project, - scope: GlobalSearchScope = GlobalSearchScope.allScope(project), - ): Pair? { - return resolve(project, scope, ::Pair) - } - - fun resolveMember(project: Project, scope: GlobalSearchScope = GlobalSearchScope.allScope(project)): PsiMember? { - return resolve(project, scope) { _, member -> member } - } - - fun resolveAsm( - project: Project, - scope: GlobalSearchScope = GlobalSearchScope.allScope(project), - ): MixinTargetMember? { - val owner = this.owner ?: return null - - fun doFind(owner: String): MixinTargetMember? { - if (owner == CommonClassNames.JAVA_LANG_OBJECT) { - return null - } - return RecursionManager.doPreventingRecursion(owner, false) { - val classNode = findQualifiedClass(project, owner, scope)?.bytecode ?: return@doPreventingRecursion null - - classNode.findMethod(this)?.let { - return@doPreventingRecursion MethodTargetMember(classNode, it) - } - - classNode.findField(this)?.let { - return@doPreventingRecursion FieldTargetMember(classNode, it) - } - - classNode.superName?.let { doFind(it.replace('/', '.')) }?.let { return@doPreventingRecursion it } - - classNode.interfaces?.let { interfaces -> - for (itf in interfaces) { - doFind(itf.replace('/', '.'))?.let { return@doPreventingRecursion it } - } - } - - null - } - } - - return doFind(owner) - } - - private inline fun resolve(project: Project, scope: GlobalSearchScope, ret: (PsiClass, PsiMember) -> R): R? { - val owner = this.owner ?: return null - - val psiClass = findQualifiedClass(project, owner, scope) ?: return null - - val field = psiClass.findField(this, checkBases = true) - return if (field != null) { - ret(psiClass, field) - } else { - psiClass.findMethods(this, checkBases = true).firstOrNull()?.let { ret(psiClass, it) } - } - } + val canMatchFields + get() = methodDescriptor == null + val canMatchMethods + get() = fieldDescriptor == null } diff --git a/src/main/kotlin/util/MemberReference.kt b/src/main/kotlin/util/MemberReference.kt index 44ac190d8..5f0ffe475 100644 --- a/src/main/kotlin/util/MemberReference.kt +++ b/src/main/kotlin/util/MemberReference.kt @@ -20,9 +20,20 @@ package com.demonwav.mcdev.util +import com.demonwav.mcdev.platform.mixin.util.FieldTargetMember +import com.demonwav.mcdev.platform.mixin.util.MethodTargetMember +import com.demonwav.mcdev.platform.mixin.util.MixinTargetMember +import com.demonwav.mcdev.platform.mixin.util.bytecode +import com.demonwav.mcdev.platform.mixin.util.findField +import com.demonwav.mcdev.platform.mixin.util.findMethod +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.RecursionManager +import com.intellij.psi.CommonClassNames import com.intellij.psi.PsiClass import com.intellij.psi.PsiField +import com.intellij.psi.PsiMember import com.intellij.psi.PsiMethod +import com.intellij.psi.search.GlobalSearchScope import org.objectweb.asm.Type /** @@ -107,11 +118,64 @@ data class MemberReference( append(descriptor) } } + + fun resolveMember(project: Project, scope: GlobalSearchScope = GlobalSearchScope.allScope(project)): PsiMember? { + return resolve(project, scope) { _, member -> member } + } + + fun resolveAsm( + project: Project, + scope: GlobalSearchScope = GlobalSearchScope.allScope(project), + ): MixinTargetMember? { + val owner = this.owner ?: return null + + fun doFind(owner: String): MixinTargetMember? { + if (owner == CommonClassNames.JAVA_LANG_OBJECT) { + return null + } + return RecursionManager.doPreventingRecursion(owner, false) { + val classNode = findQualifiedClass(project, owner, scope)?.bytecode ?: return@doPreventingRecursion null + + classNode.findMethod(this)?.let { + return@doPreventingRecursion MethodTargetMember(classNode, it) + } + + classNode.findField(this)?.let { + return@doPreventingRecursion FieldTargetMember(classNode, it) + } + + classNode.superName?.let { doFind(it.replace('/', '.')) }?.let { return@doPreventingRecursion it } + + classNode.interfaces?.let { interfaces -> + for (itf in interfaces) { + doFind(itf.replace('/', '.'))?.let { return@doPreventingRecursion it } + } + } + + null + } + } + + return doFind(owner) + } + + private inline fun resolve(project: Project, scope: GlobalSearchScope, ret: (PsiClass, PsiMember) -> R): R? { + val owner = this.owner ?: return null + + val psiClass = findQualifiedClass(project, owner, scope) ?: return null + + val field = psiClass.findField(this, checkBases = true) + return if (field != null) { + ret(psiClass, field) + } else { + psiClass.findMethods(this, checkBases = true).firstOrNull()?.let { ret(psiClass, it) } + } + } } // Class -fun PsiClass.findMethods(member: MemberMatcher, checkBases: Boolean = false): Sequence { +fun PsiClass.findMethods(member: MemberReference, checkBases: Boolean = false): Sequence { val methods = if (checkBases) { allMethods.asSequence() } else { @@ -120,7 +184,7 @@ fun PsiClass.findMethods(member: MemberMatcher, checkBases: Boolean = false): Se return methods.filter { member.matchMethod(it, this) } } -fun PsiClass.findField(selector: MemberMatcher, checkBases: Boolean = false): PsiField? { +fun PsiClass.findField(selector: MemberReference, checkBases: Boolean = false): PsiField? { val fields = if (checkBases) { allFields.toList() } else { diff --git a/src/main/kotlin/util/sequences.kt b/src/main/kotlin/util/sequences.kt index ea109c461..8e576dcb2 100644 --- a/src/main/kotlin/util/sequences.kt +++ b/src/main/kotlin/util/sequences.kt @@ -50,3 +50,21 @@ fun Sequence.memoized(): Sequence { } } } + +val Sequence<*>.countIs get() = SequenceCountCompareOps(this) + +class SequenceCountCompareOps(private val sequence: Sequence<*>) { + operator fun compareTo(size: Int): Int { + if (size < 0) return 1 + if (size == 0) return if (sequence.any()) 1 else 0 + + var count = 0 + sequence.forEach { _ -> + if (++count > size) return 1 + } + + return if (count == size) 0 else -1 + } + + override fun equals(other: Any?) = other is Int && compareTo(other) == 0 +} From dde1e1b2b55bb67377a99720f9cf15033fde8dd9 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Wed, 2 Sep 2026 23:42:01 +0100 Subject: [PATCH 5/8] Fix: Fix incorrect filter stats emptiness check. Causes targets not to show as unresolved when they should. --- .../platform/mixin/reference/target/TargetReference.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/platform/mixin/reference/target/TargetReference.kt b/src/main/kotlin/platform/mixin/reference/target/TargetReference.kt index 451b7762d..6d072d31c 100644 --- a/src/main/kotlin/platform/mixin/reference/target/TargetReference.kt +++ b/src/main/kotlin/platform/mixin/reference/target/TargetReference.kt @@ -103,10 +103,10 @@ object TargetReference : PolyReferenceResolver(), MixinReference { override fun isUnresolved(context: PsiElement): Boolean { val at = context.parentOfType() ?: return true val targets = getTargets(at, true)?.ifEmpty { return true } ?: return false - return targets.all { - val failure = AtResolver(at, it.clazz, it.method).isUnresolved() + return targets.all { target -> + val failure = AtResolver(at, target.clazz, target.method).isUnresolved() // leave it if there is a filter to blame, the target reference was at least resolved - failure != null && failure.filterStats.isEmpty() + failure != null && failure.filterStats.values.none { it > 0 } } } From d1eff9ad5397f7e2d7fbfbe2386204afc3b438b2 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Wed, 2 Sep 2026 23:59:28 +0100 Subject: [PATCH 6/8] New: Handle minimum values in quantifiers. --- .../mixin/handlers/injectionPoint/AtResolver.kt | 14 +++++++++++++- .../mixin/reference/AbstractMethodReference.kt | 8 +++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt index a3b97b028..3e87bcfc6 100644 --- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt @@ -36,9 +36,11 @@ import com.demonwav.mcdev.platform.mixin.util.findSourceClass import com.demonwav.mcdev.platform.mixin.util.findSourceElement import com.demonwav.mcdev.platform.mixin.util.isClinit import com.demonwav.mcdev.platform.mixin.util.memberReference +import com.demonwav.mcdev.util.Quantifier import com.demonwav.mcdev.util.computeStringArray import com.demonwav.mcdev.util.constantStringValue import com.demonwav.mcdev.util.constantValue +import com.demonwav.mcdev.util.countIs import com.demonwav.mcdev.util.descriptor import com.demonwav.mcdev.util.equivalentTo import com.demonwav.mcdev.util.findMethods @@ -197,7 +199,17 @@ class AtResolver( InsnResolutionInfo.Failure() } } - return collectVisitor.visit(targetMethod) as? InsnResolutionInfo.Failure + return when (val result = collectVisitor.visit(targetMethod)) { + is InsnResolutionInfo.Failure -> result + is InsnResolutionInfo.Success -> { + val minMatches = collectVisitor.quantifier.min(Quantifier.Context.INSTRUCTION).coerceAtLeast(1) + if (result.results.countIs >= minMatches) { + null + } else { + InsnResolutionInfo.Failure() + } + } + } } fun resolveInstructions( diff --git a/src/main/kotlin/platform/mixin/reference/AbstractMethodReference.kt b/src/main/kotlin/platform/mixin/reference/AbstractMethodReference.kt index 42770309a..dffe4222a 100644 --- a/src/main/kotlin/platform/mixin/reference/AbstractMethodReference.kt +++ b/src/main/kotlin/platform/mixin/reference/AbstractMethodReference.kt @@ -83,10 +83,12 @@ abstract class AbstractMethodReference : PolyReferenceResolver(), MixinReference val stringValue = context.constantStringValue ?: return false val targetMethodInfo = parseSelector(stringValue, context) ?: return false + val minMatches = targetMethodInfo.quantifier.min(Quantifier.Context.MEMBER).coerceAtLeast(1) val targets = getTargets(context) ?: return false - return !targets.asSequence().flatMap { - targetMethodInfo.getCustomOwner(it).findMethods(targetMethodInfo) - }.any() + + return targets.any { + targetMethodInfo.getCustomOwner(it).findMethods(targetMethodInfo).countIs < minMatches + } } fun getReferenceIfAmbiguous(context: PsiElement): MemberInfo? { From 9d04a574b54feaf0a02a571c019566bf39b32fa9 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Thu, 3 Sep 2026 00:14:29 +0100 Subject: [PATCH 7/8] Fix: Account for individual Mixin `method` strings contributing multiple methods. --- .../platform/mixin/handlers/InjectorAnnotationHandler.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/platform/mixin/handlers/InjectorAnnotationHandler.kt b/src/main/kotlin/platform/mixin/handlers/InjectorAnnotationHandler.kt index 9237e3e27..6f293ce46 100644 --- a/src/main/kotlin/platform/mixin/handlers/InjectorAnnotationHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/InjectorAnnotationHandler.kt @@ -68,9 +68,9 @@ abstract class InjectorAnnotationHandler : MixinAnnotationHandler { (actualTarget to actualTarget.methods) } - return targetClassMethods.mapNotNull { (selector, pair) -> + return targetClassMethods.flatMap { (selector, pair) -> val (clazz, methods) = pair - methods.firstNotNullOfOrNull { method -> + methods.mapNotNull { method -> if (selector.matchMethod(method, clazz)) { MethodTargetMember(clazz, method) } else { From ffdb662e463a8b10b6e33045ce2c7894a11ca512 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Thu, 3 Sep 2026 00:21:20 +0100 Subject: [PATCH 8/8] Fix: Don't report injector signature issues in methods where we find no targets. --- ...nvalidInjectorMethodSignatureInspection.kt | 19 +++++++-------- ...idInjectorMethodSignatureInspectionTest.kt | 23 +++++++++++++++++++ 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt b/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt index dc22e5f53..f0acaed4e 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt @@ -100,18 +100,24 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() { val methodAttribute = annotation.findDeclaredAttributeValue("method") ?: continue val targetMethods = MethodReference.resolve(methodAttribute) ?: continue - val hasDisallowedInsns = targetMethods.any { classAndMethod -> + val matchesByMethod = targetMethods.associateWith { classAndMethod -> handler.resolveInstructions( annotation, classAndMethod.clazz, classAndMethod.method - ).any { !handler.isInsnAllowed(it.insn, it.decorations) } + ) } + val hasDisallowedInsns = matchesByMethod.values.asSequence() + .flatten().any { !handler.isInsnAllowed(it.insn, it.decorations) } if (hasDisallowedInsns) { continue } - for (targetMethod in targetMethods) { + for ((targetMethod, matches) in matchesByMethod) { + if (matches.isEmpty()) { + // We will never inject + continue + } if (!reportedStatic) { var shouldBeStatic = targetMethod.method.hasAccess(Opcodes.ACC_STATIC) @@ -120,12 +126,7 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() { val methodInsns = targetMethod.method.instructions val delegateCtorCall = targetMethod.method.findDelegateConstructorCall() if (methodInsns != null && delegateCtorCall != null) { - val insns = handler.resolveInstructions( - annotation, - targetMethod.clazz, - targetMethod.method, - ) - shouldBeStatic = insns.any { + shouldBeStatic = matches.any { methodInsns.indexOf(it.insn) <= methodInsns.indexOf(delegateCtorCall) } } diff --git a/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt index 39fddcdec..2f210197e 100644 --- a/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt +++ b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt @@ -140,4 +140,27 @@ class InvalidInjectorMethodSignatureInspectionTest : BaseMixinTest() { """, ) } + + @Test + @DisplayName("Wildcard Inject with Single Target") + fun wildcardInInjectWithSingleTarget() { + doTest( + """ + package test; + + import com.demonwav.mcdev.mixintestdata.invalidInjectorMethodSignatureInspection.MixedInOuter; + 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 { + @Inject(method = "*", at = @At(value = "INVOKE", target = "Lcom/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureInspection/MixedInOuter;method2()V")) + private void test(CallbackInfo ci) { + } + } + """, + ) + } }