Skip to content

Commit bde4418

Browse files
committed
Add inspection for wrong mixin class type. Closes #2549
1 parent fae48f9 commit bde4418

File tree

4 files changed

+133
-2
lines changed

4 files changed

+133
-2
lines changed
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
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
22+
23+
import com.demonwav.mcdev.platform.mixin.util.hasAccess
24+
import com.demonwav.mcdev.platform.mixin.util.isAccessorMixin
25+
import com.demonwav.mcdev.platform.mixin.util.isMixin
26+
import com.demonwav.mcdev.platform.mixin.util.mixinTargets
27+
import com.intellij.codeInspection.LocalQuickFix
28+
import com.intellij.codeInspection.LocalQuickFixOnPsiElement
29+
import com.intellij.codeInspection.ProblemsHolder
30+
import com.intellij.openapi.project.Project
31+
import com.intellij.psi.JavaElementVisitor
32+
import com.intellij.psi.JavaPsiFacade
33+
import com.intellij.psi.JavaTokenType
34+
import com.intellij.psi.PsiClass
35+
import com.intellij.psi.PsiElement
36+
import com.intellij.psi.PsiFile
37+
import com.intellij.psi.PsiKeyword
38+
import com.intellij.psi.tree.TokenSet
39+
import org.objectweb.asm.Opcodes
40+
41+
class MixinClassTypeInspection : MixinInspection() {
42+
override fun getStaticDescription() = "Reports when a Mixin uses the wrong class type"
43+
44+
override fun buildVisitor(holder: ProblemsHolder) = object : JavaElementVisitor() {
45+
override fun visitClass(mixinClass: PsiClass) {
46+
if (!mixinClass.isMixin) {
47+
return
48+
}
49+
if (mixinClass.isAccessorMixin) {
50+
return
51+
}
52+
53+
val classKeywordElement = mixinClass.children.firstOrNull {
54+
(it as? PsiKeyword)?.tokenType in CLASS_KEYWORD_SET
55+
}
56+
val problemElement = classKeywordElement ?: mixinClass.nameIdentifier ?: mixinClass
57+
58+
val needsToBeClass = mixinClass.mixinTargets.any { !it.hasAccess(Opcodes.ACC_INTERFACE) }
59+
val needsToBeInterface = mixinClass.mixinTargets.any { it.hasAccess(Opcodes.ACC_INTERFACE) }
60+
61+
val fixes = mutableListOf<LocalQuickFix>()
62+
if (classKeywordElement != null) {
63+
if (needsToBeClass && !needsToBeInterface) {
64+
fixes += ChangeClassTypeFix(classKeywordElement, PsiKeyword.CLASS)
65+
} else if (needsToBeInterface && !needsToBeClass) {
66+
fixes += ChangeClassTypeFix(classKeywordElement, PsiKeyword.INTERFACE)
67+
}
68+
}
69+
70+
if (mixinClass.isEnum) {
71+
holder.registerProblem(problemElement, "Mixins cannot be enums", *fixes.toTypedArray())
72+
return
73+
}
74+
75+
if (mixinClass.isAnnotationType) {
76+
holder.registerProblem(problemElement, "Mixins cannot be annotation types", *fixes.toTypedArray())
77+
return
78+
}
79+
80+
if (mixinClass.isRecord) {
81+
holder.registerProblem(problemElement, "Mixins cannot be records", *fixes.toTypedArray())
82+
return
83+
}
84+
85+
if (mixinClass.isInterface) {
86+
if (needsToBeClass) {
87+
holder.registerProblem(problemElement, "Interface Mixin targets a class", *fixes.toTypedArray())
88+
}
89+
} else {
90+
if (needsToBeInterface) {
91+
holder.registerProblem(problemElement, "Class Mixin targets an interface", *fixes.toTypedArray())
92+
}
93+
}
94+
}
95+
}
96+
97+
companion object {
98+
private val CLASS_KEYWORD_SET = TokenSet.create(
99+
JavaTokenType.CLASS_KEYWORD,
100+
JavaTokenType.INTERFACE_KEYWORD,
101+
JavaTokenType.ENUM_KEYWORD,
102+
JavaTokenType.RECORD_KEYWORD,
103+
)
104+
}
105+
106+
private class ChangeClassTypeFix(
107+
classKeywordElement: PsiElement,
108+
private val newKeyword: String
109+
) : LocalQuickFixOnPsiElement(classKeywordElement) {
110+
override fun getFamilyName() = "Change class type to $newKeyword"
111+
override fun getText() = "Change class type to $newKeyword"
112+
113+
override fun invoke(project: Project, file: PsiFile, startElement: PsiElement, endElement: PsiElement) {
114+
val factory = JavaPsiFacade.getElementFactory(project)
115+
startElement.replace(factory.createKeyword(newKeyword))
116+
}
117+
}
118+
}

src/main/kotlin/platform/mixin/inspection/MixinSuperClassInspection.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ class MixinSuperClassInspection : MixinInspection() {
4646
return
4747
}
4848

49+
if (psiClass.isEnum || psiClass.isAnnotationType || psiClass.isRecord) {
50+
// these will be reported by MixinClassTypeInspection
51+
return
52+
}
53+
4954
val superClass = psiClass.superClass ?: return
5055
if (superClass.qualifiedName == CommonClassNames.JAVA_LANG_OBJECT) {
5156
return
@@ -87,7 +92,7 @@ class MixinSuperClassInspection : MixinInspection() {
8792
}
8893

8994
private fun reportSuperClass(psiClass: PsiClass, description: String) {
90-
holder.registerProblem(psiClass.extendsList?.referenceElements?.firstOrNull() ?: psiClass, description)
95+
holder.registerProblem(psiClass.extendsList?.referenceElements?.firstOrNull() ?: psiClass.nameIdentifier ?: psiClass, description)
9196
}
9297
}
9398
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ val PsiClass.bytecode: ClassNode?
116116
* 1. The class given is a Mixin.
117117
* 2. The class given is an interface.
118118
* 3. All member methods are decorated with either `@Accessor` or `@Invoker`.
119-
* 4. All Mixin targets are classes.
119+
* 4. None of the Mixin targets are interfaces.
120120
*
121121
* @receiver The class to check
122122
* @return True if the above checks are satisfied.

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1102,6 +1102,14 @@
11021102
level="ERROR"
11031103
hasStaticDescription="true"
11041104
implementationClass="com.demonwav.mcdev.platform.mixin.inspection.MixinInnerClassInspection"/>
1105+
<localInspection displayName="Mixin is wrong class type"
1106+
shortName="MixinWrongClassType"
1107+
groupName="Mixin"
1108+
language="JAVA"
1109+
enabledByDefault="true"
1110+
level="ERROR"
1111+
hasStaticDescription="true"
1112+
implementationClass="com.demonwav.mcdev.platform.mixin.inspection.MixinClassTypeInspection"/>
11051113
<localInspection displayName="Reference to @Mixin class"
11061114
shortName="ReferenceToMixin"
11071115
groupName="Mixin"

0 commit comments

Comments
 (0)