User: joe
Date: 24 Jun 23 04:11
Revision: 0da34782e5c29f3e4f13ec0cefdc9bca4e1e8baa
Summary:
Add inspection for names of members that are added to classes via a mixin, so that they don't clash with other mods
TeamCity URL: https://ci.mcdev.io/viewModification.html?tab=vcsModificationFiles&modId=8549&personal=false
Index: src/main/kotlin/platform/mixin/inspection/addedMembers/AbstractAddedMembersInspection.kt
===================================================================
--- src/main/kotlin/platform/mixin/inspection/addedMembers/AbstractAddedMembersInspection.kt (revision 0da34782e5c29f3e4f13ec0cefdc9bca4e1e8baa)
+++ src/main/kotlin/platform/mixin/inspection/addedMembers/AbstractAddedMembersInspection.kt (revision 0da34782e5c29f3e4f13ec0cefdc9bca4e1e8baa)
@@ -0,0 +1,88 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2023 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.addedMembers
+
+import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler
+import com.demonwav.mcdev.platform.mixin.inspection.MixinInspection
+import com.demonwav.mcdev.platform.mixin.util.MixinConstants
+import com.demonwav.mcdev.platform.mixin.util.isMixin
+import com.intellij.codeInspection.ProblemsHolder
+import com.intellij.psi.JavaElementVisitor
+import com.intellij.psi.PsiElementVisitor
+import com.intellij.psi.PsiField
+import com.intellij.psi.PsiMethod
+import com.intellij.psi.search.searches.SuperMethodsSearch
+
+abstract class AbstractAddedMembersInspection : MixinInspection() {
+ abstract fun visitAddedField(holder: ProblemsHolder, field: PsiField)
+ abstract fun visitAddedMethod(holder: ProblemsHolder, method: PsiMethod, isInherited: Boolean)
+
+ final override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor = Visitor(holder)
+
+ private inner class Visitor(private val holder: ProblemsHolder) : JavaElementVisitor() {
+ override fun visitField(field: PsiField) {
+ if (field.containingClass?.isMixin != true) {
+ return
+ }
+
+ if (field.hasAnnotation(MixinConstants.Annotations.SHADOW)) {
+ return
+ }
+
+ visitAddedField(holder, field)
+ }
+
+ override fun visitMethod(method: PsiMethod) {
+ if (method.containingClass?.isMixin != true) {
+ return
+ }
+
+ if (method.isConstructor) {
+ return
+ }
+
+ val hasMixinAnnotation = method.annotations.any {
+ val fqn = it.qualifiedName ?: return@any false
+ fqn in ignoredMethodAnnotations || MixinAnnotationHandler.forMixinAnnotation(
+ fqn,
+ holder.project
+ ) != null
+ }
+ if (hasMixinAnnotation) {
+ return
+ }
+
+ val superMethod = SuperMethodsSearch.search(method, null, true, false).findFirst()
+ visitAddedMethod(holder, method, superMethod != null)
+ }
+ }
+
+ companion object {
+ private val ignoredMethodAnnotations = setOf(
+ MixinConstants.Annotations.SHADOW,
+ MixinConstants.Annotations.ACCESSOR,
+ MixinConstants.Annotations.INVOKER,
+ MixinConstants.Annotations.OVERWRITE,
+ MixinConstants.Annotations.INTRINSIC,
+ MixinConstants.Annotations.SOFT_OVERRIDE,
+ )
+ }
+}
Index: src/main/kotlin/platform/mixin/inspection/addedMembers/AddedMembersNameFormatInspection.kt
===================================================================
--- src/main/kotlin/platform/mixin/inspection/addedMembers/AddedMembersNameFormatInspection.kt (revision 0da34782e5c29f3e4f13ec0cefdc9bca4e1e8baa)
+++ src/main/kotlin/platform/mixin/inspection/addedMembers/AddedMembersNameFormatInspection.kt (revision 0da34782e5c29f3e4f13ec0cefdc9bca4e1e8baa)
@@ -0,0 +1,295 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2023 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.addedMembers
+
+import com.demonwav.mcdev.util.decapitalize
+import com.demonwav.mcdev.util.findContainingClass
+import com.demonwav.mcdev.util.onShown
+import com.demonwav.mcdev.util.toJavaIdentifier
+import com.intellij.codeInsight.CodeInsightBundle
+import com.intellij.codeInsight.FileModificationService
+import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement
+import com.intellij.codeInspection.ProblemsHolder
+import com.intellij.ide.util.SuperMethodWarningUtil
+import com.intellij.openapi.editor.Editor
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.ui.ComponentValidator
+import com.intellij.openapi.ui.DialogWrapper
+import com.intellij.openapi.util.text.StringUtil
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiField
+import com.intellij.psi.PsiFile
+import com.intellij.psi.PsiMethod
+import com.intellij.psi.PsiNameIdentifierOwner
+import com.intellij.psi.PsiNamedElement
+import com.intellij.refactoring.rename.RenameProcessor
+import com.intellij.ui.DocumentAdapter
+import com.intellij.ui.components.JBCheckBox
+import com.intellij.ui.components.JBTextField
+import com.intellij.ui.dsl.builder.COLUMNS_SHORT
+import com.intellij.ui.dsl.builder.Cell
+import com.intellij.ui.dsl.builder.columns
+import com.intellij.ui.dsl.builder.panel
+import com.intellij.ui.layout.ValidationInfoBuilder
+import com.intellij.util.xmlb.Converter
+import com.intellij.util.xmlb.annotations.Attribute
+import java.util.function.Supplier
+import java.util.regex.Pattern
+import java.util.regex.PatternSyntaxException
+import javax.swing.JComponent
+import javax.swing.event.DocumentEvent
+import kotlin.reflect.KMutableProperty0
+import org.intellij.lang.annotations.Language
+
+class AddedMembersNameFormatInspection : AbstractAddedMembersInspection() {
+ @Attribute(converter = RegexConverter::class)
+ @JvmField
+ var validNameFormat = ".+[_$].+".toRegex()
+
+ @Attribute(converter = RegexConverter::class)
+ @JvmField
+ var validNameFixSearch = "^.+$".toRegex()
+
+ @JvmField
+ var validNameFixReplace = "MOD_ID\\\$\$0"
+
+ @JvmField
+ var ignoreFields = false
+ @JvmField
+ var ignoreMethods = false
+ @JvmField
+ var ignoreInheritedInterfaceMethods = false
+
+ override fun getStaticDescription() = "Reports added members not matching the correct name format"
+
+ override fun visitAddedField(holder: ProblemsHolder, field: PsiField) {
+ if (!ignoreFields) {
+ visitAdded(holder, field)
+ }
+ }
+
+ override fun visitAddedMethod(holder: ProblemsHolder, method: PsiMethod, isInherited: Boolean) {
+ if (!shouldIgnoreMethod(method, isInherited)) {
+ visitAdded(holder, method)
+ }
+ }
+
+ private fun shouldIgnoreMethod(method: PsiMethod, isInherited: Boolean): Boolean {
+ if (ignoreMethods) {
+ return true
+ }
+
+ if (isInherited) {
+ if (ignoreInheritedInterfaceMethods) {
+ return true
+ }
+
+ val superMethods = method.findDeepestSuperMethods()
+ val isInterfaceMethod = superMethods.any {
+ val clazz = it.findContainingClass() ?: return@any false
+ clazz.isInterface && clazz.containingFile?.isWritable == true
+ }
+ return !isInterfaceMethod
+ }
+
+ return false
+ }
+
+ private fun visitAdded(holder: ProblemsHolder, added: PsiNameIdentifierOwner) {
+ val name = added.name ?: return
+ if (validNameFormat.matches(name)) {
+ return
+ }
+
+ // try to get a quick fix
+ val fixed = try {
+ validNameFixSearch.replace(name, validNameFixReplace)
+ .replace("MOD_ID", getAppropriatePrefix(holder.project))
+ } catch (e: RuntimeException) {
+ null
+ }
+
+ if (fixed != null && StringUtil.isJavaIdentifier(fixed) && validNameFormat.matches(fixed)) {
+ holder.registerProblem(
+ added.nameIdentifier ?: return,
+ "Name does not match the pattern for added mixin members: \"${validNameFormat.pattern}\"",
+ RenameWithInheritanceFix(added, fixed)
+ )
+ } else {
+ holder.registerProblem(
+ added.nameIdentifier ?: return,
+ "Name does not match the pattern for added mixin members: \"${validNameFormat.pattern}\"",
+ )
+ }
+ }
+
+ private fun getAppropriatePrefix(project: Project): String {
+ return StringUtil.capitalizeWords(project.name, true)
+ .decapitalize()
+ .replace(" ", "")
+ .toJavaIdentifier(allowDollars = false)
+ }
+
+ override fun createOptionsPanel(): JComponent {
+ return panel {
+ row("Valid name format:") {
+ textField()
+ .doBindText({ validNameFormat.pattern }, { validNameFormat = it.toRegexOrDefault(".+[_$].+") })
+ .columns(COLUMNS_SHORT)
+ .regexValidator()
+ }
+ row("Valid name fix search:") {
+ textField()
+ .doBindText({ validNameFixSearch.pattern }, { validNameFixSearch = it.toRegexOrDefault(".+") })
+ .columns(COLUMNS_SHORT)
+ .regexValidator()
+ }
+ row("Valid name fix replace:") {
+ textField().doBindText(::validNameFixReplace).columns(COLUMNS_SHORT)
+ }
+
+ separator()
+
+ var ignoreFields: Cell? = null
+ row {
+ ignoreFields = checkBox("Ignore fields").doBindSelected(::ignoreFields)
+ }
+ var ignoreMethods: Cell? = null
+ row {
+ ignoreMethods = checkBox("Ignore methods").doBindSelected(::ignoreMethods)
+ }
+ // make sure ignore fields and ignore methods can't be selected at the same time
+ ignoreFields!!.component.addActionListener {
+ if (ignoreFields!!.component.isSelected) {
+ ignoreMethods!!.component.isSelected = false
+ }
+ }
+ ignoreMethods!!.component.addActionListener {
+ if (ignoreMethods!!.component.isSelected) {
+ ignoreFields!!.component.isSelected = false
+ }
+ }
+
+ row {
+ checkBox("Ignore inherited interface methods").doBindSelected(::ignoreInheritedInterfaceMethods)
+ }
+ }
+ }
+}
+
+private fun String.toRegexOrDefault(@Language("RegExp") default: String): Regex {
+ return try {
+ this.toRegex()
+ } catch (e: PatternSyntaxException) {
+ default.toRegex()
+ }
+}
+
+private fun Cell.doBindText(property: KMutableProperty0): Cell {
+ return doBindText(property.getter, property.setter)
+}
+
+private fun Cell.doBindText(getter: () -> String, setter: (String) -> Unit): Cell {
+ component.text = getter()
+ component.document.addDocumentListener(object : DocumentAdapter() {
+ override fun textChanged(e: DocumentEvent) {
+ setter(component.text)
+ }
+ })
+ return this
+}
+
+private fun Cell.doBindSelected(property: KMutableProperty0): Cell {
+ component.isSelected = property.get()
+ component.addActionListener {
+ property.set(component.isSelected)
+ }
+ return this
+}
+
+private fun Cell.regexValidator(): Cell {
+ var hasRegisteredValidator = false
+ component.onShown {
+ if (!hasRegisteredValidator) {
+ hasRegisteredValidator = true
+ val disposable = DialogWrapper.findInstance(component)?.disposable ?: return@onShown
+ ComponentValidator(disposable).withValidator(
+ Supplier {
+ try {
+ Pattern.compile(component.text)
+ null
+ } catch (e: PatternSyntaxException) {
+ ValidationInfoBuilder(component).error("Invalid regex")
+ }
+ }
+ ).andRegisterOnDocumentListener(component).installOn(component)
+ }
+ }
+ return this
+}
+
+private class RegexConverter : Converter() {
+ override fun toString(value: Regex) = value.pattern
+
+ override fun fromString(value: String) = runCatching { value.toRegex() }.getOrNull()
+}
+
+private class RenameWithInheritanceFix(
+ element: PsiNamedElement,
+ private val newName: String
+) : LocalQuickFixAndIntentionActionOnPsiElement(element) {
+ private val isMethod = element is PsiMethod
+ private val text = CodeInsightBundle.message("rename.named.element.text", element.name, newName)
+
+ override fun getFamilyName() = CodeInsightBundle.message("rename.element.family")
+
+ override fun getText() = text
+
+ override fun invoke(
+ project: Project,
+ file: PsiFile,
+ editor: Editor?,
+ startElement: PsiElement,
+ endElement: PsiElement
+ ) {
+ if (isMethod) {
+ val method = startElement as? PsiMethod ?: return
+ if (editor != null) {
+ SuperMethodWarningUtil.checkSuperMethod(method, { md ->
+ RenameProcessor(project, md, newName, false, false).run()
+ true
+ }, editor)
+ } else {
+ val superMethod = method.findDeepestSuperMethods().firstOrNull()
+ for (md in listOfNotNull(superMethod, method)) {
+ RenameProcessor(project, md, newName, false, false).run()
+ }
+ }
+ } else {
+ if (!FileModificationService.getInstance().prepareFileForWrite(file)) {
+ return
+ }
+ RenameProcessor(project, startElement, newName, false, false).run()
+ }
+ }
+
+ override fun startInWriteAction() = isMethod
+}
Index: src/main/kotlin/platform/mixin/inspection/addedMembers/MissingUniqueAnnotationInspection.kt
===================================================================
--- src/main/kotlin/platform/mixin/inspection/addedMembers/MissingUniqueAnnotationInspection.kt (revision 3aa2181c17f9286e605a65fc6589a5eb6124acc8)
+++ src/main/kotlin/platform/mixin/inspection/addedMembers/MissingUniqueAnnotationInspection.kt (revision 0da34782e5c29f3e4f13ec0cefdc9bca4e1e8baa)
@@ -20,67 +20,27 @@
package com.demonwav.mcdev.platform.mixin.inspection.addedMembers
-import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler
-import com.demonwav.mcdev.platform.mixin.inspection.MixinInspection
import com.demonwav.mcdev.platform.mixin.util.MixinConstants
-import com.demonwav.mcdev.platform.mixin.util.isMixin
import com.intellij.codeInsight.intention.AddAnnotationFix
import com.intellij.codeInspection.ProblemsHolder
-import com.intellij.psi.JavaElementVisitor
-import com.intellij.psi.PsiElementVisitor
import com.intellij.psi.PsiField
import com.intellij.psi.PsiMethod
-import com.intellij.psi.search.searches.SuperMethodsSearch
-class MissingUniqueAnnotationInspection : MixinInspection() {
+class MissingUniqueAnnotationInspection : AbstractAddedMembersInspection() {
override fun getStaticDescription() = "Reports missing @Unique annotations"
- override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor = Visitor(holder)
-
- private class Visitor(private val holder: ProblemsHolder) : JavaElementVisitor() {
- override fun visitField(field: PsiField) {
- if (field.containingClass?.isMixin != true) {
- return
- }
-
- if (field.hasAnnotation(MixinConstants.Annotations.SHADOW)) {
- return
- }
-
+ override fun visitAddedField(holder: ProblemsHolder, field: PsiField) {
- if (!field.hasAnnotation(MixinConstants.Annotations.UNIQUE)) {
- holder.registerProblem(
- field.nameIdentifier,
- "Missing @Unique annotation",
- AddAnnotationFix(MixinConstants.Annotations.UNIQUE, field)
- )
- }
- }
+ if (!field.hasAnnotation(MixinConstants.Annotations.UNIQUE)) {
+ holder.registerProblem(
+ field.nameIdentifier,
+ "Missing @Unique annotation",
+ AddAnnotationFix(MixinConstants.Annotations.UNIQUE, field)
+ )
+ }
+ }
- override fun visitMethod(method: PsiMethod) {
- if (method.containingClass?.isMixin != true) {
- return
- }
-
- if (method.isConstructor) {
- return
- }
-
- val superMethod = SuperMethodsSearch.search(method, null, true, false).findFirst()
- if (superMethod != null) {
- return
- }
-
- val hasMixinAnnotation = method.annotations.any {
- val fqn = it.qualifiedName ?: return@any false
- fqn in ignoredMethodAnnotations || MixinAnnotationHandler.forMixinAnnotation(
- fqn,
- holder.project
- ) != null
- }
- if (hasMixinAnnotation) {
- return
- }
-
+ override fun visitAddedMethod(holder: ProblemsHolder, method: PsiMethod, isInherited: Boolean) {
+ if (!isInherited && !method.hasAnnotation(MixinConstants.Annotations.UNIQUE)) {
holder.registerProblem(
method.nameIdentifier ?: return,
"Missing @Unique annotation",
@@ -88,16 +48,4 @@
)
}
}
-
- companion object {
- private val ignoredMethodAnnotations = setOf(
- MixinConstants.Annotations.UNIQUE,
- MixinConstants.Annotations.SHADOW,
- MixinConstants.Annotations.ACCESSOR,
- MixinConstants.Annotations.INVOKER,
- MixinConstants.Annotations.OVERWRITE,
- MixinConstants.Annotations.INTRINSIC,
- MixinConstants.Annotations.SOFT_OVERRIDE,
- )
- }
+}
-}
Index: src/main/resources/META-INF/plugin.xml
===================================================================
--- src/main/resources/META-INF/plugin.xml (revision 3aa2181c17f9286e605a65fc6589a5eb6124acc8)
+++ src/main/resources/META-INF/plugin.xml (revision 0da34782e5c29f3e4f13ec0cefdc9bca4e1e8baa)
@@ -760,6 +760,14 @@
level="WARNING"
hasStaticDescription="true"
implementationClass="com.demonwav.mcdev.platform.mixin.inspection.addedMembers.MissingUniqueAnnotationInspection"/>
+