Skip to content

Commit 3ef92c6

Browse files
committed
Add inspection to replace INVOKE_ASSIGN with expressions where possible
1 parent 8019549 commit 3ef92c6

File tree

3 files changed

+220
-0
lines changed

3 files changed

+220
-0
lines changed
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
/*
2+
* Minecraft Development for IntelliJ
3+
*
4+
* https://mcdev.io/
5+
*
6+
* Copyright (C) 2025 minecraft-dev
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the GNU Lesser General Public License as published
10+
* by the Free Software Foundation, version 3.0 only.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU Lesser General Public License
18+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
19+
*/
20+
21+
package com.demonwav.mcdev.platform.mixin.inspection.mixinextras
22+
23+
import com.demonwav.mcdev.MinecraftProjectSettings
24+
import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler
25+
import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.AtResolver
26+
import com.demonwav.mcdev.platform.mixin.inspection.MixinInspection
27+
import com.demonwav.mcdev.platform.mixin.reference.MixinSelector
28+
import com.demonwav.mcdev.platform.mixin.reference.parseMixinSelector
29+
import com.demonwav.mcdev.platform.mixin.util.MethodTargetMember
30+
import com.demonwav.mcdev.platform.mixin.util.MixinConstants
31+
import com.demonwav.mcdev.platform.mixin.util.nextRealInsn
32+
import com.demonwav.mcdev.util.BeforeOrAfter
33+
import com.demonwav.mcdev.util.constantStringValue
34+
import com.demonwav.mcdev.util.toJavaIdentifier
35+
import com.intellij.codeInspection.LocalQuickFixOnPsiElement
36+
import com.intellij.codeInspection.ProblemsHolder
37+
import com.intellij.openapi.project.Project
38+
import com.intellij.psi.JavaElementVisitor
39+
import com.intellij.psi.JavaPsiFacade
40+
import com.intellij.psi.PsiAnnotation
41+
import com.intellij.psi.PsiElement
42+
import com.intellij.psi.PsiElementVisitor
43+
import com.intellij.psi.PsiFile
44+
import com.intellij.psi.PsiModifierList
45+
import com.intellij.psi.codeStyle.JavaCodeStyleManager
46+
import org.objectweb.asm.Opcodes
47+
import org.objectweb.asm.Type
48+
import org.objectweb.asm.tree.MethodInsnNode
49+
import org.objectweb.asm.tree.VarInsnNode
50+
51+
class InvokeAssignReplaceWithExpressionInspection : MixinInspection() {
52+
override fun getStaticDescription() = "Reports when INVOKE_ASSIGN could be replaced with a MixinExtras expression. " +
53+
"Expressions are preferred over INVOKE_ASSIGN because they fail when an assignment doesn't exist."
54+
55+
override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor {
56+
val hasExpressions = JavaPsiFacade.getInstance(holder.project).findClass(
57+
MixinConstants.MixinExtras.EXPRESSION,
58+
holder.file.resolveScope
59+
) != null
60+
if (!hasExpressions) {
61+
return PsiElementVisitor.EMPTY_VISITOR
62+
}
63+
return object : JavaElementVisitor() {
64+
override fun visitAnnotation(annotation: PsiAnnotation) {
65+
if (!annotation.hasQualifiedName(MixinConstants.Annotations.AT)) {
66+
return
67+
}
68+
val atValue = annotation.findDeclaredAttributeValue("value") ?: return
69+
if (atValue.constantStringValue != "INVOKE_ASSIGN") {
70+
return
71+
}
72+
val atTarget = annotation.findDeclaredAttributeValue("target")
73+
if (atTarget == null) {
74+
return
75+
}
76+
val target = parseMixinSelector(atTarget) ?: return
77+
val methodInsn = resolveMethodInsn(annotation, target) ?: return
78+
val customArgs = AtResolver.getArgs(annotation)
79+
if (customArgs.containsKey("fuzz") || customArgs.containsKey("skip")) {
80+
return
81+
}
82+
83+
holder.registerProblem(
84+
atValue,
85+
"INVOKE_ASSIGN could be replaced with expression",
86+
ReplaceWithExpressionFix(
87+
annotation,
88+
methodInsn.name.toJavaIdentifier(),
89+
methodInsn.opcode == Opcodes.INVOKESTATIC,
90+
Type.getArgumentCount(methodInsn.desc)
91+
)
92+
)
93+
}
94+
}
95+
}
96+
97+
private fun resolveMethodInsn(at: PsiAnnotation, target: MixinSelector): MethodInsnNode? {
98+
val injectorAnnotation = AtResolver.findInjectorAnnotation(at) ?: return null
99+
val insns = MixinAnnotationHandler.resolveTarget(injectorAnnotation)
100+
.flatMap { targetMember ->
101+
if (targetMember !is MethodTargetMember) {
102+
return@flatMap emptyList()
103+
}
104+
val instructions = targetMember.classAndMethod.method.instructions ?: return@flatMap emptyList()
105+
instructions.asSequence()
106+
.filterIsInstance<MethodInsnNode>()
107+
.filter { target.matchMethod(it.owner, it.name, it.desc) }
108+
.asIterable()
109+
}
110+
if (insns.isEmpty()) {
111+
return null
112+
}
113+
for (insn in insns) {
114+
val assignmentInsn = insn.nextRealInsn as? VarInsnNode ?: return null
115+
if (assignmentInsn.opcode < Opcodes.ISTORE) {
116+
// it's a load insn
117+
return null
118+
}
119+
}
120+
121+
val result = insns.first()
122+
for (insn in insns) {
123+
if (insn.opcode != result.opcode || insn.owner != result.owner || insn.name != result.name || insn.desc != result.desc) {
124+
return null
125+
}
126+
}
127+
128+
return result
129+
}
130+
131+
private class ReplaceWithExpressionFix(
132+
at: PsiAnnotation,
133+
private val definitionId: String,
134+
private val isStatic: Boolean,
135+
private val argCount: Int,
136+
) : LocalQuickFixOnPsiElement(at) {
137+
override fun getFamilyName() = "Replace with expression"
138+
override fun getText() = "Replace with expression"
139+
140+
override fun invoke(project: Project, file: PsiFile, startElement: PsiElement, endElement: PsiElement) {
141+
val at = startElement as? PsiAnnotation ?: return
142+
val modifierList = AtResolver.findInjectorAnnotation(at)?.parent as? PsiModifierList ?: return
143+
val atTarget = at.findDeclaredAttributeValue("target") ?: return
144+
145+
val factory = JavaPsiFacade.getElementFactory(project)
146+
147+
val definition = factory.createAnnotationFromText(
148+
"@${MixinConstants.MixinExtras.DEFINITION}(id = \"$definitionId\")",
149+
at
150+
)
151+
definition.setDeclaredAttributeValue("method", atTarget)
152+
153+
val exprText = buildString {
154+
append("? = ")
155+
if (!isStatic) {
156+
append("?.")
157+
}
158+
append(definitionId)
159+
append("(")
160+
if (argCount > 0) {
161+
append("?")
162+
repeat(argCount - 1) {
163+
append(", ?")
164+
}
165+
}
166+
append(")")
167+
}
168+
val expression = factory.createAnnotationFromText(
169+
"@${MixinConstants.MixinExtras.EXPRESSION}(\"$exprText\")",
170+
at
171+
)
172+
173+
val definitionPosRelativeToExpression =
174+
MinecraftProjectSettings.getInstance(project).definitionPosRelativeToExpression
175+
if (definitionPosRelativeToExpression == BeforeOrAfter.BEFORE) {
176+
modifierList.addAfter(expression, null)
177+
modifierList.addAfter(definition, null)
178+
} else {
179+
modifierList.addAfter(definition, null)
180+
modifierList.addAfter(expression, null)
181+
}
182+
183+
val newAt = factory.createAnnotationFromText(
184+
"@${MixinConstants.Annotations.AT}(value = \"MIXINEXTRAS:EXPRESSION\", shift = ${MixinConstants.Classes.SHIFT}.AFTER)",
185+
at
186+
)
187+
at.replace(newAt)
188+
189+
JavaCodeStyleManager.getInstance(project).shortenClassReferences(modifierList)
190+
}
191+
}
192+
}

src/main/kotlin/platform/mixin/util/AsmUtil.kt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1100,6 +1100,26 @@ fun MethodInsnNode.fakeResolve(): ClassAndMethodNode {
11001100
return ClassAndMethodNode(clazz, method)
11011101
}
11021102

1103+
// AbstractInsnNode
1104+
1105+
val AbstractInsnNode.nextRealInsn: AbstractInsnNode?
1106+
get() {
1107+
var insn = next
1108+
while (insn != null && insn.opcode < 0) {
1109+
insn = insn.next
1110+
}
1111+
return insn
1112+
}
1113+
1114+
val AbstractInsnNode.previousRealInsn: AbstractInsnNode?
1115+
get() {
1116+
var insn = previous
1117+
while (insn != null && insn.opcode < 0) {
1118+
insn = insn.previous
1119+
}
1120+
return insn
1121+
}
1122+
11031123
// Textifier
11041124

11051125
fun ClassNode.textify(): String {

src/main/resources/META-INF/plugin.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1293,6 +1293,14 @@
12931293
level="WARNING"
12941294
hasStaticDescription="true"
12951295
implementationClass="com.demonwav.mcdev.platform.mixin.inspection.mixinextras.InjectLocalCaptureReplaceWithLocalInspection"/>
1296+
<localInspection displayName="INVOKE_ASSIGN can be replaced with expression"
1297+
shortName="InvokeAssignCanReplacedWithExpression"
1298+
groupName="Mixin"
1299+
language="JAVA"
1300+
enabledByDefault="true"
1301+
level="WARNING"
1302+
hasStaticDescription="true"
1303+
implementationClass="com.demonwav.mcdev.platform.mixin.inspection.mixinextras.InvokeAssignReplaceWithExpressionInspection"/>
12961304
<!--endregion-->
12971305

12981306
<!--region Overwrite Inspections -->

0 commit comments

Comments
 (0)