⁠
llamalad7: Feature/method signature intersection (#2647 )
* Refactor: Rework expected MethodSignatures.
- ParameterGroups are gone, with every signature now effectively containing a required leading group and an optional vararg trailing group. The prior flexibility made working them easy to get wrong, for example in the matching logic which would have been incorrect for leading vararg groups.
- Inject previously contained 2 optional groups (the target's params and the captured locals) but this was in fact incorrect, since the captured params are required if we want to capture any locals, so it is now (better) represented as 2 separate signature options.
- The distinction between `WARN_IF_ABSENT` and `ERROR_IF_ABSENT` is entirely removed. It was effectively unused since it only applied to captured Inject locals, which are varargs and therefore never "absent" (a separate inspection handles unused LocalCapture).
- ModifyArgs is in fact all-or-nothing wrt capturing the target parameters, and this is now reflected.
* Fix: Disallow non-int int-like types in @ModifyConstant.
Not sure why these are there, Mixin doesn't allow them.
* Fix: Disallow `nullValue` in `@ModifyConstant`.
It does not work and cannot ever work.
* stash
* stash
* finish general intersection
* cleanup
* stash
* stash
* fix general intersection
* stash
* fix tests
* return type only suggestions
* prioritise existing return type in modifier suggestions
* restore BeforeConstant's behaviour of ensuring only 1 discriminator is specified, and supporting wildcard matches
* Fix: Support `.class` constants in `@ModifyConstant`.
* Switch `@ModifyConstant` to a general signature type.
* int coercion kinda fixed ish
* fix int coercion properly
* cleanup
* proper int-like logic
* cleanup
* some docs and fixes
* fix return type solving
* Cache walked supertypes and improve handling of Object as a supertype.
* Cleanup
* Cleanup
* Small fixes
* Small fixes
* Fix: Suggest "ci" for coerced callba...
* Refactor: Rework expected MethodSignatures.
- ParameterGroups are gone, with every signature now effectively containing a required leading group and an optional vararg trailing group. The prior flexibility made working them easy to get wrong, for example in the matching logic which would have been incorrect for leading vararg groups.
- Inject previously contained 2 optional groups (the target's params and the captured locals) but this was in fact incorrect, since the captured params are required if we want to capture any locals, so it is now (better) represented as 2 separate signature options.
- The distinction between `WARN_IF_ABSENT` and `ERROR_IF_ABSENT` is entirely removed. It was effectively unused since it only applied to captured Inject locals, which are varargs and therefore never "absent" (a separate inspection handles unused LocalCapture).
- ModifyArgs is in fact all-or-nothing wrt capturing the target parameters, and this is now reflected.
* Fix: Disallow non-int int-like types in @ModifyConstant.
Not sure why these are there, Mixin doesn't allow them.
* Fix: Disallow `nullValue` in `@ModifyConstant`.
It does not work and cannot ever work.
* stash
* stash
* finish general intersection
* cleanup
* stash
* stash
* fix general intersection
* stash
* fix tests
* return type only suggestions
* prioritise existing return type in modifier suggestions
* restore BeforeConstant's behaviour of ensuring only 1 discriminator is specified, and supporting wildcard matches
* Fix: Support `.class` constants in `@ModifyConstant`.
* Switch `@ModifyConstant` to a general signature type.
* int coercion kinda fixed ish
* fix int coercion properly
* cleanup
* proper int-like logic
* cleanup
* some docs and fixes
* fix return type solving
* Cache walked supertypes and improve handling of Object as a supertype.
* Cleanup
* Cleanup
* Small fixes
* Small fixes
* Fix: Suggest "ci" for coerced callba...
- /*
- * 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.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.Quantifier
- import com.demonwav.mcdev.util.constantValue
- import com.intellij.codeInsight.completion.JavaLookupElementBuilder
- import com.intellij.codeInsight.lookup.LookupElementBuilder
- import com.intellij.openapi.editor.Editor
- import com.intellij.openapi.project.Project
- import com.intellij.psi.PsiAnnotation
- import com.intellij.psi.PsiArrayAccessExpression
- import com.intellij.psi.PsiClass
- import com.intellij.psi.PsiField
- import com.intellij.psi.PsiLiteral
- import com.intellij.psi.PsiMethodReferenceExpression
- import com.intellij.psi.PsiModifier
- import com.intellij.psi.PsiReferenceExpression
- import com.intellij.psi.util.PsiUtil
- import com.intellij.util.ArrayUtilRt
- import org.objectweb.asm.Opcodes
- import org.objectweb.asm.Type
- import org.objectweb.asm.tree.AbstractInsnNode
- import org.objectweb.asm.tree.ClassNode
- import org.objectweb.asm.tree.FieldInsnNode
- import org.objectweb.asm.tree.MethodNode
-
- class FieldInjectionPoint : QualifiedInjectionPoint<PsiField>() {
- private object Const {
- val VALID_OPCODES = setOf(Opcodes.GETFIELD, Opcodes.GETSTATIC, Opcodes.PUTFIELD, Opcodes.PUTSTATIC)
- val ARGS_KEYS = arrayOf("array")
- val ARRAY_VALUES = arrayOf("length", "get", "set")
- }
-
- override fun onCompleted(editor: Editor, reference: PsiLiteral) {
- completeExtraStringAtAttribute(editor, reference, "target")
- }
-
- override fun isShiftDiscouraged(shift: Int, at: PsiAnnotation): Boolean {
- // allow shift after the field access
- return shift != 0 && shift != 1
- }
-
- override fun getArgsKeys(at: PsiAnnotation) = Const.ARGS_KEYS
-
- override fun getArgsValues(at: PsiAnnotation, key: String): Array<out Any> =
- Const.ARRAY_VALUES.takeIf { key == "array" } ?: ArrayUtilRt.EMPTY_OBJECT_ARRAY
-
- private fun getArrayAccessType(args: Map<String, String>): ArrayAccessType? {
- return when (args["array"]) {
- "length" -> ArrayAccessType.LENGTH
- "get" -> ArrayAccessType.GET
- "set" -> ArrayAccessType.SET
- else -> null
- }
- }
-
- override val validOpcodes = Const.VALID_OPCODES
-
- override fun createNavigationVisitor(
- at: PsiAnnotation,
- target: MixinSelector?,
- targetClass: PsiClass,
- ): NavigationVisitor? {
- val opcode = (at.findDeclaredAttributeValue("opcode")?.constantValue as? Int)
- ?.takeIf { it in Const.VALID_OPCODES } ?: -1
- val args = AtResolver.getArgs(at)
- val arrayAccess = getArrayAccessType(args)
- return target?.let { MyNavigationVisitor(targetClass, it, opcode, arrayAccess) }
- }
-
- override fun doCreateCollectVisitor(
- at: PsiAnnotation,
- target: MixinSelector?,
- targetClass: ClassNode,
- mode: CollectVisitor.Mode,
- ): CollectVisitor<PsiField>? {
- if (mode == CollectVisitor.Mode.COMPLETION) {
- return MyCollectVisitor(mode, at.project, MemberInfo(), -1, null, 8)
- }
- val opcode = (at.findDeclaredAttributeValue("opcode")?.constantValue as? Int)
- ?.takeIf { it in Const.VALID_OPCODES } ?: -1
- val args = AtResolver.getArgs(at)
- val arrayAccess = getArrayAccessType(args)
- val fuzz = args["fuzz"]?.toIntOrNull()?.coerceIn(1, 32) ?: 8
- return target?.let { MyCollectVisitor(mode, at.project, it, opcode, arrayAccess, fuzz) }
- }
-
- override fun createLookup(targetClass: ClassNode, m: PsiField, insn: AbstractInsnNode): LookupElementBuilder {
- insn as FieldInsnNode
- return JavaLookupElementBuilder.forField(
- m,
- "L${insn.owner};${insn.name}:${insn.desc}",
- null,
- )
- .setBoldIfInClass(m, targetClass)
- .withPresentableText(m.name)
- .withLookupString(m.name)
- }
-
- private class MyNavigationVisitor(
- private val targetClass: PsiClass,
- private val selector: MixinSelector,
- private val opcode: Int,
- private val arrayAccess: ArrayAccessType?,
- ) : NavigationVisitor() {
- override fun visitReferenceExpression(expression: PsiReferenceExpression) {
- if (expression !is PsiMethodReferenceExpression) {
- // early out for if the name does not match
- val name = expression.referenceName
- if (name == null || selector.canEverMatch(name)) {
- (expression.resolve() as? PsiField)?.let { resolved ->
- var matches = selector.matchField(
- resolved,
- QualifiedMember.resolveQualifier(expression) ?: targetClass,
- )
- if (matches && opcode != -1) {
- // check if we match the opcode
- val isStatic = opcode == Opcodes.GETSTATIC || opcode == Opcodes.PUTSTATIC
- if (isStatic != resolved.hasModifierProperty(PsiModifier.STATIC)) {
- matches = false
- } else {
- val isWrite = opcode == Opcodes.PUTFIELD || opcode == Opcodes.PUTSTATIC
- if (isWrite && !PsiUtil.isAccessedForWriting(expression)) {
- matches = false
- } else if (!isWrite && !PsiUtil.isAccessedForReading(expression)) {
- matches = false
- }
- }
- }
- if (matches) {
- // figure out where the array access is.
- // ignore fuzz, I don't even want to think about that in source code
- val actualResult = when (arrayAccess) {
- ArrayAccessType.LENGTH -> {
- val parentRef = PsiUtil.skipParenthesizedExprUp(expression.parent)
- as? PsiReferenceExpression ?: return
- parentRef.takeIf { it.referenceName == "length" }
- }
- ArrayAccessType.GET -> {
- val parentArrayAccess = PsiUtil.skipParenthesizedExprUp(expression.parent)
- as? PsiArrayAccessExpression ?: return
- parentArrayAccess.takeIf(PsiUtil::isAccessedForReading)
- }
- ArrayAccessType.SET -> {
- val parentArrayAccess = PsiUtil.skipParenthesizedExprUp(expression.parent)
- as? PsiArrayAccessExpression ?: return
- parentArrayAccess.takeIf(PsiUtil::isAccessedForWriting)
- }
- null -> expression
- } ?: return
-
- addResult(actualResult)
-
- // if an expression is accessed for reading *and* writing, add it twice to properly handle ordinals
- if (opcode == -1 &&
- PsiUtil.isAccessedForReading(actualResult) &&
- PsiUtil.isAccessedForWriting(actualResult)
- ) {
- addResult(actualResult)
- }
- }
- }
- }
- }
-
- super.visitReferenceExpression(expression)
- }
- }
-
- private class MyCollectVisitor(
- mode: Mode,
- private val project: Project,
- private val selector: MixinSelector,
- private val opcode: Int,
- private val arrayAccess: ArrayAccessType?,
- private val fuzz: Int,
- ) : CollectVisitor<PsiField>(mode) {
- override val quantifier: Quantifier
- get() = selector.quantifier
-
- override fun accept(methodNode: MethodNode) = sequence {
- val insns = methodNode.instructions ?: return@sequence
- for (insn in insns) {
- if (insn !is FieldInsnNode) continue
- if (mode != Mode.COMPLETION) {
- if (opcode != -1 && opcode != insn.opcode) {
- continue
- }
- if (!selector.matchField(insn.owner, insn.name, insn.desc)) {
- continue
- }
- }
- val actualInsn = if (arrayAccess == null) {
- insn
- } else {
- findArrayInsn(insn, arrayAccess)
- } ?: continue
- val fieldNode = insn.fakeResolve()
- val psiField = fieldNode.field.findOrConstructSourceField(
- fieldNode.clazz,
- project,
- canDecompile = false,
- )
- addResult(actualInsn, psiField, qualifier = insn.owner.replace('/', '.'))
- }
- }
-
- private fun findArrayInsn(fieldInsn: FieldInsnNode, arrayAccess: ArrayAccessType): AbstractInsnNode? {
- val arrayType = Type.getType(fieldInsn.desc)
- if (arrayType.sort != Type.ARRAY) {
- return null
- }
- val wantedOpcode = when (arrayAccess) {
- ArrayAccessType.LENGTH -> Opcodes.ARRAYLENGTH
- ArrayAccessType.GET -> arrayType.elementType.getOpcode(Opcodes.IALOAD)
- ArrayAccessType.SET -> arrayType.elementType.getOpcode(Opcodes.IASTORE)
- }
-
- var insn = fieldInsn.next
- var pos = 0
- while (insn != null) {
- if (insn.opcode == wantedOpcode) {
- return insn
- }
- if (insn.opcode == Opcodes.ARRAYLENGTH && pos == 0) {
- return null
- }
- if (insn is FieldInsnNode &&
- insn.owner == fieldInsn.owner &&
- insn.name == fieldInsn.name &&
- insn.desc == fieldInsn.desc
- ) {
- return null
- }
- if (pos > fuzz) {
- return null
- }
- pos++
- insn = insn.next
- }
-
- return null
- }
- }
-
- private enum class ArrayAccessType {
- LENGTH, GET, SET
- }
- }
- /*
- * 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.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.Quantifier
- import com.demonwav.mcdev.util.constantValue
- import com.intellij.codeInsight.completion.JavaLookupElementBuilder
- import com.intellij.codeInsight.lookup.LookupElementBuilder
- import com.intellij.openapi.editor.Editor
- import com.intellij.openapi.project.Project
- import com.intellij.psi.PsiAnnotation
- import com.intellij.psi.PsiArrayAccessExpression
- import com.intellij.psi.PsiClass
- import com.intellij.psi.PsiField
- import com.intellij.psi.PsiLiteral
- import com.intellij.psi.PsiMethodReferenceExpression
- import com.intellij.psi.PsiModifier
- import com.intellij.psi.PsiReferenceExpression
- import com.intellij.psi.util.PsiUtil
- import com.intellij.util.ArrayUtilRt
- import org.objectweb.asm.Opcodes
- import org.objectweb.asm.Type
- import org.objectweb.asm.tree.AbstractInsnNode
- import org.objectweb.asm.tree.ClassNode
- import org.objectweb.asm.tree.FieldInsnNode
- import org.objectweb.asm.tree.MethodNode
-
- class FieldInjectionPoint : QualifiedInjectionPoint<PsiField>() {
- private object Const {
- val VALID_OPCODES = setOf(Opcodes.GETFIELD, Opcodes.GETSTATIC, Opcodes.PUTFIELD, Opcodes.PUTSTATIC)
- val ARGS_KEYS = arrayOf("array")
- val ARRAY_VALUES = arrayOf("length", "get", "set")
- }
-
- override fun onCompleted(editor: Editor, reference: PsiLiteral) {
- completeExtraStringAtAttribute(editor, reference, "target")
- }
-
- override fun isShiftDiscouraged(shift: Int, at: PsiAnnotation): Boolean {
- // allow shift after the field access
- return shift != 0 && shift != 1
- }
-
- override fun getArgsKeys(at: PsiAnnotation) = Const.ARGS_KEYS
-
- override fun getArgsValues(at: PsiAnnotation, key: String): Array<out Any> =
- Const.ARRAY_VALUES.takeIf { key == "array" } ?: ArrayUtilRt.EMPTY_OBJECT_ARRAY
-
- private fun getArrayAccessType(args: Map<String, String>): ArrayAccessType? {
- return when (args["array"]) {
- "length" -> ArrayAccessType.LENGTH
- "get" -> ArrayAccessType.GET
- "set" -> ArrayAccessType.SET
- else -> null
- }
- }
-
- override val validOpcodes = Const.VALID_OPCODES
-
- override fun createNavigationVisitor(
- at: PsiAnnotation,
- target: MixinSelector?,
- targetClass: PsiClass,
- ): NavigationVisitor? {
- val opcode = (at.findDeclaredAttributeValue("opcode")?.constantValue as? Int)
- ?.takeIf { it in Const.VALID_OPCODES } ?: -1
- val args = AtResolver.getArgs(at)
- val arrayAccess = getArrayAccessType(args)
- return target?.let { MyNavigationVisitor(targetClass, it, opcode, arrayAccess) }
- }
-
- override fun doCreateCollectVisitor(
- at: PsiAnnotation,
- target: MixinSelector?,
- targetClass: ClassNode,
- mode: CollectVisitor.Mode,
- ): CollectVisitor<PsiField>? {
- if (!mode.assumeCorrectAt) {
- return MyCollectVisitor(mode, at.project, MemberInfo(), -1, null, 8)
- }
- val opcode = (at.findDeclaredAttributeValue("opcode")?.constantValue as? Int)
- ?.takeIf { it in Const.VALID_OPCODES } ?: -1
- val args = AtResolver.getArgs(at)
- val arrayAccess = getArrayAccessType(args)
- val fuzz = args["fuzz"]?.toIntOrNull()?.coerceIn(1, 32) ?: 8
- return target?.let { MyCollectVisitor(mode, at.project, it, opcode, arrayAccess, fuzz) }
- }
-
- override fun createLookup(targetClass: ClassNode, m: PsiField, insn: AbstractInsnNode): LookupElementBuilder {
- insn as FieldInsnNode
- return JavaLookupElementBuilder.forField(
- m,
- "L${insn.owner};${insn.name}:${insn.desc}",
- null,
- )
- .setBoldIfInClass(m, targetClass)
- .withPresentableText(m.name)
- .withLookupString(m.name)
- }
-
- private class MyNavigationVisitor(
- private val targetClass: PsiClass,
- private val selector: MixinSelector,
- private val opcode: Int,
- private val arrayAccess: ArrayAccessType?,
- ) : NavigationVisitor() {
- override fun visitReferenceExpression(expression: PsiReferenceExpression) {
- if (expression !is PsiMethodReferenceExpression) {
- // early out for if the name does not match
- val name = expression.referenceName
- if (name == null || selector.canEverMatch(name)) {
- (expression.resolve() as? PsiField)?.let { resolved ->
- var matches = selector.matchField(
- resolved,
- QualifiedMember.resolveQualifier(expression) ?: targetClass,
- )
- if (matches && opcode != -1) {
- // check if we match the opcode
- val isStatic = opcode == Opcodes.GETSTATIC || opcode == Opcodes.PUTSTATIC
- if (isStatic != resolved.hasModifierProperty(PsiModifier.STATIC)) {
- matches = false
- } else {
- val isWrite = opcode == Opcodes.PUTFIELD || opcode == Opcodes.PUTSTATIC
- if (isWrite && !PsiUtil.isAccessedForWriting(expression)) {
- matches = false
- } else if (!isWrite && !PsiUtil.isAccessedForReading(expression)) {
- matches = false
- }
- }
- }
- if (matches) {
- // figure out where the array access is.
- // ignore fuzz, I don't even want to think about that in source code
- val actualResult = when (arrayAccess) {
- ArrayAccessType.LENGTH -> {
- val parentRef = PsiUtil.skipParenthesizedExprUp(expression.parent)
- as? PsiReferenceExpression ?: return
- parentRef.takeIf { it.referenceName == "length" }
- }
- ArrayAccessType.GET -> {
- val parentArrayAccess = PsiUtil.skipParenthesizedExprUp(expression.parent)
- as? PsiArrayAccessExpression ?: return
- parentArrayAccess.takeIf(PsiUtil::isAccessedForReading)
- }
- ArrayAccessType.SET -> {
- val parentArrayAccess = PsiUtil.skipParenthesizedExprUp(expression.parent)
- as? PsiArrayAccessExpression ?: return
- parentArrayAccess.takeIf(PsiUtil::isAccessedForWriting)
- }
- null -> expression
- } ?: return
-
- addResult(actualResult)
-
- // if an expression is accessed for reading *and* writing, add it twice to properly handle ordinals
- if (opcode == -1 &&
- PsiUtil.isAccessedForReading(actualResult) &&
- PsiUtil.isAccessedForWriting(actualResult)
- ) {
- addResult(actualResult)
- }
- }
- }
- }
- }
-
- super.visitReferenceExpression(expression)
- }
- }
-
- private class MyCollectVisitor(
- mode: Mode,
- private val project: Project,
- private val selector: MixinSelector,
- private val opcode: Int,
- private val arrayAccess: ArrayAccessType?,
- private val fuzz: Int,
- ) : CollectVisitor<PsiField>(mode) {
- override val quantifier: Quantifier
- get() = selector.quantifier
-
- override fun accept(methodNode: MethodNode) = sequence {
- val insns = methodNode.instructions ?: return@sequence
- for (insn in insns) {
- if (insn !is FieldInsnNode) continue
- if (mode.assumeCorrectAt) {
- if (opcode != -1 && opcode != insn.opcode) {
- continue
- }
- if (!selector.matchField(insn.owner, insn.name, insn.desc)) {
- continue
- }
- }
- val actualInsn = if (arrayAccess == null) {
- insn
- } else {
- findArrayInsn(insn, arrayAccess)
- } ?: continue
- val fieldNode = insn.fakeResolve()
- val psiField = fieldNode.field.findOrConstructSourceField(
- fieldNode.clazz,
- project,
- canDecompile = false,
- )
- addResult(actualInsn, psiField, qualifier = insn.owner.replace('/', '.'))
- }
- }
-
- private fun findArrayInsn(fieldInsn: FieldInsnNode, arrayAccess: ArrayAccessType): AbstractInsnNode? {
- val arrayType = Type.getType(fieldInsn.desc)
- if (arrayType.sort != Type.ARRAY) {
- return null
- }
- val wantedOpcode = when (arrayAccess) {
- ArrayAccessType.LENGTH -> Opcodes.ARRAYLENGTH
- ArrayAccessType.GET -> arrayType.elementType.getOpcode(Opcodes.IALOAD)
- ArrayAccessType.SET -> arrayType.elementType.getOpcode(Opcodes.IASTORE)
- }
-
- var insn = fieldInsn.next
- var pos = 0
- while (insn != null) {
- if (insn.opcode == wantedOpcode) {
- return insn
- }
- if (insn.opcode == Opcodes.ARRAYLENGTH && pos == 0) {
- return null
- }
- if (insn is FieldInsnNode &&
- insn.owner == fieldInsn.owner &&
- insn.name == fieldInsn.name &&
- insn.desc == fieldInsn.desc
- ) {
- return null
- }
- if (pos > fuzz) {
- return null
- }
- pos++
- insn = insn.next
- }
-
- return null
- }
- }
-
- private enum class ArrayAccessType {
- LENGTH, GET, SET
- }
- }