User: joe Date: 20 Dec 25 16:38 Revision: bde4418ffe213b400b668c8877c68183b9dae543 Summary: Add inspection for wrong mixin class type. Closes #2549 TeamCity URL: http://ci.mcdev.io:80/viewModification.html?tab=vcsModificationFiles&modId=10364&personal=false Index: src/main/kotlin/platform/mixin/inspection/MixinClassTypeInspection.kt =================================================================== --- src/main/kotlin/platform/mixin/inspection/MixinClassTypeInspection.kt (revision bde4418ffe213b400b668c8877c68183b9dae543) +++ src/main/kotlin/platform/mixin/inspection/MixinClassTypeInspection.kt (revision bde4418ffe213b400b668c8877c68183b9dae543) @@ -0,0 +1,118 @@ +/* + * 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 . + */ + +package com.demonwav.mcdev.platform.mixin.inspection + +import com.demonwav.mcdev.platform.mixin.util.hasAccess +import com.demonwav.mcdev.platform.mixin.util.isAccessorMixin +import com.demonwav.mcdev.platform.mixin.util.isMixin +import com.demonwav.mcdev.platform.mixin.util.mixinTargets +import com.intellij.codeInspection.LocalQuickFix +import com.intellij.codeInspection.LocalQuickFixOnPsiElement +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.openapi.project.Project +import com.intellij.psi.JavaElementVisitor +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.JavaTokenType +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiKeyword +import com.intellij.psi.tree.TokenSet +import org.objectweb.asm.Opcodes + +class MixinClassTypeInspection : MixinInspection() { + override fun getStaticDescription() = "Reports when a Mixin uses the wrong class type" + + override fun buildVisitor(holder: ProblemsHolder) = object : JavaElementVisitor() { + override fun visitClass(mixinClass: PsiClass) { + if (!mixinClass.isMixin) { + return + } + if (mixinClass.isAccessorMixin) { + return + } + + val classKeywordElement = mixinClass.children.firstOrNull { + (it as? PsiKeyword)?.tokenType in CLASS_KEYWORD_SET + } + val problemElement = classKeywordElement ?: mixinClass.nameIdentifier ?: mixinClass + + val needsToBeClass = mixinClass.mixinTargets.any { !it.hasAccess(Opcodes.ACC_INTERFACE) } + val needsToBeInterface = mixinClass.mixinTargets.any { it.hasAccess(Opcodes.ACC_INTERFACE) } + + val fixes = mutableListOf() + if (classKeywordElement != null) { + if (needsToBeClass && !needsToBeInterface) { + fixes += ChangeClassTypeFix(classKeywordElement, PsiKeyword.CLASS) + } else if (needsToBeInterface && !needsToBeClass) { + fixes += ChangeClassTypeFix(classKeywordElement, PsiKeyword.INTERFACE) + } + } + + if (mixinClass.isEnum) { + holder.registerProblem(problemElement, "Mixins cannot be enums", *fixes.toTypedArray()) + return + } + + if (mixinClass.isAnnotationType) { + holder.registerProblem(problemElement, "Mixins cannot be annotation types", *fixes.toTypedArray()) + return + } + + if (mixinClass.isRecord) { + holder.registerProblem(problemElement, "Mixins cannot be records", *fixes.toTypedArray()) + return + } + + if (mixinClass.isInterface) { + if (needsToBeClass) { + holder.registerProblem(problemElement, "Interface Mixin targets a class", *fixes.toTypedArray()) + } + } else { + if (needsToBeInterface) { + holder.registerProblem(problemElement, "Class Mixin targets an interface", *fixes.toTypedArray()) + } + } + } + } + + companion object { + private val CLASS_KEYWORD_SET = TokenSet.create( + JavaTokenType.CLASS_KEYWORD, + JavaTokenType.INTERFACE_KEYWORD, + JavaTokenType.ENUM_KEYWORD, + JavaTokenType.RECORD_KEYWORD, + ) + } + + private class ChangeClassTypeFix( + classKeywordElement: PsiElement, + private val newKeyword: String + ) : LocalQuickFixOnPsiElement(classKeywordElement) { + override fun getFamilyName() = "Change class type to $newKeyword" + override fun getText() = "Change class type to $newKeyword" + + override fun invoke(project: Project, file: PsiFile, startElement: PsiElement, endElement: PsiElement) { + val factory = JavaPsiFacade.getElementFactory(project) + startElement.replace(factory.createKeyword(newKeyword)) + } + } +} Index: src/main/kotlin/platform/mixin/inspection/MixinSuperClassInspection.kt =================================================================== --- src/main/kotlin/platform/mixin/inspection/MixinSuperClassInspection.kt (revision fae48f91c50cead06d12269e8cd2673f41183ef2) +++ src/main/kotlin/platform/mixin/inspection/MixinSuperClassInspection.kt (revision bde4418ffe213b400b668c8877c68183b9dae543) @@ -46,6 +46,11 @@ return } + if (psiClass.isEnum || psiClass.isAnnotationType || psiClass.isRecord) { + // these will be reported by MixinClassTypeInspection + return + } + val superClass = psiClass.superClass ?: return if (superClass.qualifiedName == CommonClassNames.JAVA_LANG_OBJECT) { return @@ -87,7 +92,7 @@ } private fun reportSuperClass(psiClass: PsiClass, description: String) { - holder.registerProblem(psiClass.extendsList?.referenceElements?.firstOrNull() ?: psiClass, description) + holder.registerProblem(psiClass.extendsList?.referenceElements?.firstOrNull() ?: psiClass.nameIdentifier ?: psiClass, description) } } } Index: src/main/kotlin/platform/mixin/util/Mixin.kt =================================================================== --- src/main/kotlin/platform/mixin/util/Mixin.kt (revision fae48f91c50cead06d12269e8cd2673f41183ef2) +++ src/main/kotlin/platform/mixin/util/Mixin.kt (revision bde4418ffe213b400b668c8877c68183b9dae543) @@ -116,7 +116,7 @@ * 1. The class given is a Mixin. * 2. The class given is an interface. * 3. All member methods are decorated with either `@Accessor` or `@Invoker`. - * 4. All Mixin targets are classes. + * 4. None of the Mixin targets are interfaces. * * @receiver The class to check * @return True if the above checks are satisfied. Index: src/main/resources/META-INF/plugin.xml =================================================================== --- src/main/resources/META-INF/plugin.xml (revision fae48f91c50cead06d12269e8cd2673f41183ef2) +++ src/main/resources/META-INF/plugin.xml (revision bde4418ffe213b400b668c8877c68183b9dae543) @@ -1102,6 +1102,14 @@ level="ERROR" hasStaticDescription="true" implementationClass="com.demonwav.mcdev.platform.mixin.inspection.MixinInnerClassInspection"/> +