⁠
llamalad7: Feature/mixin quantifiers (#2643 )
* Refactor: Split MemberInfo and MemberReference.
Mixin MemberInfo strings are only tangentially related to concrete MemberReferences. We also introduce a MemberMatcher superinterface for resolving things.
* Refactor: Parse MemberInfos properly and represent Quantifiers.
* 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.
* New: Handle maximum values in quantifiers.
* Fix: Fix incorrect filter stats emptiness check.
Causes targets not to show as unresolved when they should.
* New: Handle minimum values in quantifiers.
* Fix: Account for individual Mixin `method` strings contributing multiple methods.
* Fix: Don't report injector signature issues in methods where we find no targets.
* Move insn resolution failure message to field in failure class, and add message for min quantifier not satisfied
---------
Co-authored-by: joe <[email protected]>
* Refactor: Split MemberInfo and MemberReference.
Mixin MemberInfo strings are only tangentially related to concrete MemberReferences. We also introduce a MemberMatcher superinterface for resolving things.
* Refactor: Parse MemberInfos properly and represent Quantifiers.
* 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.
* New: Handle maximum values in quantifiers.
* Fix: Fix incorrect filter stats emptiness check.
Causes targets not to show as unresolved when they should.
* New: Handle minimum values in quantifiers.
* Fix: Account for individual Mixin `method` strings contributing multiple methods.
* Fix: Don't report injector signature issues in methods where we find no targets.
* Move insn resolution failure message to field in failure class, and add message for min quantifier not satisfied
---------
Co-authored-by: joe <[email protected]>
- /*
- * Minecraft Development for IntelliJ
- *
- * https://mcdev.io/
- *
- * Copyright (C) 2025 minecraft-dev
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published
- * by the Free Software Foundation, version 3.0 only.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-
- 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.
- */
- 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 {
-
- 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("(") }
- override val displayName = name
-
- 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)
- }
-
- 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
- }
- }
-
- 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 != "<init>" && name != "<clinit>") {
- return null
- }
-
- return MemberReference(if (matchAllNames) "*" else name, descriptor, owner, matchAllNames, matchAllDescs)
- }
- }
- }
-
- // Class
-
- fun PsiClass.findMethods(member: MixinSelector, checkBases: Boolean = false): Sequence<PsiMethod> {
- val methods = if (checkBases) {
- allMethods.asSequence()
- } else {
- methods.asSequence()
- } + constructors
- return methods.filter { member.matchMethod(it, this) }
- }
-
- fun PsiClass.findField(selector: MixinSelector, checkBases: Boolean = false): PsiField? {
- val fields = if (checkBases) {
- allFields.toList()
- } else {
- fields.toList()
- }
- return fields.firstOrNull { selector.matchField(it, this) }
- }
-
- // Method
-
- val PsiMethod.memberReference
- get() = MemberReference(internalName, descriptor)
-
- val PsiMethod.qualifiedMemberReference
- get() = MemberReference(internalName, descriptor, containingClass?.fullQualifiedName)
-
- fun PsiMethod.getQualifiedMemberReference(owner: PsiClass): MemberReference {
- return getQualifiedMemberReference(owner.fullQualifiedName)
- }
-
- fun PsiMethod.getQualifiedMemberReference(owner: String?): MemberReference {
- return MemberReference(internalName, descriptor, owner)
- }
-
- fun PsiMethod?.isSameReference(reference: PsiMethod?): Boolean =
- this != null && (this === reference || qualifiedMemberReference == reference?.qualifiedMemberReference)
-
- // Field
- val PsiField.simpleMemberReference
- get() = MemberReference(name)
-
- val PsiField.memberReference
- get() = MemberReference(name, descriptor)
-
- val PsiField.simpleQualifiedMemberReference
- get() = MemberReference(name, null, containingClass!!.fullQualifiedName)
-
- val PsiField.qualifiedMemberReference
- get() = MemberReference(name, descriptor, containingClass!!.fullQualifiedName)
-
- fun PsiField.getQualifiedMemberReference(owner: PsiClass): MemberReference {
- return getQualifiedMemberReference(owner.fullQualifiedName)
- }
-
- fun PsiField.getQualifiedMemberReference(owner: String?): MemberReference {
- return MemberReference(name, descriptor, owner)
- }
- /*
- * 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 <https://www.gnu.org/licenses/>.
- */
-
- 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
-
- /**
- * 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,
- ) : MemberMatcher {
-
- 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 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.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.name == name &&
- matchOwner(owner) &&
- (this.descriptor == null || this.descriptor == desc)
- }
-
- fun toMixinString() = buildString {
- if (owner != null) {
- append('L').append(owner.replace('.', '/')).append(';')
- }
-
- append(name)
-
- descriptor?.let { descriptor ->
- if (!descriptor.startsWith('(')) {
- // Field descriptor
- append(':')
- }
-
- 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 <R> 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: MemberReference, checkBases: Boolean = false): Sequence<PsiMethod> {
- val methods = if (checkBases) {
- allMethods.asSequence()
- } else {
- methods.asSequence()
- } + constructors
- return methods.filter { member.matchMethod(it, this) }
- }
-
- fun PsiClass.findField(selector: MemberReference, checkBases: Boolean = false): PsiField? {
- val fields = if (checkBases) {
- allFields.toList()
- } else {
- fields.toList()
- }
- return fields.firstOrNull { selector.matchField(it, this) }
- }
-
- // Method
-
- val PsiMethod.memberReference
- get() = MemberReference(internalName, descriptor)
-
- val PsiMethod.qualifiedMemberReference
- get() = MemberReference(internalName, descriptor, containingClass?.fullQualifiedName)
-
- fun PsiMethod.getQualifiedMemberReference(owner: PsiClass): MemberReference {
- return getQualifiedMemberReference(owner.fullQualifiedName)
- }
-
- fun PsiMethod.getQualifiedMemberReference(owner: String?): MemberReference {
- return MemberReference(internalName, descriptor, owner)
- }
-
- fun PsiMethod?.isSameReference(reference: PsiMethod?): Boolean =
- this != null && (this === reference || qualifiedMemberReference == reference?.qualifiedMemberReference)
-
- // Field
- val PsiField.simpleMemberReference
- get() = MemberReference(name)
-
- val PsiField.memberReference
- get() = MemberReference(name, descriptor)
-
- val PsiField.simpleQualifiedMemberReference
- get() = MemberReference(name, null, containingClass!!.fullQualifiedName)
-
- val PsiField.qualifiedMemberReference
- get() = MemberReference(name, descriptor, containingClass!!.fullQualifiedName)
-
- fun PsiField.getQualifiedMemberReference(owner: PsiClass): MemberReference {
- return getQualifiedMemberReference(owner.fullQualifiedName)
- }
-
- fun PsiField.getQualifiedMemberReference(owner: String?): MemberReference {
- return MemberReference(name, descriptor, owner)
- }