From 37a26361abb0ed67604b850722efe02f676e5732 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Sun, 25 Aug 2024 14:57:55 +0200 Subject: [PATCH 01/20] Fix bad AT lexing for highlighting This should resolved all the highlighting weirdnesses happening while editing ATs, like whole entries being red while correct Also adds a recovery rule for keywords and only use consumeTokenFast for recovery rules, to show users what is wrong with their syntax --- src/main/grammars/AtLexer.flex | 14 +++++++++----- src/main/grammars/AtParser.bnf | 4 +++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/main/grammars/AtLexer.flex b/src/main/grammars/AtLexer.flex index d5e1ad8f0..6ffc9d78f 100644 --- a/src/main/grammars/AtLexer.flex +++ b/src/main/grammars/AtLexer.flex @@ -21,7 +21,7 @@ package com.demonwav.mcdev.platform.mcp.at.gen; import com.intellij.lexer.*; -import com.intellij.psi.tree.IElementType; +import com.intellij.psi.TokenType;import com.intellij.psi.tree.IElementType; import static com.demonwav.mcdev.platform.mcp.at.gen.psi.AtTypes.*; import static com.intellij.psi.TokenType.*; @@ -48,8 +48,9 @@ import static com.intellij.psi.TokenType.*; PRIMITIVE=[ZBCSIFDJV] CLASS_VALUE=(\[+[ZBCSIFDJ]|(\[*L[^;\n]+;)) KEYWORD_ELEMENT=(public|private|protected|default)([-+]f)? -NAME_ELEMENT=([\p{L}_\p{Sc}][\p{L}\p{N}_\p{Sc}]*)| -CLASS_NAME_ELEMENT=([\p{L}_\p{Sc}][\p{L}\p{N}_\p{Sc}]*\.)*[\p{L}_\p{Sc}][\p{L}\p{N}_\p{Sc}]* +IDENTIFIER=[\p{L}_\p{Sc}][\p{L}\p{N}_\p{Sc}]* +NAME_ELEMENT=({IDENTIFIER})| +CLASS_NAME_ELEMENT=({IDENTIFIER}*\.)*{IDENTIFIER} COMMENT=#.* CRLF=\n|\r|\r\n WHITE_SPACE=\s @@ -57,7 +58,10 @@ WHITE_SPACE=\s %% { - {KEYWORD_ELEMENT} { yybegin(CLASS_NAME); return KEYWORD_ELEMENT; } + // Force a whitespace because otherwise the keyword and class name can be right next to each other + {KEYWORD_ELEMENT}/{WHITE_SPACE} { yybegin(CLASS_NAME); return KEYWORD_ELEMENT; } + // Fallback to avoid breaking code highlighting at the keyword + {NAME_ELEMENT} { return NAME_ELEMENT; } } { @@ -73,7 +77,7 @@ WHITE_SPACE=\s "(" { return OPEN_PAREN; } ")" { return CLOSE_PAREN; } {CLASS_VALUE} { return CLASS_VALUE; } - {PRIMITIVE} ({PRIMITIVE}|{CLASS_VALUE})* { zzMarkedPos = zzStartRead + 1; return PRIMITIVE; } + {PRIMITIVE} { return PRIMITIVE; } } {CRLF} { yybegin(YYINITIAL); return CRLF; } diff --git a/src/main/grammars/AtParser.bnf b/src/main/grammars/AtParser.bnf index fcad83735..24b5d1092 100644 --- a/src/main/grammars/AtParser.bnf +++ b/src/main/grammars/AtParser.bnf @@ -32,7 +32,7 @@ elementTypeClass="com.demonwav.mcdev.platform.mcp.at.psi.AtElementType" tokenTypeClass="com.demonwav.mcdev.platform.mcp.at.psi.AtTokenType" - consumeTokenMethod="consumeTokenFast" + consumeTokenMethod(".*_recover")="consumeTokenFast" } at_file ::= line* @@ -60,7 +60,9 @@ keyword ::= KEYWORD_ELEMENT { methods=[ keywordElement="KEYWORD_ELEMENT" ] + recoverWhile=keyword_recover } +private keyword_recover ::= !(NAME_ELEMENT | CLASS_NAME_ELEMENT) class_name ::= CLASS_NAME_ELEMENT { mixin="com.demonwav.mcdev.platform.mcp.at.psi.mixins.impl.AtClassNameImplMixin" From 50786caf5fa6fb9e33628a3ec781bca327fcb4d7 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Mon, 26 Aug 2024 16:35:35 +0200 Subject: [PATCH 02/20] Rework how AT references and completion work AtGotoDeclarationHandler is replaced by proper PsiReferences --- changelog.md | 6 + .../mcp/at/AtGotoDeclarationHandler.kt | 110 ------ .../platform/mcp/at/AtReferenceContributor.kt | 324 ++++++++++++++++++ .../at/completion/AtCompletionContributor.kt | 303 ++-------------- .../kotlin/platform/mcp/at/manipulators.kt | 46 +++ .../psi/mixins/impl/AtClassNameImplMixin.kt | 14 +- .../psi/mixins/impl/AtFieldNameImplMixin.kt | 10 + .../at/psi/mixins/impl/AtFunctionImplMixin.kt | 10 + src/main/resources/META-INF/plugin.xml | 14 +- src/test/kotlin/framework/test-util.kt | 44 +++ .../platform/mcp/at/AtCompletionTest.kt | 163 +++++++++ .../platform/mcp/at/AtReferencesTest.kt | 139 ++++++++ 12 files changed, 796 insertions(+), 387 deletions(-) delete mode 100644 src/main/kotlin/platform/mcp/at/AtGotoDeclarationHandler.kt create mode 100644 src/main/kotlin/platform/mcp/at/AtReferenceContributor.kt create mode 100644 src/main/kotlin/platform/mcp/at/manipulators.kt create mode 100644 src/test/kotlin/platform/mcp/at/AtCompletionTest.kt create mode 100644 src/test/kotlin/platform/mcp/at/AtReferencesTest.kt diff --git a/changelog.md b/changelog.md index 00f5aa55e..ac7796422 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,12 @@ - `plugin.yml`, `paper-plugin.yml` and `bungee.yml` main class reference and validity inspection +### Changed + +- Overhauled Access Transformer support: + - many lexing errors should now be fixed + - class names and member names now have their own references, replacing the custom Goto handler + ## [1.8.1] - 2024-08-10 ### Added diff --git a/src/main/kotlin/platform/mcp/at/AtGotoDeclarationHandler.kt b/src/main/kotlin/platform/mcp/at/AtGotoDeclarationHandler.kt deleted file mode 100644 index a15df98a8..000000000 --- a/src/main/kotlin/platform/mcp/at/AtGotoDeclarationHandler.kt +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Minecraft Development for IntelliJ - * - * https://mcdev.io/ - * - * Copyright (C) 2024 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.mcp.at - -import com.demonwav.mcdev.facet.MinecraftFacet -import com.demonwav.mcdev.platform.mcp.McpModuleType -import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtClassName -import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtEntry -import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtFieldName -import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtFuncName -import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtFunction -import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtTypes -import com.demonwav.mcdev.util.findQualifiedClass -import com.demonwav.mcdev.util.getPrimitiveType -import com.demonwav.mcdev.util.parseClassDescriptor -import com.intellij.codeInsight.navigation.actions.GotoDeclarationHandler -import com.intellij.openapi.actionSystem.DataContext -import com.intellij.openapi.editor.Editor -import com.intellij.openapi.module.ModuleUtilCore -import com.intellij.psi.JavaPsiFacade -import com.intellij.psi.PsiElement -import com.intellij.psi.search.GlobalSearchScope - -class AtGotoDeclarationHandler : GotoDeclarationHandler { - override fun getGotoDeclarationTargets( - sourceElement: PsiElement?, - offset: Int, - editor: Editor, - ): Array? { - if (sourceElement?.language !== AtLanguage) { - return null - } - - val module = ModuleUtilCore.findModuleForPsiElement(sourceElement) ?: return null - - val instance = MinecraftFacet.getInstance(module) ?: return null - - val mcpModule = instance.getModuleOfType(McpModuleType) ?: return null - - val srgMap = mcpModule.mappingsManager?.mappingsNow ?: return null - - return when { - sourceElement.node.treeParent.elementType === AtTypes.CLASS_NAME -> { - val className = sourceElement.parent as AtClassName - val classSrgToMcp = srgMap.getMappedClass(className.classNameText) - val psiClass = findQualifiedClass(sourceElement.project, classSrgToMcp) ?: return null - arrayOf(psiClass) - } - sourceElement.node.treeParent.elementType === AtTypes.FUNC_NAME -> { - val funcName = sourceElement.parent as AtFuncName - val function = funcName.parent as AtFunction - val entry = function.parent as AtEntry - - val reference = srgMap.getMappedMethod(AtMemberReference.get(entry, function) ?: return null) - val member = reference.resolveMember(sourceElement.project) ?: return null - arrayOf(member) - } - sourceElement.node.treeParent.elementType === AtTypes.FIELD_NAME -> { - val fieldName = sourceElement.parent as AtFieldName - val entry = fieldName.parent as AtEntry - - val reference = srgMap.getMappedField(AtMemberReference.get(entry, fieldName) ?: return null) - val member = reference.resolveMember(sourceElement.project) ?: return null - arrayOf(member) - } - sourceElement.node.elementType === AtTypes.CLASS_VALUE -> { - val className = srgMap.getMappedClass(parseClassDescriptor(sourceElement.text)) - val psiClass = findQualifiedClass(sourceElement.project, className) ?: return null - arrayOf(psiClass) - } - sourceElement.node.elementType === AtTypes.PRIMITIVE -> { - val text = sourceElement.text - if (text.length != 1) { - return null - } - - val type = getPrimitiveType(text[0]) ?: return null - - val boxedType = type.boxedTypeName ?: return null - - val psiClass = JavaPsiFacade.getInstance(sourceElement.project).findClass( - boxedType, - GlobalSearchScope.allScope(sourceElement.project), - ) ?: return null - arrayOf(psiClass) - } - else -> null - } - } - - override fun getActionText(context: DataContext): String? = null -} diff --git a/src/main/kotlin/platform/mcp/at/AtReferenceContributor.kt b/src/main/kotlin/platform/mcp/at/AtReferenceContributor.kt new file mode 100644 index 000000000..95195d441 --- /dev/null +++ b/src/main/kotlin/platform/mcp/at/AtReferenceContributor.kt @@ -0,0 +1,324 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.mcp.at + +import com.demonwav.mcdev.facet.MinecraftFacet +import com.demonwav.mcdev.platform.mcp.McpModuleType +import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtClassName +import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtEntry +import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtFieldName +import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtFunction +import com.demonwav.mcdev.platform.mcp.at.psi.AtElement +import com.demonwav.mcdev.platform.neoforge.NeoForgeModuleType +import com.demonwav.mcdev.util.MemberReference +import com.demonwav.mcdev.util.MinecraftVersions +import com.demonwav.mcdev.util.SemanticVersion +import com.demonwav.mcdev.util.findMethods +import com.demonwav.mcdev.util.findModule +import com.demonwav.mcdev.util.findQualifiedClass +import com.demonwav.mcdev.util.getPrimitiveWrapperClass +import com.demonwav.mcdev.util.memberReference +import com.demonwav.mcdev.util.nameAndParameterTypes +import com.demonwav.mcdev.util.qualifiedMemberReference +import com.demonwav.mcdev.util.simpleQualifiedMemberReference +import com.intellij.codeInsight.completion.InsertHandler +import com.intellij.codeInsight.completion.InsertionContext +import com.intellij.codeInsight.completion.JavaLookupElementBuilder +import com.intellij.codeInsight.completion.PrioritizedLookupElement +import com.intellij.codeInsight.lookup.LookupElement +import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.openapi.util.Iconable +import com.intellij.openapi.util.TextRange +import com.intellij.patterns.PlatformPatterns.psiElement +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiField +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiReference +import com.intellij.psi.PsiReferenceBase +import com.intellij.psi.PsiReferenceContributor +import com.intellij.psi.PsiReferenceProvider +import com.intellij.psi.PsiReferenceRegistrar +import com.intellij.util.ArrayUtil +import com.intellij.util.PlatformIcons +import com.intellij.util.ProcessingContext + +class AtReferenceContributor : PsiReferenceContributor() { + + override fun registerReferenceProviders(registrar: PsiReferenceRegistrar) { + registrar.registerReferenceProvider(psiElement(AtClassName::class.java), AtClassNameReferenceProvider) + registrar.registerReferenceProvider(psiElement(AtFieldName::class.java), AtFieldNameReferenceProvider) + registrar.registerReferenceProvider(psiElement(AtFunction::class.java), AtFuncNameReferenceProvider) + } +} + +object AtClassNameReferenceProvider : PsiReferenceProvider() { + + override fun getReferencesByElement( + element: PsiElement, + context: ProcessingContext + ): Array { + element as AtClassName + + val references = mutableListOf() + val fqn = element.text + + var partStart = 0 + while (true) { + val partEnd = fqn.indexOf('.', partStart) + if (partEnd == -1) { + while (true) { + var outerEnd = fqn.indexOf('$', partStart) + if (outerEnd == -1) { + val range = TextRange(partStart, fqn.length) + references.add(AtClassNamePartReference(element, range, true)) + break + } else { + val range = TextRange(partStart, outerEnd) + references.add(AtClassNamePartReference(element, range, true)) + } + + partStart = outerEnd + 1 + } + + break + } else { + val range = TextRange(partStart, partEnd) + references.add(AtClassNamePartReference(element, range, false)) + } + + partStart = partEnd + 1 + } + + return references.toTypedArray() + } +} + +class AtClassNamePartReference(element: AtClassName, range: TextRange, val isClass: Boolean) : + PsiReferenceBase(element, range) { + + override fun resolve(): PsiElement? { + val project = element.project + val fqn = element.text.substring(0, rangeInElement.endOffset) + val psiFacade = JavaPsiFacade.getInstance(project) + if (isClass) { + val scope = element.resolveScope + if (fqn.contains('$')) { + val outermostClass = psiFacade.findClass(fqn.substringBefore('$'), scope) + if (outermostClass != null) { + val innerClassNames = fqn.substringAfter('$').split('$') + return innerClassNames.fold(outermostClass) { clazz, innerClassName -> + clazz.findInnerClassByName(innerClassName, false) ?: return null + } + } + } else { + val containingPackage = psiFacade.findPackage(fqn.substringBeforeLast('.')) + val clazz = containingPackage?.findClassByShortName(fqn.substringAfterLast('.'), scope)?.firstOrNull() + if (clazz != null) { + return clazz + } + } + } + + return psiFacade.findPackage(fqn) + } + + override fun getVariants(): Array { + val project = element.project + val text = element.text + if (text.contains('$')) { + val classFqn = text.substringBeforeLast('$').replace('$', '.') + val scope = element.resolveScope + val clazz = JavaPsiFacade.getInstance(project).findClass(classFqn, scope) + if (clazz != null) { + return clazz.allInnerClasses.mapNotNull { JavaLookupElementBuilder.forClass(it) }.toTypedArray() + } + } else { + val packFqn = text.substringBeforeLast('.') + val pack = JavaPsiFacade.getInstance(project).findPackage(packFqn) + if (pack != null) { + val elements = mutableListOf() + pack.classes.filter { it.name != "package-info" } + .mapNotNullTo(elements) { JavaLookupElementBuilder.forClass(it) } + pack.subPackages.mapNotNullTo(elements) { subPackage -> + LookupElementBuilder.create(subPackage) + .withIcon(subPackage.getIcon(Iconable.ICON_FLAG_VISIBILITY)) + } + return elements.toTypedArray() + } + } + + return ArrayUtil.EMPTY_STRING_ARRAY + } +} + +abstract class AtClassMemberReference(element: E, range: TextRange) : + PsiReferenceBase(element, range) { + + override fun getVariants(): Array { + val entry = element.parent as? AtEntry ?: return ArrayUtil.EMPTY_OBJECT_ARRAY + + val module = element.findModule() ?: return ArrayUtil.EMPTY_OBJECT_ARRAY + val instance = MinecraftFacet.getInstance(module) + val mcpModule = instance?.getModuleOfType(McpModuleType) ?: return ArrayUtil.EMPTY_OBJECT_ARRAY + val isNeoForge = instance.isOfType(NeoForgeModuleType) + val (mapField, mapMethod) = if (isNeoForge) { + { it: PsiField -> it.memberReference } to { it: PsiMethod -> it.memberReference } + } else { + val srgMap = mcpModule.mappingsManager?.mappingsNow ?: return ArrayUtil.EMPTY_OBJECT_ARRAY + { it: PsiField -> srgMap.getIntermediaryField(it) } to { it: PsiMethod -> srgMap.getIntermediaryMethod(it) } + } + + val results = mutableListOf() + + val entryClass = entry.className.classNameValue ?: return ArrayUtil.EMPTY_OBJECT_ARRAY + for (field in entryClass.fields) { + val memberReference = mapField(field) ?: field.simpleQualifiedMemberReference + val lookupElement = LookupElementBuilder.create(memberReference.name) + .withLookupStrings(listOf(field.name)) // Some fields don't appear in completion without this + .withPsiElement(field) + .withPresentableText(field.name) + .withIcon(PlatformIcons.FIELD_ICON) + .withTailText(" (${memberReference.name})".takeUnless { isNeoForge }, true) + .withInsertHandler(AtClassMemberInsertionHandler(field.name.takeUnless { isNeoForge })) + results.add(PrioritizedLookupElement.withPriority(lookupElement, 1.0)) + } + + for (method in entryClass.methods) { + val memberReference = mapMethod(method) ?: method.qualifiedMemberReference + val lookupElement = LookupElementBuilder.create(memberReference.name + memberReference.descriptor) + .withLookupStrings(listOf(method.name)) // For symmetry with fields, might happen too + .withPsiElement(method) + .withPresentableText(method.nameAndParameterTypes) + .withIcon(PlatformIcons.METHOD_ICON) + .withTailText(" (${memberReference.name})".takeUnless { isNeoForge }, true) + .withInsertHandler(AtClassMemberInsertionHandler(method.name.takeUnless { isNeoForge })) + results.add(PrioritizedLookupElement.withPriority(lookupElement, 0.0)) + } + + return results.toTypedArray() + } +} + +object AtFieldNameReferenceProvider : PsiReferenceProvider() { + + override fun getReferencesByElement( + element: PsiElement, + context: ProcessingContext + ): Array = arrayOf(AtFieldNameReference(element as AtFieldName)) +} + +class AtFieldNameReference(element: AtFieldName) : + AtClassMemberReference(element, TextRange(0, element.text.length)) { + + override fun resolve(): PsiElement? { + val entry = element.parent as? AtEntry ?: return null + val entryClass = entry.className?.classNameValue ?: return null + + val module = element.findModule() ?: return null + val instance = MinecraftFacet.getInstance(module) ?: return null + val mcpModule = instance.getModuleOfType(McpModuleType) ?: return null + + return if (instance.isOfType(NeoForgeModuleType) && + mcpModule.getSettings().minecraftVersion?.let(SemanticVersion::tryParse) + ?.let { it >= MinecraftVersions.MC1_20_2 } == true + ) { + entryClass.findFieldByName(element.text, false) + } else { + val srgMap = mcpModule.mappingsManager?.mappingsNow ?: return null + val reference = srgMap.getMappedField(AtMemberReference.get(entry, element) ?: return null) + reference.resolveMember(module.project) + } + } +} + +object AtFuncNameReferenceProvider : PsiReferenceProvider() { + + override fun getReferencesByElement( + element: PsiElement, + context: ProcessingContext + ): Array { + val func = element as AtFunction + val references = mutableListOf(AtFuncNameReference(func)) + + element.argumentList.mapTo(references) { AtClassValueReference(func, it) } + + references.add(AtClassValueReference(element, element.returnValue)) + + return references.toTypedArray() + } +} + +class AtFuncNameReference(element: AtFunction) : + AtClassMemberReference(element, element.funcName.textRangeInParent) { + + override fun resolve(): PsiElement? { + val entry = element.parent as? AtEntry ?: return null + val entryClass = entry.className?.classNameValue ?: return null + + val module = element.findModule() ?: return null + val instance = MinecraftFacet.getInstance(module) ?: return null + val mcpModule = instance.getModuleOfType(McpModuleType) ?: return null + + return if (instance.isOfType(NeoForgeModuleType) && + mcpModule.getSettings().minecraftVersion?.let(SemanticVersion::tryParse) + ?.let { it >= MinecraftVersions.MC1_20_2 } == true + ) { + val memberReference = MemberReference.parse(element.text) ?: return null + entryClass.findMethods(memberReference).firstOrNull() + } else { + val srgMap = mcpModule.mappingsManager?.mappingsNow ?: return null + val reference = srgMap.getMappedMethod(AtMemberReference.get(entry, element) ?: return null) + reference.resolveMember(module.project) + } + } +} + +class AtClassValueReference(val element: AtFunction, val argument: AtElement) : + PsiReferenceBase(element, argument.textRangeInParent, false) { + + override fun resolve(): PsiElement? { + val text = argument.text.substringAfterLast('[') + return when (val c = text[0]) { + 'L' -> if (!text.contains('.')) { + findQualifiedClass(element.project, text.substring(1, text.length - 1).replace('/', '.')) + } else { + null + } + + else -> getPrimitiveWrapperClass(c, element.project) + } + } +} + +private class AtClassMemberInsertionHandler(val memberName: String?) : InsertHandler { + + override fun handleInsert(context: InsertionContext, item: LookupElement) { + val line = context.document.getLineNumber(context.tailOffset) + context.document.deleteString(context.tailOffset, context.document.getLineEndOffset(line)) + + if (memberName != null) { + val comment = " # $memberName" + context.document.insertString(context.editor.caretModel.offset, comment) + context.editor.caretModel.moveCaretRelatively(comment.length, 0, false, false, false) + } + } +} diff --git a/src/main/kotlin/platform/mcp/at/completion/AtCompletionContributor.kt b/src/main/kotlin/platform/mcp/at/completion/AtCompletionContributor.kt index 48ebeb794..140a16b70 100644 --- a/src/main/kotlin/platform/mcp/at/completion/AtCompletionContributor.kt +++ b/src/main/kotlin/platform/mcp/at/completion/AtCompletionContributor.kt @@ -20,41 +20,29 @@ package com.demonwav.mcdev.platform.mcp.at.completion -import com.demonwav.mcdev.facet.MinecraftFacet -import com.demonwav.mcdev.platform.mcp.McpModuleType -import com.demonwav.mcdev.platform.mcp.at.AtElementFactory +import com.demonwav.mcdev.platform.mcp.at.AtElementFactory.Keyword import com.demonwav.mcdev.platform.mcp.at.AtLanguage import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtEntry -import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtFieldName -import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtFunction import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtTypes -import com.demonwav.mcdev.util.anonymousElements import com.demonwav.mcdev.util.fullQualifiedName -import com.demonwav.mcdev.util.getSimilarity -import com.demonwav.mcdev.util.nameAndParameterTypes -import com.demonwav.mcdev.util.qualifiedMemberReference -import com.demonwav.mcdev.util.simpleQualifiedMemberReference import com.intellij.codeInsight.completion.CompletionContributor import com.intellij.codeInsight.completion.CompletionParameters import com.intellij.codeInsight.completion.CompletionResultSet import com.intellij.codeInsight.completion.CompletionType -import com.intellij.codeInsight.completion.CompletionUtil -import com.intellij.codeInsight.completion.PrioritizedLookupElement +import com.intellij.codeInsight.completion.JavaLookupElementBuilder import com.intellij.codeInsight.lookup.LookupElementBuilder -import com.intellij.openapi.module.ModuleUtilCore -import com.intellij.patterns.PlatformPatterns.elementType import com.intellij.patterns.PlatformPatterns.psiElement import com.intellij.patterns.PsiElementPattern import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.JavaRecursiveElementVisitor +import com.intellij.psi.PsiAnonymousClass import com.intellij.psi.PsiClass -import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiElement -import com.intellij.psi.search.GlobalSearchScope -import com.intellij.psi.search.PsiShortNamesCache +import com.intellij.psi.PsiPackage +import com.intellij.psi.TokenType import com.intellij.psi.tree.IElementType -import com.intellij.psi.tree.TokenSet import com.intellij.psi.util.PsiUtilCore -import com.intellij.util.PlatformIcons +import com.intellij.psi.util.parentOfType class AtCompletionContributor : CompletionContributor() { @@ -69,279 +57,46 @@ class AtCompletionContributor : CompletionContributor() { } val parent = position.parent - - val parentText = parent.text ?: return - if (parentText.length < CompletionUtil.DUMMY_IDENTIFIER.length) { - return - } - val text = parentText.substring(0, parentText.length - CompletionUtil.DUMMY_IDENTIFIER.length) - when { - AFTER_KEYWORD.accepts(parent) -> handleAtClassName(text, parent, result) - AFTER_CLASS_NAME.accepts(parent) -> handleAtName(text, parent, result) - AFTER_NEWLINE.accepts(parent) -> handleNewLine(text, result) + AFTER_KEYWORD.accepts(parent) -> completeAtClassName(parent, result) + position.parentOfType() == null -> completeKeywords(result) } } - private fun handleAtClassName(text: String, element: PsiElement, result: CompletionResultSet) { - if (text.isEmpty()) { - return - } - - val currentPackage = text.substringBeforeLast('.', "") - val beginning = text.substringAfterLast('.', "") - - if (currentPackage == "" || beginning == "") { - return - } - - val module = ModuleUtilCore.findModuleForPsiElement(element) ?: return - val scope = GlobalSearchScope.moduleWithDependenciesAndLibrariesScope(module) - val project = module.project - - // Short name completion - if (!text.contains('.')) { - val kindResult = result.withPrefixMatcher(KindPrefixMatcher(text)) - val cache = PsiShortNamesCache.getInstance(project) - - var counter = 0 - for (className in cache.allClassNames) { - if (!className.contains(beginning, ignoreCase = true)) { - continue - } - - if (counter++ > 1000) { - break // Prevent insane CPU usage - } - - val classesByName = cache.getClassesByName(className, scope) - for (classByName in classesByName) { - val name = classByName.fullQualifiedName ?: continue - kindResult.addElement( - PrioritizedLookupElement.withPriority( - LookupElementBuilder.create(name).withIcon(PlatformIcons.CLASS_ICON), - 1.0 + name.getValue(beginning), - ), - ) - } - } - } - - // Anonymous and inner class completion - if (text.contains('$')) { - val currentClass = - JavaPsiFacade.getInstance(project).findClass(text.substringBeforeLast('$'), scope) ?: return - - for (innerClass in currentClass.allInnerClasses) { - if (innerClass.name?.contains(beginning.substringAfterLast('$'), ignoreCase = true) != true) { - continue - } - - val name = innerClass.fullQualifiedName ?: continue - result.addElement( - PrioritizedLookupElement.withPriority( - LookupElementBuilder.create(name).withIcon(PlatformIcons.CLASS_ICON), - 1.0, - ), - ) - } - - for (anonymousElement in currentClass.anonymousElements) { - val anonClass = anonymousElement as? PsiClass ?: continue - - val name = anonClass.fullQualifiedName ?: continue - result.addElement( - PrioritizedLookupElement.withPriority( - LookupElementBuilder.create(name).withIcon(PlatformIcons.CLASS_ICON), - 1.0, - ), - ) - } - return - } - - val psiPackage = JavaPsiFacade.getInstance(project).findPackage(currentPackage) ?: return - - // Classes in package completion - val used = mutableSetOf() - for (psiClass in psiPackage.classes) { - if (psiClass.name == null) { - continue - } - - if (!psiClass.name!!.contains(beginning, ignoreCase = true) || psiClass.name == "package-info") { - continue - } - - if (!used.add(psiClass.name!!)) { - continue - } - - val name = psiClass.fullQualifiedName ?: continue - result.addElement( - PrioritizedLookupElement.withPriority( - LookupElementBuilder.create(name).withIcon(PlatformIcons.CLASS_ICON), - 1.0, - ), - ) - } - used.clear() // help GC - - // Packages in package completion - for (subPackage in psiPackage.subPackages) { - if (subPackage.name == null) { - continue - } - - if (!subPackage.name!!.contains(beginning, ignoreCase = true)) { - continue - } - - val name = subPackage.qualifiedName - result.addElement( - PrioritizedLookupElement.withPriority( - LookupElementBuilder.create(name).withIcon(PlatformIcons.PACKAGE_ICON), - 0.0, - ), - ) - } + private fun completeKeywords(result: CompletionResultSet) { + result.addAllElements(Keyword.entries.map { LookupElementBuilder.create(it.text) }) } - private fun handleAtName(text: String, memberName: PsiElement, result: CompletionResultSet) { - if (memberName !is AtFieldName) { + private fun completeAtClassName(element: PsiElement, result: CompletionResultSet) { + if (element.textContains('.')) { + // Only complete "empty" class names here, the rest is handled by the reference variants return } - val entry = memberName.parent as? AtEntry ?: return - - val entryClass = entry.className?.classNameValue ?: return - - val module = ModuleUtilCore.findModuleForPsiElement(memberName) ?: return - val project = module.project - - val mcpModule = MinecraftFacet.getInstance(module)?.getModuleOfType(McpModuleType) ?: return - - val srgMap = mcpModule.mappingsManager?.mappingsNow ?: return - - val srgResult = result.withPrefixMatcher(SrgPrefixMatcher(text)) - - for (field in entryClass.fields) { - if (!field.name.contains(text, ignoreCase = true)) { - continue + val mcPackage = JavaPsiFacade.getInstance(element.project).findPackage("net.minecraft") ?: return + mcPackage.accept(object : JavaRecursiveElementVisitor() { + override fun visitPackage(aPackage: PsiPackage) { + aPackage.subPackages.forEach { it.accept(this) } + aPackage.classes.forEach { it.accept(this); it.acceptChildren(this) } } - val memberReference = srgMap.getIntermediaryField(field) ?: field.simpleQualifiedMemberReference - srgResult.addElement( - PrioritizedLookupElement.withPriority( - LookupElementBuilder - .create(field.name) - .withIcon(PlatformIcons.FIELD_ICON) - .withTailText(" (${memberReference.name})", true) - .withInsertHandler handler@{ context, _ -> - val currentElement = context.file.findElementAt(context.startOffset) ?: return@handler - currentElement.replace( - AtElementFactory.createFieldName( - context.project, - memberReference.name, - ), - ) - - // TODO: Fix visibility decrease - PsiDocumentManager.getInstance(context.project) - .doPostponedOperationsAndUnblockDocument(context.document) - val comment = " # ${field.name}" - context.document.insertString(context.editor.caretModel.offset, comment) - context.editor.caretModel.moveCaretRelatively(comment.length, 0, false, false, false) - }, - 1.0, - ), - ) - } + override fun visitClass(aClass: PsiClass) { + if (aClass !is PsiAnonymousClass) { + val fqn = aClass.fullQualifiedName + if (fqn != null) { + result.addElement(JavaLookupElementBuilder.forClass(aClass, fqn)) + } + } - for (method in entryClass.methods) { - if (!method.name.contains(text, ignoreCase = true)) { - continue + super.visitClass(aClass) } - - val memberReference = srgMap.getIntermediaryMethod(method) ?: method.qualifiedMemberReference - srgResult.addElement( - PrioritizedLookupElement.withPriority( - LookupElementBuilder.create(method.nameAndParameterTypes) - .withIcon(PlatformIcons.METHOD_ICON) - .withTailText(" (${memberReference.name})", true) - .withInsertHandler handler@{ context, _ -> - var currentElement = context.file.findElementAt(context.startOffset) ?: return@handler - var counter = 0 - while (currentElement !is AtFieldName && currentElement !is AtFunction) { - currentElement = currentElement.parent - if (counter++ > 3) { - break - } - } - - // Hopefully this won't happen lol - if (currentElement !is AtFieldName && currentElement !is AtFunction) { - return@handler - } - - if (currentElement is AtFieldName) { - // get rid of the bad parameters - val parent = currentElement.parent - val children = - parent.node.getChildren(TokenSet.create(AtTypes.OPEN_PAREN, AtTypes.CLOSE_PAREN)) - if (children.size == 2) { - parent.node.removeRange(children[0], children[1].treeNext) - } - } - - currentElement.replace( - AtElementFactory.createFunction( - project, - memberReference.name + memberReference.descriptor, - ), - ) - - // TODO: Fix visibility decreases - PsiDocumentManager.getInstance(context.project) - .doPostponedOperationsAndUnblockDocument(context.document) - val comment = " # ${method.name}" - context.document.insertString(context.editor.caretModel.offset, comment) - context.editor.caretModel.moveCaretRelatively(comment.length, 0, false, false, false) - }, - 0.0, - ), - ) - } - } - - private fun handleNewLine(text: String, result: CompletionResultSet) { - for (keyword in AtElementFactory.Keyword.softMatch(text)) { - result.addElement(LookupElementBuilder.create(keyword.text)) - } - } - - /** - * This helps order the (hopefully) most relevant entries in the short name completion - */ - private fun String?.getValue(text: String): Int { - if (this == null) { - return 0 - } - - // Push net.minecraft{forge} classes up to the top - val packageBonus = if (this.startsWith("net.minecraft")) 10_000 else 0 - - val thisName = this.substringAfterLast('.') - - return thisName.getSimilarity(text, packageBonus) + }) } companion object { fun after(type: IElementType): PsiElementPattern.Capture = - psiElement().afterSibling(psiElement().withElementType(elementType().oneOf(type))) + psiElement().afterSiblingSkipping(psiElement(TokenType.WHITE_SPACE), psiElement(type)) val AFTER_KEYWORD = after(AtTypes.KEYWORD) - val AFTER_CLASS_NAME = after(AtTypes.CLASS_NAME) - val AFTER_NEWLINE = after(AtTypes.CRLF) } } diff --git a/src/main/kotlin/platform/mcp/at/manipulators.kt b/src/main/kotlin/platform/mcp/at/manipulators.kt new file mode 100644 index 000000000..5422776fd --- /dev/null +++ b/src/main/kotlin/platform/mcp/at/manipulators.kt @@ -0,0 +1,46 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.mcp.at + +import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtClassName +import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtFieldName +import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtFunction +import com.demonwav.mcdev.platform.mcp.at.psi.AtElement +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.TextRange +import com.intellij.psi.AbstractElementManipulator + +abstract class AtElementManipulator(val factory: (Project, String) -> E) : + AbstractElementManipulator() { + + override fun handleContentChange(element: E, range: TextRange, newContent: String): E? { + val text = element.text + val newText = text.substring(0, range.startOffset) + newContent + text.substring(range.endOffset) + @Suppress("UNCHECKED_CAST") + return element.replace(factory(element.project, newText)) as E + } +} + +class AtClassNameElementManipulator : AtElementManipulator(AtElementFactory::createClassName) + +class AtFieldNameElementManipulator : AtElementManipulator(AtElementFactory::createFieldName) + +class AtFuncNameElementManipulator : AtElementManipulator(AtElementFactory::createFunction) diff --git a/src/main/kotlin/platform/mcp/at/psi/mixins/impl/AtClassNameImplMixin.kt b/src/main/kotlin/platform/mcp/at/psi/mixins/impl/AtClassNameImplMixin.kt index f9a5a8aa0..db3e41b8b 100644 --- a/src/main/kotlin/platform/mcp/at/psi/mixins/impl/AtClassNameImplMixin.kt +++ b/src/main/kotlin/platform/mcp/at/psi/mixins/impl/AtClassNameImplMixin.kt @@ -22,14 +22,16 @@ package com.demonwav.mcdev.platform.mcp.at.psi.mixins.impl import com.demonwav.mcdev.platform.mcp.at.AtElementFactory import com.demonwav.mcdev.platform.mcp.at.psi.mixins.AtClassNameMixin -import com.demonwav.mcdev.util.findQualifiedClass import com.intellij.extapi.psi.ASTWrapperPsiElement import com.intellij.lang.ASTNode +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiReference +import com.intellij.psi.impl.source.resolve.reference.ReferenceProvidersRegistry abstract class AtClassNameImplMixin(node: ASTNode) : ASTWrapperPsiElement(node), AtClassNameMixin { override val classNameValue - get() = findQualifiedClass(project, classNameText) + get() = references.last()?.resolve() as? PsiClass override val classNameText: String get() = classNameElement.text @@ -37,4 +39,12 @@ abstract class AtClassNameImplMixin(node: ASTNode) : ASTWrapperPsiElement(node), override fun setClassName(className: String) { replace(AtElementFactory.createClassName(project, className)) } + + override fun getReference(): PsiReference? { + return references.firstOrNull() + } + + override fun getReferences(): Array { + return ReferenceProvidersRegistry.getReferencesFromProviders(this) + } } diff --git a/src/main/kotlin/platform/mcp/at/psi/mixins/impl/AtFieldNameImplMixin.kt b/src/main/kotlin/platform/mcp/at/psi/mixins/impl/AtFieldNameImplMixin.kt index 982d04d03..84516dc90 100644 --- a/src/main/kotlin/platform/mcp/at/psi/mixins/impl/AtFieldNameImplMixin.kt +++ b/src/main/kotlin/platform/mcp/at/psi/mixins/impl/AtFieldNameImplMixin.kt @@ -24,6 +24,8 @@ import com.demonwav.mcdev.platform.mcp.at.AtElementFactory import com.demonwav.mcdev.platform.mcp.at.psi.mixins.AtFieldNameMixin import com.intellij.extapi.psi.ASTWrapperPsiElement import com.intellij.lang.ASTNode +import com.intellij.psi.PsiReference +import com.intellij.psi.impl.source.resolve.reference.ReferenceProvidersRegistry abstract class AtFieldNameImplMixin(node: ASTNode) : ASTWrapperPsiElement(node), AtFieldNameMixin { @@ -33,4 +35,12 @@ abstract class AtFieldNameImplMixin(node: ASTNode) : ASTWrapperPsiElement(node), override val fieldNameText: String get() = nameElement.text + + override fun getReference(): PsiReference? { + return references.firstOrNull() + } + + override fun getReferences(): Array { + return ReferenceProvidersRegistry.getReferencesFromProviders(this) + } } diff --git a/src/main/kotlin/platform/mcp/at/psi/mixins/impl/AtFunctionImplMixin.kt b/src/main/kotlin/platform/mcp/at/psi/mixins/impl/AtFunctionImplMixin.kt index dd8b39df5..5b1d977ed 100644 --- a/src/main/kotlin/platform/mcp/at/psi/mixins/impl/AtFunctionImplMixin.kt +++ b/src/main/kotlin/platform/mcp/at/psi/mixins/impl/AtFunctionImplMixin.kt @@ -24,6 +24,8 @@ import com.demonwav.mcdev.platform.mcp.at.AtElementFactory import com.demonwav.mcdev.platform.mcp.at.psi.mixins.AtFunctionMixin import com.intellij.extapi.psi.ASTWrapperPsiElement import com.intellij.lang.ASTNode +import com.intellij.psi.PsiReference +import com.intellij.psi.impl.source.resolve.reference.ReferenceProvidersRegistry abstract class AtFunctionImplMixin(node: ASTNode) : ASTWrapperPsiElement(node), AtFunctionMixin { @@ -42,4 +44,12 @@ abstract class AtFunctionImplMixin(node: ASTNode) : ASTWrapperPsiElement(node), override fun setFunction(function: String) { replace(AtElementFactory.createFunction(project, function)) } + + override fun getReference(): PsiReference? { + return references.firstOrNull() + } + + override fun getReferences(): Array { + return ReferenceProvidersRegistry.getReferencesFromProviders(this) + } } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 0ed01a521..68b954960 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -625,8 +625,15 @@ - + + + + + + + + diff --git a/src/test/kotlin/framework/test-util.kt b/src/test/kotlin/framework/test-util.kt index 8242d9de8..08a2b66a1 100644 --- a/src/test/kotlin/framework/test-util.kt +++ b/src/test/kotlin/framework/test-util.kt @@ -40,6 +40,7 @@ import com.intellij.testFramework.LexerTestCase import com.intellij.testFramework.fixtures.JavaCodeInsightTestFixture import com.intellij.util.ReflectionUtil import org.junit.jupiter.api.Assertions +import org.opentest4j.AssertionFailedError typealias ProjectBuilderFunc = ProjectBuilder.(path: String, code: String, configure: Boolean, allowAst: Boolean) -> VirtualFile @@ -131,3 +132,46 @@ fun testInspectionFix(fixture: JavaCodeInsightTestFixture, basePath: String, fix fixture.launchAction(intention) fixture.checkResult(expected) } + +fun assertEqualsUnordered(expected: Collection, actual: Collection) { + val expectedSet = expected.toSet() + val actualSet = actual.toSet() + val notFound = expectedSet.minus(actualSet) + val notExpected = actualSet.minus(expectedSet) + + if (notExpected.isNotEmpty() && notFound.isNotEmpty()) { + val message = """| + |Expecting actual: + | $actual + |to contain exactly in any order: + | $expected + |elements not found: + | $notFound + |and elements not expected: + | $notExpected + """.trimMargin() + throw AssertionFailedError(message, expected, actual) + } + if (notFound.isNotEmpty()) { + val message = """| + |Expecting actual: + | $actual + |to contain exactly in any order: + | $expected + |but could not find the following elements: + | $notFound + """.trimMargin() + throw AssertionFailedError(message, expected, actual) + } + if (notExpected.isNotEmpty()) { + val message = """| + |Expecting actual: + | $actual + |to contain exactly in any order: + | $expected + |but the following elements were unexpected: + | $notExpected + """.trimMargin() + throw AssertionFailedError(message, expected, actual) + } +} diff --git a/src/test/kotlin/platform/mcp/at/AtCompletionTest.kt b/src/test/kotlin/platform/mcp/at/AtCompletionTest.kt new file mode 100644 index 000000000..8bc24bf75 --- /dev/null +++ b/src/test/kotlin/platform/mcp/at/AtCompletionTest.kt @@ -0,0 +1,163 @@ +package com.demonwav.mcdev.platform.mcp.at + +import com.demonwav.mcdev.facet.MinecraftFacet +import com.demonwav.mcdev.framework.BaseMinecraftTest +import com.demonwav.mcdev.framework.assertEqualsUnordered +import com.demonwav.mcdev.platform.PlatformType +import com.demonwav.mcdev.platform.mcp.McpModuleSettings +import com.demonwav.mcdev.platform.mcp.McpModuleType +import com.demonwav.mcdev.platform.mcp.at.AtElementFactory.Keyword +import com.demonwav.mcdev.util.runWriteActionAndWait +import com.intellij.codeInsight.lookup.Lookup +import org.intellij.lang.annotations.Language +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("Access Transformer Completion Tests") +class AtCompletionTest : BaseMinecraftTest(PlatformType.MCP, PlatformType.NEOFORGE) { + + @BeforeEach + fun setupProject() { + buildProject { + java( + "net/minecraft/Minecraft.java", + """ + package net.minecraft; + public class Minecraft { + private String privString; + private void method() {} + private void overloaded() {} + private void overloaded(String arg) {} + private void copy(TestLibrary from) {} + private void add(int amount) {} + } + """.trimIndent() + ) + java( + "net/minecraft/server/MinecraftServer.java", + """ + package net.minecraft.server; + public class MinecraftServer {} + """.trimIndent() + ) + } + + // Force 1.20.2 because we test the non-SRG member names with NeoForge + MinecraftFacet.getInstance(fixture.module, McpModuleType)!! + .updateSettings(McpModuleSettings.State(minecraftVersion = "1.20.2")) + } + + private fun doCompletionTest( + @Language("Access Transformers") before: String, + @Language("Access Transformers") after: String, + lookupToUse: String? = null + ) { + fixture.configureByText("test_at.cfg", before) + fixture.completeBasic() + if (lookupToUse != null) { + val lookupElement = fixture.lookupElements?.find { it.lookupString == lookupToUse } + assertNotNull(lookupElement, "Could not find lookup element with lookup string '$lookupToUse'") + runWriteActionAndWait { + fixture.lookup.currentItem = lookupElement + } + fixture.type(Lookup.NORMAL_SELECT_CHAR) + } + fixture.checkResult(after) + } + + @Test + @DisplayName("Keyword Lookup Elements In Empty File") + fun keywordLookupElements() { + fixture.configureByText("test_at.cfg", "") + val lookupElements = fixture.completeBasic() + val lookupStrings = lookupElements.map { it.lookupString } + val expectedStrings = Keyword.entries.map { it.text } + assertEqualsUnordered(expectedStrings, lookupStrings) + } + + @Test + @DisplayName("Empty Class Name Lookup Elements") + fun emptyClassNameLookupElements() { + fixture.configureByText("test_at.cfg", "public ") + val lookupElements = fixture.completeBasic() + val lookupStrings = lookupElements.map { it.lookupString } + val expectedStrings = setOf("net.minecraft.Minecraft", "net.minecraft.server.MinecraftServer") + assertEqualsUnordered(expectedStrings, lookupStrings) + } + + @Test + @DisplayName("Class Name Package Lookup Elements") + fun packageLookupElements() { + fixture.configureByText("test_at.cfg", "public net.") + val lookupElements = fixture.completeBasic() + val lookupStrings = lookupElements.map { it.lookupString } + val expectedStrings = setOf("minecraft") + assertEqualsUnordered(expectedStrings, lookupStrings) + } + + @Test + @DisplayName("Class Name Package And Class Lookup Elements") + fun packageAndClassLookupElements() { + fixture.configureByText("test_at.cfg", "public net.minecraft.") + val lookupElements = fixture.completeBasic() + val lookupStrings = lookupElements.map { it.lookupString } + val expectedStrings = setOf("server", "Minecraft") + assertEqualsUnordered(expectedStrings, lookupStrings) + } + + @Test + @DisplayName("Member Lookup Elements") + fun memberLookupElements() { + fixture.configureByText("test_at.cfg", "public net.minecraft.Minecraft ") + val lookupElements = fixture.completeBasic() + val lookupStrings = lookupElements.map { it.lookupString } + val expectedStrings = + setOf("privString", "add(I)V", "copy(L;)V", "method()V", "overloaded()V", "overloaded(Ljava/lang/String;)V") + assertEqualsUnordered(expectedStrings, lookupStrings) + } + + @Test + @DisplayName("Full Class Name Completion") + fun fullClassNameCompletion() { + doCompletionTest( + "public ", + "public net.minecraft.Minecraft", + "net.minecraft.Minecraft" + ) + doCompletionTest( + "public ", + "public net.minecraft.server.MinecraftServer", + "net.minecraft.server.MinecraftServer" + ) + } + + @Test + @DisplayName("Field Name Completion") + fun fieldNameCompletion() { + doCompletionTest( + "public net.minecraft.Minecraft privS", + "public net.minecraft.Minecraft privString" + ) + } + + @Test + @DisplayName("Method Name Completion") + fun methodNameCompletion() { + doCompletionTest( + "public net.minecraft.Minecraft add", + "public net.minecraft.Minecraft add(I)V" + ) + } + + @Test + @DisplayName("Method Name Completion Cleaning End Of Line") + fun methodNameCompletionCleaningEndOfLine() { + doCompletionTest( + "public net.minecraft.Minecraft overloaded(Ljava/some)V invalid; stuff", + "public net.minecraft.Minecraft overloaded(Ljava/lang/String;)V", + "overloaded(Ljava/lang/String;)V" + ) + } +} diff --git a/src/test/kotlin/platform/mcp/at/AtReferencesTest.kt b/src/test/kotlin/platform/mcp/at/AtReferencesTest.kt new file mode 100644 index 000000000..b51c226dd --- /dev/null +++ b/src/test/kotlin/platform/mcp/at/AtReferencesTest.kt @@ -0,0 +1,139 @@ +package com.demonwav.mcdev.platform.mcp.at + +import com.demonwav.mcdev.facet.MinecraftFacet +import com.demonwav.mcdev.framework.BaseMinecraftTest +import com.demonwav.mcdev.platform.PlatformType +import com.demonwav.mcdev.platform.mcp.McpModuleSettings +import com.demonwav.mcdev.platform.mcp.McpModuleType +import com.intellij.openapi.application.runReadAction +import com.intellij.psi.CommonClassNames +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiField +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiPackage +import org.intellij.lang.annotations.Language +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("Access Transformer References Tests") +class AtReferencesTest : BaseMinecraftTest(PlatformType.MCP, PlatformType.NEOFORGE) { + + @BeforeEach + fun setupProject() { + buildProject { + java( + "com/demonwav/mcdev/mcp/test/TestLibrary.java", + """ + package com.demonwav.mcdev.mcp.test; + public class TestLibrary { + private String privString; + private void method() {} + private void overloaded() {} + private void overloaded(String arg) {} + private void copy(TestLibrary from) {} + private void add(int amount) {} + } + """.trimIndent() + ) + } + + // Force 1.20.2 because we test the non-SRG member names with NeoForge + MinecraftFacet.getInstance(fixture.module, McpModuleType)!! + .updateSettings(McpModuleSettings.State(minecraftVersion = "1.20.2")) + } + + private inline fun testReferenceAtCaret( + @Language("Access Transformers") at: String, + crossinline test: (element: E) -> Unit + ) { + fixture.configureByText("test_at.cfg", at) + runReadAction { + val ref = fixture.getReferenceAtCaretPositionWithAssertion() + val resolved = ref.resolve().also(::assertNotNull)!! + test(assertInstanceOf(E::class.java, resolved)) + } + } + + @Test + @DisplayName("Package Reference") + fun packageReference() { + testReferenceAtCaret("public com.demonwav.mcdev.mcp.test.TestLibrary privString") { pack -> + val expectedPackage = fixture.findPackage("com.demonwav.mcdev.mcp") + assertEquals(expectedPackage, pack) + } + } + + @Test + @DisplayName("Class Reference") + fun classReference() { + testReferenceAtCaret("public com.demonwav.mcdev.mcp.test.TestLibrary privString") { clazz -> + val expectedClass = fixture.findClass("com.demonwav.mcdev.mcp.test.TestLibrary") + assertEquals(expectedClass, clazz) + } + } + + @Test + @DisplayName("Field Reference") + fun fieldReference() { + testReferenceAtCaret("public com.demonwav.mcdev.mcp.test.TestLibrary privString") { field -> + val expectedClass = fixture.findClass("com.demonwav.mcdev.mcp.test.TestLibrary") + val expectedField = expectedClass.findFieldByName("privString", false) + assertEquals(expectedField, field) + } + } + + @Test + @DisplayName("Method Reference") + fun methodReference() { + testReferenceAtCaret("public com.demonwav.mcdev.mcp.test.TestLibrary method()V") { method -> + val expectedClass = fixture.findClass("com.demonwav.mcdev.mcp.test.TestLibrary") + val expectedMethod = expectedClass.findMethodsByName("method", false).single() + assertEquals(expectedMethod, method) + } + } + + @Test + @DisplayName("Method Overload Reference") + fun methodOverloadReference() { + testReferenceAtCaret( + "public com.demonwav.mcdev.mcp.test.TestLibrary overloaded()V" + ) { method -> + val expectedClass = fixture.findClass("com.demonwav.mcdev.mcp.test.TestLibrary") + val expectedMethod = expectedClass.findMethodsByName("overloaded", false).single { !it.hasParameters() } + assertEquals(expectedMethod, method) + } + + testReferenceAtCaret( + "public com.demonwav.mcdev.mcp.test.TestLibrary overloaded(Ljava/lang/String;)V" + ) { method -> + val expectedClass = fixture.findClass("com.demonwav.mcdev.mcp.test.TestLibrary") + val expectedMethod = expectedClass.findMethodsByName("overloaded", false).single { it.hasParameters() } + assertEquals(expectedMethod, method) + } + } + + @Test + @DisplayName("Descriptor Class Type Reference") + fun descriptorClassTypeReference() { + testReferenceAtCaret( + "public com.demonwav.mcdev.mcp.test.TestLibrary copy(Ljava/lang/String;)V" + ) { clazz -> + val expectedClass = fixture.findClass(CommonClassNames.JAVA_LANG_STRING) + assertEquals(expectedClass, clazz) + } + } + + @Test + @DisplayName("Descriptor Primitive Type Reference") + fun descriptorPrimitiveTypeReference() { + testReferenceAtCaret( + "public com.demonwav.mcdev.mcp.test.TestLibrary copy(I)V" + ) { clazz -> + val expectedClass = fixture.findClass(CommonClassNames.JAVA_LANG_INTEGER) + assertEquals(expectedClass, clazz) + } + } +} From 66171aece9f32d406d55a4e2cab7df9176beecd7 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Mon, 26 Aug 2024 16:45:00 +0200 Subject: [PATCH 03/20] Add a AT entry copy action for NeoForge 1.20.2+ They no longer use SRG for member names --- changelog.md | 1 + .../mcp/actions/CopyNeoForgeAtAction.kt | 107 ++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 src/main/kotlin/platform/mcp/actions/CopyNeoForgeAtAction.kt diff --git a/changelog.md b/changelog.md index ac7796422..702d21a82 100644 --- a/changelog.md +++ b/changelog.md @@ -11,6 +11,7 @@ - Overhauled Access Transformer support: - many lexing errors should now be fixed - class names and member names now have their own references, replacing the custom Goto handler + - SRG names are no longer used on NeoForge 1.20.2+ and a new copy action is available for it ## [1.8.1] - 2024-08-10 diff --git a/src/main/kotlin/platform/mcp/actions/CopyNeoForgeAtAction.kt b/src/main/kotlin/platform/mcp/actions/CopyNeoForgeAtAction.kt new file mode 100644 index 000000000..11e42e23f --- /dev/null +++ b/src/main/kotlin/platform/mcp/actions/CopyNeoForgeAtAction.kt @@ -0,0 +1,107 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.mcp.actions + +import com.demonwav.mcdev.platform.mcp.McpModuleType +import com.demonwav.mcdev.platform.mcp.actions.SrgActionBase.Companion.showBalloon +import com.demonwav.mcdev.platform.mcp.actions.SrgActionBase.Companion.showSuccessBalloon +import com.demonwav.mcdev.platform.mixin.handlers.ShadowHandler +import com.demonwav.mcdev.platform.neoforge.NeoForgeModuleType +import com.demonwav.mcdev.util.MinecraftVersions +import com.demonwav.mcdev.util.SemanticVersion +import com.demonwav.mcdev.util.descriptor +import com.demonwav.mcdev.util.getDataFromActionEvent +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.editor.Editor +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiField +import com.intellij.psi.PsiMember +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiReference +import java.awt.Toolkit +import java.awt.datatransfer.StringSelection + +class CopyNeoForgeAtAction : AnAction() { + + override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT + + override fun update(e: AnActionEvent) { + e.presentation.isEnabledAndVisible = isAvailable(e) + } + + private fun isAvailable(e: AnActionEvent): Boolean { + val data = getDataFromActionEvent(e) ?: return false + if (!data.instance.isOfType(NeoForgeModuleType)) { + return false + } + + val mcpModule = data.instance.getModuleOfType(McpModuleType) ?: return false + val mcVersion = mcpModule.getSettings().minecraftVersion?.let(SemanticVersion::tryParse) ?: return false + return mcVersion >= MinecraftVersions.MC1_20_2 + } + + override fun actionPerformed(e: AnActionEvent) { + val data = getDataFromActionEvent(e) ?: return + + var parent = data.element.parent + if (parent is PsiMember) { + val shadowTarget = ShadowHandler.getInstance()?.findFirstShadowTargetForReference(parent)?.element + if (shadowTarget != null) { + parent = shadowTarget + } + } + + if (parent is PsiReference) { + parent = parent.resolve() ?: return showBalloon("Not a valid element", e) + } + + when (parent) { + is PsiClass -> { + val fqn = parent.qualifiedName ?: return showBalloon("Could not find class FQN", e) + copyToClipboard(data.editor, data.element, fqn) + } + is PsiField -> { + val classFqn = parent.containingClass?.qualifiedName + ?: return showBalloon("Could not find class FQN", e) + copyToClipboard(data.editor, data.element, "$classFqn ${parent.name}") + } + is PsiMethod -> { + val classFqn = parent.containingClass?.qualifiedName + ?: return showBalloon("Could not find class FQN", e) + val methodDescriptor = parent.descriptor + ?: return showBalloon("Could not compute method descriptor", e) + copyToClipboard(data.editor, data.element, "$classFqn ${parent.name}$methodDescriptor") + } + else -> showBalloon("Not a valid element", e) + } + return + } + + private fun copyToClipboard(editor: Editor, element: PsiElement, text: String) { + val stringSelection = StringSelection(text) + val clpbrd = Toolkit.getDefaultToolkit().systemClipboard + clpbrd.setContents(stringSelection, null) + showSuccessBalloon(editor, element, "Copied $text") + } +} From cd7aada25e9c0481e14b2ffd3aff17567108f69e Mon Sep 17 00:00:00 2001 From: RedNesto Date: Tue, 27 Aug 2024 00:05:26 +0200 Subject: [PATCH 04/20] Add missing license headers --- .../platform/mcp/at/AtCompletionTest.kt | 20 +++++++++++++++++++ .../platform/mcp/at/AtReferencesTest.kt | 20 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/src/test/kotlin/platform/mcp/at/AtCompletionTest.kt b/src/test/kotlin/platform/mcp/at/AtCompletionTest.kt index 8bc24bf75..329cff22e 100644 --- a/src/test/kotlin/platform/mcp/at/AtCompletionTest.kt +++ b/src/test/kotlin/platform/mcp/at/AtCompletionTest.kt @@ -1,3 +1,23 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.mcp.at import com.demonwav.mcdev.facet.MinecraftFacet diff --git a/src/test/kotlin/platform/mcp/at/AtReferencesTest.kt b/src/test/kotlin/platform/mcp/at/AtReferencesTest.kt index b51c226dd..d0ba8c2f7 100644 --- a/src/test/kotlin/platform/mcp/at/AtReferencesTest.kt +++ b/src/test/kotlin/platform/mcp/at/AtReferencesTest.kt @@ -1,3 +1,23 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.mcp.at import com.demonwav.mcdev.facet.MinecraftFacet From 931e7f95dd362bd7d36815d197ac0711eb8364b7 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Tue, 27 Aug 2024 13:44:18 +0200 Subject: [PATCH 05/20] Rewrite AtUsageInspection Removed redundant code and fixes some false positives --- .../platform/mcp/at/AtUsageInspection.kt | 141 ++++++++++++++---- .../psi/mixins/impl/AtClassNameImplMixin.kt | 2 +- src/main/kotlin/util/scope-utils.kt | 33 ++++ src/main/resources/META-INF/plugin.xml | 1 + .../platform/mcp/at/AtUsageInspectionTest.kt | 92 ++++++++++++ 5 files changed, 237 insertions(+), 32 deletions(-) create mode 100644 src/main/kotlin/util/scope-utils.kt create mode 100644 src/test/kotlin/platform/mcp/at/AtUsageInspectionTest.kt diff --git a/src/main/kotlin/platform/mcp/at/AtUsageInspection.kt b/src/main/kotlin/platform/mcp/at/AtUsageInspection.kt index ccba93f39..797d053c5 100644 --- a/src/main/kotlin/platform/mcp/at/AtUsageInspection.kt +++ b/src/main/kotlin/platform/mcp/at/AtUsageInspection.kt @@ -20,24 +20,34 @@ package com.demonwav.mcdev.platform.mcp.at -import com.demonwav.mcdev.facet.MinecraftFacet -import com.demonwav.mcdev.platform.mcp.McpModuleType import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtEntry -import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtFieldName -import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtFunction +import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtTypes +import com.demonwav.mcdev.util.excludeFileTypes import com.intellij.codeInspection.LocalInspectionTool -import com.intellij.codeInspection.ProblemHighlightType +import com.intellij.codeInspection.LocalQuickFixOnPsiElement import com.intellij.codeInspection.ProblemsHolder -import com.intellij.openapi.module.ModuleUtilCore +import com.intellij.codeInspection.util.IntentionFamilyName +import com.intellij.codeInspection.util.IntentionName +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiClass import com.intellij.psi.PsiElement import com.intellij.psi.PsiElementVisitor +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiMethod import com.intellij.psi.search.GlobalSearchScope +import com.intellij.psi.search.searches.OverridingMethodsSearch import com.intellij.psi.search.searches.ReferencesSearch +import com.intellij.psi.util.elementType +import com.intellij.psi.util.siblings class AtUsageInspection : LocalInspectionTool() { override fun getStaticDescription(): String { - return "The declared access transformer is never used" + return "Reports unused Access Transformer entries" + } + + override fun isSuppressedFor(element: PsiElement): Boolean { + return super.isSuppressedFor(element) } override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { @@ -47,33 +57,102 @@ class AtUsageInspection : LocalInspectionTool() { return } - val module = ModuleUtilCore.findModuleForPsiElement(element) ?: return - val instance = MinecraftFacet.getInstance(module) ?: return - val mcpModule = instance.getModuleOfType(McpModuleType) ?: return - val srgMap = mcpModule.mappingsManager?.mappingsNow ?: return - - val member = element.function ?: element.fieldName ?: return - val reference = AtMemberReference.get(element, member) ?: return - - val psi = when (member) { - is AtFunction -> - reference.resolveMember(element.project) ?: srgMap.tryGetMappedMethod(reference)?.resolveMember( - element.project, - ) ?: return - is AtFieldName -> - reference.resolveMember(element.project) - ?: srgMap.tryGetMappedField(reference)?.resolveMember(element.project) ?: return - else -> + val function = element.function + if (function != null) { + checkElement(element, function) + return + } + + val fieldName = element.fieldName + if (fieldName != null) { + checkElement(element, fieldName) + return + } + + // Only check class names if it is the target of the entry + checkElement(element, element.className) + } + + private fun checkElement(entry: AtEntry, element: PsiElement) { + val referenced = element.reference?.resolve() ?: return + val scope = GlobalSearchScope.projectScope(element.project) + .excludeFileTypes(element.project, AtFileType) + val query = ReferencesSearch.search(referenced, scope, true) + if (query.any()) { + return + } + + if (referenced is PsiMethod) { + // The regular references search doesn't cover overridden methods + val overridingQuery = OverridingMethodsSearch.search(referenced, scope, true) + if (overridingQuery.any()) { return + } + + // Also ignore if other entries cover super methods + val superMethods = referenced.findSuperMethods() + for (childEntry in entry.containingFile.children) { + if (childEntry !is AtEntry || childEntry == entry) { + continue + } + + val function = childEntry.function ?: continue + val otherResolved = function.reference?.resolve() + if (superMethods.contains(otherResolved)) { + return + } + } + } + + if (referenced is PsiClass) { + // Do not report classes whose members are used in the mod + for (field in referenced.fields) { + if (ReferencesSearch.search(field, scope, true).any()) { + return + } + } + for (method in referenced.methods) { + if (ReferencesSearch.search(method, scope, true).any()) { + return + } + } + for (innerClass in referenced.innerClasses) { + if (ReferencesSearch.search(innerClass, scope, true).any()) { + return + } + } } - val query = ReferencesSearch.search(psi, GlobalSearchScope.projectScope(element.project)) - query.findFirst() - ?: holder.registerProblem( - element, - "Access Transformer entry is never used", - ProblemHighlightType.LIKE_UNUSED_SYMBOL, - ) + val fix = RemoveAtEntryFix.forWholeLine(entry) + holder.registerProblem(entry, "Access Transformer entry is never used", fix) + } + } + } + + private class RemoveAtEntryFix(startElement: PsiElement, endElement: PsiElement) : + LocalQuickFixOnPsiElement(startElement, endElement) { + + override fun getFamilyName(): @IntentionFamilyName String = "Remove entry" + + override fun getText(): @IntentionName String = familyName + + override fun invoke( + project: Project, + file: PsiFile, + startElement: PsiElement, + endElement: PsiElement + ) { + startElement.parent.deleteChildRange(startElement, endElement) + } + + companion object { + + fun forWholeLine(entry: AtEntry): RemoveAtEntryFix { + val start = entry.siblings(forward = false, withSelf = false) + .firstOrNull { it.elementType == AtTypes.CRLF }?.nextSibling + val end = entry.siblings(forward = true, withSelf = true) + .firstOrNull { it.elementType == AtTypes.CRLF } + return RemoveAtEntryFix(start ?: entry, end ?: entry) } } } diff --git a/src/main/kotlin/platform/mcp/at/psi/mixins/impl/AtClassNameImplMixin.kt b/src/main/kotlin/platform/mcp/at/psi/mixins/impl/AtClassNameImplMixin.kt index db3e41b8b..f264535ab 100644 --- a/src/main/kotlin/platform/mcp/at/psi/mixins/impl/AtClassNameImplMixin.kt +++ b/src/main/kotlin/platform/mcp/at/psi/mixins/impl/AtClassNameImplMixin.kt @@ -41,7 +41,7 @@ abstract class AtClassNameImplMixin(node: ASTNode) : ASTWrapperPsiElement(node), } override fun getReference(): PsiReference? { - return references.firstOrNull() + return references.lastOrNull() } override fun getReferences(): Array { diff --git a/src/main/kotlin/util/scope-utils.kt b/src/main/kotlin/util/scope-utils.kt new file mode 100644 index 000000000..c4a881a9c --- /dev/null +++ b/src/main/kotlin/util/scope-utils.kt @@ -0,0 +1,33 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.util + +import com.intellij.openapi.fileTypes.FileType +import com.intellij.openapi.project.Project +import com.intellij.psi.search.GlobalSearchScope + +fun GlobalSearchScope.excludeFileTypes(project: Project, vararg fileTypes: FileType): GlobalSearchScope = + this.intersectWith(GlobalSearchScope.everythingScope(project).restrictByFileTypes(*fileTypes).not()) + +fun GlobalSearchScope.restrictByFileTypes(vararg fileTypes: FileType): GlobalSearchScope = + GlobalSearchScope.getScopeRestrictedByFileTypes(this, *fileTypes) + +fun GlobalSearchScope.not(): GlobalSearchScope = GlobalSearchScope.notScope(this) diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 68b954960..678b37fa8 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -992,6 +992,7 @@ language="Access Transformers" enabledByDefault="true" level="WARNING" + editorAttributes="NOT_USED_ELEMENT_ATTRIBUTES" hasStaticDescription="true" implementationClass="com.demonwav.mcdev.platform.mcp.at.AtUsageInspection"/> . + */ + +package com.demonwav.mcdev.platform.mcp.at + +import com.demonwav.mcdev.facet.MinecraftFacet +import com.demonwav.mcdev.framework.BaseMinecraftTest +import com.demonwav.mcdev.platform.PlatformType +import com.demonwav.mcdev.platform.mcp.McpModuleSettings +import com.demonwav.mcdev.platform.mcp.McpModuleType +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("Access Transformer Usage Inspection Tests") +class AtUsageInspectionTest : BaseMinecraftTest(PlatformType.MCP, PlatformType.NEOFORGE) { + + @Test + @DisplayName("Usage Inspection") + fun usageInspection() { + buildProject { + java( + "net/minecraft/Used.java", + """ + package net.minecraft; + public class Used { + public int usedField; + public int unusedField; + public void usedMethod() {} + public void unusedMethod() {} + } + """.trimIndent(), + allowAst = true + ) + java( + "net/minecraft/server/Unused.java", + """ + package net.minecraft.server; + public class Unused {} + """.trimIndent(), + allowAst = true + ) + java( + "com/demonwav/mcdev/mcp/test/TestMod.java", + """ + package com.demonwav.mcdev.mcp.test; + public class TestMod { + public TestMod () { + net.minecraft.Used mc = new net.minecraft.Used(); + int value = mc.usedField; + mc.usedMethod(); + } + } + """.trimIndent(), + allowAst = true + ) + at( + "test_at.cfg", + """ + public net.minecraft.Used + public net.minecraft.Used usedField + public net.minecraft.Used unusedField + public net.minecraft.Used usedMethod()V + public net.minecraft.Used unusedMethod()V + public net.minecraft.server.Unused + """.trimIndent()) + } + + // Force 1.20.2 because we test the non-SRG member names with NeoForge + MinecraftFacet.getInstance(fixture.module, McpModuleType)!! + .updateSettings(McpModuleSettings.State(minecraftVersion = "1.20.2")) + + fixture.enableInspections(AtUsageInspection::class.java) + fixture.checkHighlighting() + } +} From 0227e84d4d4df6afea914deba9a5e3239255bfe4 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Tue, 27 Aug 2024 13:45:03 +0200 Subject: [PATCH 06/20] Add AT inspection suppressor --- .../platform/mcp/at/AtInspectionSuppressor.kt | 91 +++++++++++++++++++ .../mcp/at/psi/mixins/AtEntryMixin.kt | 4 + .../at/psi/mixins/impl/AtEntryImplMixin.kt | 24 +++++ src/main/resources/META-INF/plugin.xml | 1 + 4 files changed, 120 insertions(+) create mode 100644 src/main/kotlin/platform/mcp/at/AtInspectionSuppressor.kt diff --git a/src/main/kotlin/platform/mcp/at/AtInspectionSuppressor.kt b/src/main/kotlin/platform/mcp/at/AtInspectionSuppressor.kt new file mode 100644 index 000000000..9419c5069 --- /dev/null +++ b/src/main/kotlin/platform/mcp/at/AtInspectionSuppressor.kt @@ -0,0 +1,91 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.mcp.at + +import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtEntry +import com.intellij.codeInspection.InspectionSuppressor +import com.intellij.codeInspection.LocalQuickFixOnPsiElement +import com.intellij.codeInspection.SuppressQuickFix +import com.intellij.codeInspection.util.IntentionFamilyName +import com.intellij.codeInspection.util.IntentionName +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.util.parentOfType + +class AtInspectionSuppressor : InspectionSuppressor { + + override fun isSuppressedFor(element: PsiElement, toolId: String): Boolean { + val entry = element.parentOfType(withSelf = true) ?: return false + val comment = entry.commentText ?: return false + val suppressed = comment.substringAfter("Suppress:").substringBefore(' ').split(',') + return toolId in suppressed + } + + override fun getSuppressActions( + element: PsiElement?, + toolId: String + ): Array { + if (element == null) { + return SuppressQuickFix.EMPTY_ARRAY + } + + return arrayOf(AtSuppressQuickFix(element, toolId)) + } + + class AtSuppressQuickFix(element: PsiElement, val toolId: String) : LocalQuickFixOnPsiElement(element), SuppressQuickFix { + + override fun getText(): @IntentionName String = "Suppress $toolId" + + override fun getFamilyName(): @IntentionFamilyName String = "Suppress inspection" + + override fun invoke( + project: Project, + file: PsiFile, + startElement: PsiElement, + endElement: PsiElement + ) { + val entry = startElement.parentOfType(withSelf = true) ?: return + val commentText = entry.commentText?.trim() + if (commentText == null) { + entry.setComment("Suppress:$toolId") + return + } + + val suppressStart = commentText.indexOf("Suppress:") + if (suppressStart == -1) { + entry.setComment("Suppress:$toolId $commentText") + return + } + + val suppressEnd = commentText.indexOf(' ', suppressStart).takeUnless { it == -1 } ?: commentText.length + val newComment = commentText.substring(suppressStart, suppressEnd) + ",$toolId" + commentText.substring(suppressEnd) + entry.setComment(newComment) + } + + override fun isAvailable( + project: Project, + context: PsiElement + ): Boolean = context.isValid + + override fun isSuppressAll(): Boolean = false + } +} diff --git a/src/main/kotlin/platform/mcp/at/psi/mixins/AtEntryMixin.kt b/src/main/kotlin/platform/mcp/at/psi/mixins/AtEntryMixin.kt index 45599bdb6..874b377ac 100644 --- a/src/main/kotlin/platform/mcp/at/psi/mixins/AtEntryMixin.kt +++ b/src/main/kotlin/platform/mcp/at/psi/mixins/AtEntryMixin.kt @@ -27,6 +27,7 @@ import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtFieldName import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtFunction import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtKeyword import com.demonwav.mcdev.platform.mcp.at.psi.AtElement +import com.intellij.psi.PsiComment interface AtEntryMixin : AtElement { @@ -35,6 +36,8 @@ interface AtEntryMixin : AtElement { val fieldName: AtFieldName? val function: AtFunction? val keyword: AtKeyword + val comment: PsiComment? + val commentText: String? fun setEntry(entry: String) fun setKeyword(keyword: AtElementFactory.Keyword) @@ -42,6 +45,7 @@ interface AtEntryMixin : AtElement { fun setFieldName(fieldName: String) fun setFunction(function: String) fun setAsterisk() + fun setComment(text: String?) fun replaceMember(element: AtElement) { // One of these must be true diff --git a/src/main/kotlin/platform/mcp/at/psi/mixins/impl/AtEntryImplMixin.kt b/src/main/kotlin/platform/mcp/at/psi/mixins/impl/AtEntryImplMixin.kt index e4558b6bc..888e49748 100644 --- a/src/main/kotlin/platform/mcp/at/psi/mixins/impl/AtEntryImplMixin.kt +++ b/src/main/kotlin/platform/mcp/at/psi/mixins/impl/AtEntryImplMixin.kt @@ -24,9 +24,17 @@ import com.demonwav.mcdev.platform.mcp.at.AtElementFactory import com.demonwav.mcdev.platform.mcp.at.psi.mixins.AtEntryMixin import com.intellij.extapi.psi.ASTWrapperPsiElement import com.intellij.lang.ASTNode +import com.intellij.psi.PsiComment +import com.intellij.psi.util.PsiTreeUtil abstract class AtEntryImplMixin(node: ASTNode) : ASTWrapperPsiElement(node), AtEntryMixin { + override val comment: PsiComment? + get() = PsiTreeUtil.skipWhitespacesForward(this) as? PsiComment + + override val commentText: String? + get() = comment?.text?.substring(1) + override fun setEntry(entry: String) { replace(AtElementFactory.createEntry(project, entry)) } @@ -53,4 +61,20 @@ abstract class AtEntryImplMixin(node: ASTNode) : ASTWrapperPsiElement(node), AtE val asterisk = AtElementFactory.createAsterisk(project) replaceMember(asterisk) } + + override fun setComment(text: String?) { + if (text == null) { + comment?.delete() + return + } + + val newComment = AtElementFactory.createComment(project, text) + val existingComment = comment + if (existingComment == null) { + parent.addAfter(newComment, this) + return + } + + existingComment.replace(newComment) + } } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 678b37fa8..eccc2c673 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -627,6 +627,7 @@ implementationClass="com.demonwav.mcdev.platform.mcp.at.completion.AtCompletionContributor"/> + From 5b12e00f75e36a8c851988916b1ae6371e86c18f Mon Sep 17 00:00:00 2001 From: RedNesto Date: Tue, 27 Aug 2024 13:51:59 +0200 Subject: [PATCH 07/20] Fix ktlint errors --- src/main/kotlin/platform/mcp/at/AtUsageInspection.kt | 4 ---- src/test/kotlin/platform/mcp/at/AtReferencesTest.kt | 4 +++- src/test/kotlin/platform/mcp/at/AtUsageInspectionTest.kt | 3 ++- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/platform/mcp/at/AtUsageInspection.kt b/src/main/kotlin/platform/mcp/at/AtUsageInspection.kt index 797d053c5..5d8bf90b3 100644 --- a/src/main/kotlin/platform/mcp/at/AtUsageInspection.kt +++ b/src/main/kotlin/platform/mcp/at/AtUsageInspection.kt @@ -46,10 +46,6 @@ class AtUsageInspection : LocalInspectionTool() { return "Reports unused Access Transformer entries" } - override fun isSuppressedFor(element: PsiElement): Boolean { - return super.isSuppressedFor(element) - } - override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { return object : PsiElementVisitor() { override fun visitElement(element: PsiElement) { diff --git a/src/test/kotlin/platform/mcp/at/AtReferencesTest.kt b/src/test/kotlin/platform/mcp/at/AtReferencesTest.kt index d0ba8c2f7..5d00519f0 100644 --- a/src/test/kotlin/platform/mcp/at/AtReferencesTest.kt +++ b/src/test/kotlin/platform/mcp/at/AtReferencesTest.kt @@ -33,7 +33,9 @@ import com.intellij.psi.PsiField import com.intellij.psi.PsiMethod import com.intellij.psi.PsiPackage import org.intellij.lang.annotations.Language -import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertInstanceOf +import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test diff --git a/src/test/kotlin/platform/mcp/at/AtUsageInspectionTest.kt b/src/test/kotlin/platform/mcp/at/AtUsageInspectionTest.kt index b232febeb..753f1db0b 100644 --- a/src/test/kotlin/platform/mcp/at/AtUsageInspectionTest.kt +++ b/src/test/kotlin/platform/mcp/at/AtUsageInspectionTest.kt @@ -79,7 +79,8 @@ class AtUsageInspectionTest : BaseMinecraftTest(PlatformType.MCP, PlatformType.N public net.minecraft.Used usedMethod()V public net.minecraft.Used unusedMethod()V public net.minecraft.server.Unused - """.trimIndent()) + """.trimIndent() + ) } // Force 1.20.2 because we test the non-SRG member names with NeoForge From 12664ede2086c3c12a71dd15fec470a47c005167 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Tue, 27 Aug 2024 14:58:35 +0200 Subject: [PATCH 08/20] Add AT unresolved inspection --- .../mcp/at/AtUnresolvedReferenceInspection.kt | 48 +++++++++++++++++++ src/main/resources/META-INF/plugin.xml | 7 +++ 2 files changed, 55 insertions(+) create mode 100644 src/main/kotlin/platform/mcp/at/AtUnresolvedReferenceInspection.kt diff --git a/src/main/kotlin/platform/mcp/at/AtUnresolvedReferenceInspection.kt b/src/main/kotlin/platform/mcp/at/AtUnresolvedReferenceInspection.kt new file mode 100644 index 000000000..0aec1d1b0 --- /dev/null +++ b/src/main/kotlin/platform/mcp/at/AtUnresolvedReferenceInspection.kt @@ -0,0 +1,48 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.mcp.at + +import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtVisitor +import com.intellij.codeInspection.LocalInspectionTool +import com.intellij.codeInspection.ProblemHighlightType +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiElementVisitor + +class AtUnresolvedReferenceInspection : LocalInspectionTool() { + + override fun getStaticDescription(): String? = "Unresolved reference" + + override fun buildVisitor( + holder: ProblemsHolder, + isOnTheFly: Boolean + ): PsiElementVisitor = object : AtVisitor() { + override fun visitElement(element: PsiElement) { + super.visitElement(element) + + for (reference in element.references) { + if (reference.resolve() == null) { + holder.registerProblem(reference, ProblemHighlightType.LIKE_UNKNOWN_SYMBOL) + } + } + } + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index eccc2c673..df9d81e61 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -996,6 +996,13 @@ editorAttributes="NOT_USED_ELEMENT_ATTRIBUTES" hasStaticDescription="true" implementationClass="com.demonwav.mcdev.platform.mcp.at.AtUsageInspection"/> + Date: Tue, 27 Aug 2024 16:30:26 +0200 Subject: [PATCH 09/20] Add AT duplicate entry inspection --- changelog.md | 4 ++ .../mcp/at/AtDuplicateEntryInspection.kt | 49 ++++++++++++++ .../platform/mcp/at/AtUsageInspection.kt | 38 +---------- .../platform/mcp/at/RemoveAtEntryFix.kt | 62 +++++++++++++++++ .../mcp/at/psi/mixins/AtEntryMixin.kt | 2 + .../at/psi/mixins/impl/AtEntryImplMixin.kt | 6 ++ src/main/resources/META-INF/plugin.xml | 8 +++ .../mcp/at/AtDuplicateEntryInspectionTest.kt | 67 +++++++++++++++++++ 8 files changed, 199 insertions(+), 37 deletions(-) create mode 100644 src/main/kotlin/platform/mcp/at/AtDuplicateEntryInspection.kt create mode 100644 src/main/kotlin/platform/mcp/at/RemoveAtEntryFix.kt create mode 100644 src/test/kotlin/platform/mcp/at/AtDuplicateEntryInspectionTest.kt diff --git a/changelog.md b/changelog.md index 702d21a82..85e5b0c03 100644 --- a/changelog.md +++ b/changelog.md @@ -12,6 +12,10 @@ - many lexing errors should now be fixed - class names and member names now have their own references, replacing the custom Goto handler - SRG names are no longer used on NeoForge 1.20.2+ and a new copy action is available for it + - the usage inspection no longer incorrectly reports methods overridden in your code or entries covering super methods + - suppressing inspections is now possible by adding `# Suppress:AtInspectionName` after an entry, or using the built-in suppress action + - added an inspection to report unresolved references, to help find out old, superfluous entries + - added an inspection to report duplicate entries in the same file ## [1.8.1] - 2024-08-10 diff --git a/src/main/kotlin/platform/mcp/at/AtDuplicateEntryInspection.kt b/src/main/kotlin/platform/mcp/at/AtDuplicateEntryInspection.kt new file mode 100644 index 000000000..52bde2633 --- /dev/null +++ b/src/main/kotlin/platform/mcp/at/AtDuplicateEntryInspection.kt @@ -0,0 +1,49 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.mcp.at + +import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtEntry +import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtVisitor +import com.demonwav.mcdev.util.childrenOfType +import com.intellij.codeInspection.LocalInspectionTool +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.psi.PsiElementVisitor + +class AtDuplicateEntryInspection : LocalInspectionTool() { + + override fun getStaticDescription(): String? = "Reports duplicate AT entries in the same file" + + override fun buildVisitor( + holder: ProblemsHolder, + isOnTheFly: Boolean + ): PsiElementVisitor = object : AtVisitor() { + + override fun visitEntry(entry: AtEntry) { + // Either a MemberReference or the class name text for class-level entries + val entryMemberReference = entry.memberReference ?: entry.className.text + val allMemberReferences = entry.containingFile.childrenOfType() + .map { it.memberReference ?: it.className.text } + if (allMemberReferences.count { it == entryMemberReference } > 1) { + holder.registerProblem(entry, "Duplicate entry", RemoveAtEntryFix.forWholeLine(entry, false)) + } + } + } +} diff --git a/src/main/kotlin/platform/mcp/at/AtUsageInspection.kt b/src/main/kotlin/platform/mcp/at/AtUsageInspection.kt index 5d8bf90b3..5fab48416 100644 --- a/src/main/kotlin/platform/mcp/at/AtUsageInspection.kt +++ b/src/main/kotlin/platform/mcp/at/AtUsageInspection.kt @@ -21,24 +21,16 @@ package com.demonwav.mcdev.platform.mcp.at import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtEntry -import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtTypes import com.demonwav.mcdev.util.excludeFileTypes import com.intellij.codeInspection.LocalInspectionTool -import com.intellij.codeInspection.LocalQuickFixOnPsiElement import com.intellij.codeInspection.ProblemsHolder -import com.intellij.codeInspection.util.IntentionFamilyName -import com.intellij.codeInspection.util.IntentionName -import com.intellij.openapi.project.Project import com.intellij.psi.PsiClass import com.intellij.psi.PsiElement import com.intellij.psi.PsiElementVisitor -import com.intellij.psi.PsiFile import com.intellij.psi.PsiMethod import com.intellij.psi.search.GlobalSearchScope import com.intellij.psi.search.searches.OverridingMethodsSearch import com.intellij.psi.search.searches.ReferencesSearch -import com.intellij.psi.util.elementType -import com.intellij.psi.util.siblings class AtUsageInspection : LocalInspectionTool() { @@ -119,37 +111,9 @@ class AtUsageInspection : LocalInspectionTool() { } } - val fix = RemoveAtEntryFix.forWholeLine(entry) + val fix = RemoveAtEntryFix.forWholeLine(entry, true) holder.registerProblem(entry, "Access Transformer entry is never used", fix) } } } - - private class RemoveAtEntryFix(startElement: PsiElement, endElement: PsiElement) : - LocalQuickFixOnPsiElement(startElement, endElement) { - - override fun getFamilyName(): @IntentionFamilyName String = "Remove entry" - - override fun getText(): @IntentionName String = familyName - - override fun invoke( - project: Project, - file: PsiFile, - startElement: PsiElement, - endElement: PsiElement - ) { - startElement.parent.deleteChildRange(startElement, endElement) - } - - companion object { - - fun forWholeLine(entry: AtEntry): RemoveAtEntryFix { - val start = entry.siblings(forward = false, withSelf = false) - .firstOrNull { it.elementType == AtTypes.CRLF }?.nextSibling - val end = entry.siblings(forward = true, withSelf = true) - .firstOrNull { it.elementType == AtTypes.CRLF } - return RemoveAtEntryFix(start ?: entry, end ?: entry) - } - } - } } diff --git a/src/main/kotlin/platform/mcp/at/RemoveAtEntryFix.kt b/src/main/kotlin/platform/mcp/at/RemoveAtEntryFix.kt new file mode 100644 index 000000000..9aabe329d --- /dev/null +++ b/src/main/kotlin/platform/mcp/at/RemoveAtEntryFix.kt @@ -0,0 +1,62 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.mcp.at + +import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtEntry +import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtTypes +import com.intellij.codeInspection.LocalQuickFixOnPsiElement +import com.intellij.codeInspection.util.IntentionFamilyName +import com.intellij.codeInspection.util.IntentionName +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.util.elementType +import com.intellij.psi.util.siblings + +class RemoveAtEntryFix(startElement: PsiElement, endElement: PsiElement, val inBatchMode: Boolean) : + LocalQuickFixOnPsiElement(startElement, endElement) { + + override fun getFamilyName(): @IntentionFamilyName String = "Remove entry" + + override fun getText(): @IntentionName String = familyName + + override fun invoke( + project: Project, + file: PsiFile, + startElement: PsiElement, + endElement: PsiElement + ) { + startElement.parent.deleteChildRange(startElement, endElement) + } + + override fun availableInBatchMode(): Boolean = inBatchMode + + companion object { + + fun forWholeLine(entry: AtEntry, inBatchMode: Boolean): RemoveAtEntryFix { + val start = entry.siblings(forward = false, withSelf = false) + .firstOrNull { it.elementType == AtTypes.CRLF }?.nextSibling + val end = entry.siblings(forward = true, withSelf = true) + .firstOrNull { it.elementType == AtTypes.CRLF } + return RemoveAtEntryFix(start ?: entry, end ?: entry, inBatchMode) + } + } +} diff --git a/src/main/kotlin/platform/mcp/at/psi/mixins/AtEntryMixin.kt b/src/main/kotlin/platform/mcp/at/psi/mixins/AtEntryMixin.kt index 874b377ac..12d01ef95 100644 --- a/src/main/kotlin/platform/mcp/at/psi/mixins/AtEntryMixin.kt +++ b/src/main/kotlin/platform/mcp/at/psi/mixins/AtEntryMixin.kt @@ -27,6 +27,7 @@ import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtFieldName import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtFunction import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtKeyword import com.demonwav.mcdev.platform.mcp.at.psi.AtElement +import com.demonwav.mcdev.util.MemberReference import com.intellij.psi.PsiComment interface AtEntryMixin : AtElement { @@ -38,6 +39,7 @@ interface AtEntryMixin : AtElement { val keyword: AtKeyword val comment: PsiComment? val commentText: String? + val memberReference: MemberReference? fun setEntry(entry: String) fun setKeyword(keyword: AtElementFactory.Keyword) diff --git a/src/main/kotlin/platform/mcp/at/psi/mixins/impl/AtEntryImplMixin.kt b/src/main/kotlin/platform/mcp/at/psi/mixins/impl/AtEntryImplMixin.kt index 888e49748..a8a8cfbbc 100644 --- a/src/main/kotlin/platform/mcp/at/psi/mixins/impl/AtEntryImplMixin.kt +++ b/src/main/kotlin/platform/mcp/at/psi/mixins/impl/AtEntryImplMixin.kt @@ -21,7 +21,10 @@ package com.demonwav.mcdev.platform.mcp.at.psi.mixins.impl import com.demonwav.mcdev.platform.mcp.at.AtElementFactory +import com.demonwav.mcdev.platform.mcp.at.AtMemberReference +import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtEntry import com.demonwav.mcdev.platform.mcp.at.psi.mixins.AtEntryMixin +import com.demonwav.mcdev.util.MemberReference import com.intellij.extapi.psi.ASTWrapperPsiElement import com.intellij.lang.ASTNode import com.intellij.psi.PsiComment @@ -35,6 +38,9 @@ abstract class AtEntryImplMixin(node: ASTNode) : ASTWrapperPsiElement(node), AtE override val commentText: String? get() = comment?.text?.substring(1) + override val memberReference: MemberReference? + get() = (function ?: fieldName ?: asterisk)?.let { AtMemberReference.get(this as AtEntry, it) } + override fun setEntry(entry: String) { replace(AtElementFactory.createEntry(project, entry)) } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index df9d81e61..a0486813e 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -1003,6 +1003,14 @@ level="ERROR" hasStaticDescription="true" implementationClass="com.demonwav.mcdev.platform.mcp.at.AtUnresolvedReferenceInspection"/> + . + */ + +package com.demonwav.mcdev.platform.mcp.at + +import com.demonwav.mcdev.framework.BaseMinecraftTest +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("Access Transformer Duplicate Entry Inspection") +class AtDuplicateEntryInspectionTest : BaseMinecraftTest() { + + @Test + @DisplayName("Duplicate Entries") + fun duplicateEntries() { + buildProject { + at( + "test_at.cfg", + """ + public test.value.UniqueClass + public test.value.DuplicateClass + public test.value.DuplicateClass + + public test.value.UniqueClass * + public test.value.DuplicateClass * + public test.value.DuplicateClass * + + public test.value.UniqueClass *() + public test.value.DuplicateClass *() + public test.value.DuplicateClass *() + + public test.value.UniqueClass field + public test.value.DuplicateClass field + public test.value.DuplicateClass field + + public test.value.UniqueClass method()V + public test.value.DuplicateClass method()V + public test.value.DuplicateClass method()V + + public test.value.UniqueClass method(II)V + public test.value.DuplicateClass method(II)V + public test.value.DuplicateClass method(II)V + """.trimIndent() + ) + } + + fixture.enableInspections(AtDuplicateEntryInspection::class.java) + fixture.checkHighlighting() + } +} From 5cea0df508bfdc7ff9aa8ef5ba3d09021419e633 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Wed, 28 Aug 2024 18:02:54 +0200 Subject: [PATCH 10/20] Support suppressing AT inspections for the whole file Also fixes AtParserDefinition#spaceExistenceTypeBetweenTokens --- src/main/kotlin/platform/mcp/at/AtFile.kt | 34 +++++ .../platform/mcp/at/AtInspectionSuppressor.kt | 66 ++++++++- .../platform/mcp/at/AtParserDefinition.kt | 16 +-- src/test/kotlin/framework/test-util.kt | 14 ++ .../mcp/at/AtDuplicateEntryInspectionTest.kt | 2 +- .../mcp/at/AtInspectionSuppressorTest.kt | 131 ++++++++++++++++++ 6 files changed, 247 insertions(+), 16 deletions(-) create mode 100644 src/test/kotlin/platform/mcp/at/AtInspectionSuppressorTest.kt diff --git a/src/main/kotlin/platform/mcp/at/AtFile.kt b/src/main/kotlin/platform/mcp/at/AtFile.kt index d99d7666c..b1980e3cc 100644 --- a/src/main/kotlin/platform/mcp/at/AtFile.kt +++ b/src/main/kotlin/platform/mcp/at/AtFile.kt @@ -23,10 +23,13 @@ package com.demonwav.mcdev.platform.mcp.at import com.demonwav.mcdev.asset.PlatformAssets import com.demonwav.mcdev.facet.MinecraftFacet import com.demonwav.mcdev.platform.mcp.McpModuleType +import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtEntry import com.intellij.extapi.psi.PsiFileBase import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.module.ModuleUtilCore import com.intellij.psi.FileViewProvider +import com.intellij.psi.PsiComment +import com.intellij.psi.PsiElement class AtFile(viewProvider: FileViewProvider) : PsiFileBase(viewProvider, AtLanguage) { @@ -34,6 +37,37 @@ class AtFile(viewProvider: FileViewProvider) : PsiFileBase(viewProvider, AtLangu setup() } + val headComments: List + get() { + val comments = mutableListOf() + for (child in children) { + if (child is AtEntry) { + break + } + + if (child is PsiComment) { + comments.add(child) + } + } + + return comments + } + + fun addHeadComment(text: String) { + val toAdd = text.lines().flatMap { listOf(AtElementFactory.createComment(project, it)) } + val lastHeadComment = headComments.lastOrNull() + if (lastHeadComment == null) { + for (comment in toAdd.reversed()) { + addAfter(comment, null) + } + } else { + var previousComment: PsiElement? = lastHeadComment + for (comment in toAdd) { + previousComment = addAfter(comment, previousComment) + } + } + } + private fun setup() { if (ApplicationManager.getApplication().isUnitTestMode) { return diff --git a/src/main/kotlin/platform/mcp/at/AtInspectionSuppressor.kt b/src/main/kotlin/platform/mcp/at/AtInspectionSuppressor.kt index 9419c5069..8e7d885de 100644 --- a/src/main/kotlin/platform/mcp/at/AtInspectionSuppressor.kt +++ b/src/main/kotlin/platform/mcp/at/AtInspectionSuppressor.kt @@ -29,14 +29,26 @@ import com.intellij.codeInspection.util.IntentionName import com.intellij.openapi.project.Project import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile +import com.intellij.psi.util.PsiTreeUtil import com.intellij.psi.util.parentOfType class AtInspectionSuppressor : InspectionSuppressor { override fun isSuppressedFor(element: PsiElement, toolId: String): Boolean { val entry = element.parentOfType(withSelf = true) ?: return false - val comment = entry.commentText ?: return false - val suppressed = comment.substringAfter("Suppress:").substringBefore(' ').split(',') + val entryComment = entry.commentText + if (entryComment != null) { + if (isSuppressing(entryComment, toolId)) { + return true + } + } + + val file = element.containingFile as AtFile + return file.headComments.any { comment -> isSuppressing(comment.text, toolId) } + } + + private fun isSuppressing(entryComment: String, toolId: String): Boolean { + val suppressed = entryComment.substringAfter("Suppress:").substringBefore(' ').split(',') return toolId in suppressed } @@ -48,12 +60,24 @@ class AtInspectionSuppressor : InspectionSuppressor { return SuppressQuickFix.EMPTY_ARRAY } - return arrayOf(AtSuppressQuickFix(element, toolId)) + val entry = element as? AtEntry + ?: element.parentOfType(withSelf = true) + ?: PsiTreeUtil.getPrevSiblingOfType(element, AtEntry::class.java) // For when we are at a CRLF + return if (entry != null) { + arrayOf(AtSuppressQuickFix(entry, toolId), AtSuppressQuickFix(element.containingFile, toolId)) + } else { + arrayOf(AtSuppressQuickFix(element.containingFile, toolId)) + } } - class AtSuppressQuickFix(element: PsiElement, val toolId: String) : LocalQuickFixOnPsiElement(element), SuppressQuickFix { + class AtSuppressQuickFix(element: PsiElement, val toolId: String) : + LocalQuickFixOnPsiElement(element), SuppressQuickFix { - override fun getText(): @IntentionName String = "Suppress $toolId" + override fun getText(): @IntentionName String = when (startElement) { + is AtEntry -> "Suppress $toolId for entry" + is AtFile -> "Suppress $toolId for file" + else -> "Suppress $toolId" + } override fun getFamilyName(): @IntentionFamilyName String = "Suppress inspection" @@ -63,7 +87,13 @@ class AtInspectionSuppressor : InspectionSuppressor { startElement: PsiElement, endElement: PsiElement ) { - val entry = startElement.parentOfType(withSelf = true) ?: return + when (startElement) { + is AtEntry -> suppressForEntry(startElement) + is AtFile -> suppressForFile(startElement) + } + } + + private fun suppressForEntry(entry: AtEntry) { val commentText = entry.commentText?.trim() if (commentText == null) { entry.setComment("Suppress:$toolId") @@ -77,10 +107,32 @@ class AtInspectionSuppressor : InspectionSuppressor { } val suppressEnd = commentText.indexOf(' ', suppressStart).takeUnless { it == -1 } ?: commentText.length - val newComment = commentText.substring(suppressStart, suppressEnd) + ",$toolId" + commentText.substring(suppressEnd) + val newComment = + commentText.substring(suppressStart, suppressEnd) + ",$toolId" + commentText.substring(suppressEnd) entry.setComment(newComment) } + private fun suppressForFile(file: AtFile) { + val existingSuppressComment = file.headComments.firstOrNull { it.text.contains("Suppress:") } + if (existingSuppressComment == null) { + file.addHeadComment("Suppress:$toolId") + return + } + + val commentText = existingSuppressComment.text + val suppressStart = commentText.indexOf("Suppress:") + if (suppressStart == -1) { + file.addHeadComment("Suppress:$toolId") + return + } + + val suppressEnd = commentText.indexOf(' ', suppressStart).takeUnless { it == -1 } ?: commentText.length + val newCommentText = + commentText.substring(suppressStart, suppressEnd) + ",$toolId" + commentText.substring(suppressEnd) + val newComment = AtElementFactory.createComment(file.project, newCommentText) + existingSuppressComment.replace(newComment) + } + override fun isAvailable( project: Project, context: PsiElement diff --git a/src/main/kotlin/platform/mcp/at/AtParserDefinition.kt b/src/main/kotlin/platform/mcp/at/AtParserDefinition.kt index 135fba57c..dedee341e 100644 --- a/src/main/kotlin/platform/mcp/at/AtParserDefinition.kt +++ b/src/main/kotlin/platform/mcp/at/AtParserDefinition.kt @@ -43,8 +43,7 @@ class AtParserDefinition : ParserDefinition { override fun createElement(node: ASTNode): PsiElement = AtTypes.Factory.createElement(node) override fun spaceExistenceTypeBetweenTokens(left: ASTNode, right: ASTNode) = - map.entries.firstOrNull { e -> left.elementType == e.key.first || right.elementType == e.key.second }?.value - ?: ParserDefinition.SpaceRequirements.MUST_NOT + map[left.elementType to right.elementType] ?: ParserDefinition.SpaceRequirements.MUST_NOT companion object { private val COMMENTS = TokenSet.create(AtTypes.COMMENT) @@ -52,13 +51,14 @@ class AtParserDefinition : ParserDefinition { private val FILE = IFileElementType(Language.findInstance(AtLanguage::class.java)) private val map: Map, ParserDefinition.SpaceRequirements> = mapOf( - (AtTypes.KEYWORD to AtTypes.CLASS_NAME) to ParserDefinition.SpaceRequirements.MUST, - (AtTypes.CLASS_NAME to AtTypes.FIELD_NAME) to ParserDefinition.SpaceRequirements.MUST, - (AtTypes.CLASS_NAME to AtTypes.FUNCTION) to ParserDefinition.SpaceRequirements.MUST, - (AtTypes.CLASS_NAME to AtTypes.ASTERISK) to ParserDefinition.SpaceRequirements.MUST, + (AtTypes.KEYWORD_ELEMENT to AtTypes.CLASS_NAME_ELEMENT) to ParserDefinition.SpaceRequirements.MUST, + (AtTypes.CLASS_NAME_ELEMENT to AtTypes.FIELD_NAME) to ParserDefinition.SpaceRequirements.MUST, + (AtTypes.CLASS_NAME_ELEMENT to AtTypes.FUNCTION) to ParserDefinition.SpaceRequirements.MUST, + (AtTypes.CLASS_NAME_ELEMENT to AtTypes.ASTERISK_ELEMENT) to ParserDefinition.SpaceRequirements.MUST, + (AtTypes.CLASS_NAME_ELEMENT to AtTypes.COMMENT) to ParserDefinition.SpaceRequirements.MUST, (AtTypes.FIELD_NAME to AtTypes.COMMENT) to ParserDefinition.SpaceRequirements.MUST, - (AtTypes.ASTERISK to AtTypes.COMMENT) to ParserDefinition.SpaceRequirements.MUST, - (AtTypes.COMMENT to AtTypes.KEYWORD) to ParserDefinition.SpaceRequirements.MUST_LINE_BREAK, + (AtTypes.ASTERISK_ELEMENT to AtTypes.COMMENT) to ParserDefinition.SpaceRequirements.MUST, + (AtTypes.COMMENT to AtTypes.KEYWORD_ELEMENT) to ParserDefinition.SpaceRequirements.MUST_LINE_BREAK, (AtTypes.COMMENT to AtTypes.COMMENT) to ParserDefinition.SpaceRequirements.MUST_LINE_BREAK, (AtTypes.FUNCTION to AtTypes.COMMENT) to ParserDefinition.SpaceRequirements.MUST, ) diff --git a/src/test/kotlin/framework/test-util.kt b/src/test/kotlin/framework/test-util.kt index 08a2b66a1..e365d1287 100644 --- a/src/test/kotlin/framework/test-util.kt +++ b/src/test/kotlin/framework/test-util.kt @@ -24,6 +24,7 @@ package com.demonwav.mcdev.framework import com.intellij.ide.highlighter.JavaFileType import com.intellij.lexer.Lexer +import com.intellij.openapi.fileTypes.FileType import com.intellij.openapi.project.Project import com.intellij.openapi.roots.OrderRootType import com.intellij.openapi.roots.libraries.Library @@ -133,6 +134,19 @@ fun testInspectionFix(fixture: JavaCodeInsightTestFixture, basePath: String, fix fixture.checkResult(expected) } +fun testInspectionFix( + fixture: JavaCodeInsightTestFixture, + fixName: String, + fileType: FileType, + before: String, + after: String +) { + fixture.configureByText(fileType, before) + val intention = fixture.findSingleIntention(fixName) + fixture.launchAction(intention) + fixture.checkResult(after) +} + fun assertEqualsUnordered(expected: Collection, actual: Collection) { val expectedSet = expected.toSet() val actualSet = actual.toSet() diff --git a/src/test/kotlin/platform/mcp/at/AtDuplicateEntryInspectionTest.kt b/src/test/kotlin/platform/mcp/at/AtDuplicateEntryInspectionTest.kt index 3340c0c26..92a7397ba 100644 --- a/src/test/kotlin/platform/mcp/at/AtDuplicateEntryInspectionTest.kt +++ b/src/test/kotlin/platform/mcp/at/AtDuplicateEntryInspectionTest.kt @@ -24,7 +24,7 @@ import com.demonwav.mcdev.framework.BaseMinecraftTest import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test -@DisplayName("Access Transformer Duplicate Entry Inspection") +@DisplayName("Access Transformer Duplicate Entry Inspection Tests") class AtDuplicateEntryInspectionTest : BaseMinecraftTest() { @Test diff --git a/src/test/kotlin/platform/mcp/at/AtInspectionSuppressorTest.kt b/src/test/kotlin/platform/mcp/at/AtInspectionSuppressorTest.kt new file mode 100644 index 000000000..3ec497fa0 --- /dev/null +++ b/src/test/kotlin/platform/mcp/at/AtInspectionSuppressorTest.kt @@ -0,0 +1,131 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.mcp.at + +import com.demonwav.mcdev.framework.BaseMinecraftTest +import com.demonwav.mcdev.framework.testInspectionFix +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("Access Transformer Inspection Suppressor Tests") +class AtInspectionSuppressorTest : BaseMinecraftTest() { + + @Test + @DisplayName("Entry-Level Suppress") + fun entryLevelSuppress() { + fixture.configureByText( + "test_at.cfg", + """ + public Unresolved # Suppress:AtUnresolvedReference + public Unresolved + """.trimIndent() + ) + + fixture.enableInspections(AtUnresolvedReferenceInspection::class.java) + fixture.checkHighlighting() + } + + @Test + @DisplayName("Entry-Level Suppress Fix") + fun entryLevelSuppressFix() { + fixture.enableInspections(AtUnresolvedReferenceInspection::class.java) + testInspectionFix( + fixture, + "Suppress AtUnresolvedReference for entry", + AtFileType, + "public Unresolved", + "public Unresolved # Suppress:AtUnresolvedReference" + ) + } + + @Test + @DisplayName("File-Level Suppress") + fun fileLevelSuppress() { + fixture.configureByText( + "test_at.cfg", + """ + # Suppress:AtUnresolvedReference + public Unresolved + public Unresolved + """.trimIndent() + ) + + fixture.enableInspections(AtUnresolvedReferenceInspection::class.java) + fixture.checkHighlighting() + } + + @Test + @DisplayName("File-Level Suppress Fix With No Existing Comments") + fun fileLevelSuppressFixNoComments() { + fixture.enableInspections(AtUnresolvedReferenceInspection::class.java) + testInspectionFix( + fixture, + "Suppress AtUnresolvedReference for file", + AtFileType, + "public Unresolved", + """ + # Suppress:AtUnresolvedReference + public Unresolved + """.trimIndent() + ) + } + + @Test + @DisplayName("File-Level Suppress Fix With Unrelated Comment") + fun fileLevelSuppressFixWithUnrelatedComment() { + fixture.enableInspections(AtUnresolvedReferenceInspection::class.java) + testInspectionFix( + fixture, + "Suppress AtUnresolvedReference for file", + AtFileType, + """ + # This is a header comment + public Unresolved + """.trimIndent(), + """ + # This is a header comment + # Suppress:AtUnresolvedReference + public Unresolved + """.trimIndent() + ) + } + + @Test + @DisplayName("File-Level Suppress Fix With Existing Suppress") + fun fileLevelSuppressFixWithExistingSuppress() { + fixture.enableInspections(AtUnresolvedReferenceInspection::class.java) + testInspectionFix( + fixture, + "Suppress AtUnresolvedReference for file", + AtFileType, + """ + # This is a header comment + # Suppress:AtUsage + public Unresolved + """.trimIndent(), + """ + # This is a header comment + # Suppress:AtUsage,AtUnresolvedReference + public Unresolved + """.trimIndent() + ) + } +} From d8ecab0362bc9adb1d40d30ddb0b0d9afd979a7f Mon Sep 17 00:00:00 2001 From: RedNesto Date: Wed, 28 Aug 2024 21:32:55 +0200 Subject: [PATCH 11/20] Add AT formatter --- .../kotlin/platform/mcp/at/format/AtBlock.kt | 104 ++++++++++++++++++ .../mcp/at/format/AtCodeStyleSettings.kt | 97 ++++++++++++++++ .../mcp/at/format/AtFormattingModelBuilder.kt | 65 +++++++++++ src/main/resources/META-INF/plugin.xml | 4 +- .../kotlin/platform/mcp/at/AtFormatterTest.kt | 93 ++++++++++++++++ 5 files changed, 362 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/platform/mcp/at/format/AtBlock.kt create mode 100644 src/main/kotlin/platform/mcp/at/format/AtCodeStyleSettings.kt create mode 100644 src/main/kotlin/platform/mcp/at/format/AtFormattingModelBuilder.kt create mode 100644 src/test/kotlin/platform/mcp/at/AtFormatterTest.kt diff --git a/src/main/kotlin/platform/mcp/at/format/AtBlock.kt b/src/main/kotlin/platform/mcp/at/format/AtBlock.kt new file mode 100644 index 000000000..8b46576f6 --- /dev/null +++ b/src/main/kotlin/platform/mcp/at/format/AtBlock.kt @@ -0,0 +1,104 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.mcp.at.format + +import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtTypes +import com.intellij.formatting.Alignment +import com.intellij.formatting.Block +import com.intellij.formatting.Indent +import com.intellij.formatting.Spacing +import com.intellij.formatting.SpacingBuilder +import com.intellij.formatting.Wrap +import com.intellij.formatting.WrapType +import com.intellij.lang.ASTNode +import com.intellij.lang.tree.util.children +import com.intellij.psi.TokenType +import com.intellij.psi.codeStyle.CodeStyleSettings +import com.intellij.psi.formatter.common.AbstractBlock +import com.intellij.psi.tree.IFileElementType + +class AtBlock( + node: ASTNode, + wrap: Wrap?, + alignment: Alignment?, + val spacingBuilder: SpacingBuilder, + val codeStyleSettings: CodeStyleSettings, + val entryClassAlignment: Alignment? = null, + val entryMemberAlignment: Alignment? = null, +) : AbstractBlock(node, wrap, alignment) { + + override fun buildChildren(): List { + val blocks = mutableListOf() + + var entryClassAlignment: Alignment? = entryClassAlignment + var entryMemberAlignment: Alignment? = entryMemberAlignment + + var newlineCount = 0 + val alignGroups = node.elementType is IFileElementType && + codeStyleSettings.getCustomSettings(AtCodeStyleSettings::class.java).ALIGN_ENTRY_CLASS_AND_MEMBER + for (child in node.children()) { + val childType = child.elementType + if (childType == TokenType.WHITE_SPACE) { + continue + } + + if (alignGroups) { + if (childType == AtTypes.CRLF) { + newlineCount++ + continue + } else if (childType != AtTypes.COMMENT) { + if (newlineCount >= 2) { + // Align different groups separately, comments are not counted towards any group + entryClassAlignment = Alignment.createAlignment(true) + entryMemberAlignment = Alignment.createAlignment(true) + } + newlineCount = 0 + } + } + + val alignment = when (childType) { + AtTypes.CLASS_NAME -> entryClassAlignment + AtTypes.FIELD_NAME, AtTypes.FUNCTION, AtTypes.ASTERISK -> entryMemberAlignment + else -> null + } + + blocks.add( + AtBlock( + child, + Wrap.createWrap(WrapType.NONE, false), + alignment, + spacingBuilder, + codeStyleSettings, + entryClassAlignment, + entryMemberAlignment + ) + ) + } + + return blocks + } + + override fun getIndent(): Indent? = Indent.getNoneIndent() + + override fun getSpacing(child1: Block?, child2: Block): Spacing? = spacingBuilder.getSpacing(this, child1, child2) + + override fun isLeaf(): Boolean = node.firstChildNode == null +} diff --git a/src/main/kotlin/platform/mcp/at/format/AtCodeStyleSettings.kt b/src/main/kotlin/platform/mcp/at/format/AtCodeStyleSettings.kt new file mode 100644 index 000000000..a4af706f0 --- /dev/null +++ b/src/main/kotlin/platform/mcp/at/format/AtCodeStyleSettings.kt @@ -0,0 +1,97 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.mcp.at.format + +import com.demonwav.mcdev.platform.mcp.at.AtLanguage +import com.intellij.application.options.CodeStyleAbstractConfigurable +import com.intellij.application.options.CodeStyleAbstractPanel +import com.intellij.application.options.TabbedLanguageCodeStylePanel +import com.intellij.lang.Language +import com.intellij.openapi.util.NlsContexts +import com.intellij.psi.codeStyle.CodeStyleConfigurable +import com.intellij.psi.codeStyle.CodeStyleSettings +import com.intellij.psi.codeStyle.CodeStyleSettingsCustomizable +import com.intellij.psi.codeStyle.CodeStyleSettingsProvider +import com.intellij.psi.codeStyle.CustomCodeStyleSettings +import com.intellij.psi.codeStyle.LanguageCodeStyleSettingsProvider + +class AtCodeStyleSettings(val settings: CodeStyleSettings) : CustomCodeStyleSettings("AtCodeStyleSettings", settings) { + @JvmField + var SPACE_BEFORE_ENTRY_COMMENT = true + + @JvmField + var ALIGN_ENTRY_CLASS_AND_MEMBER = true +} + +class AtCodeStyleSettingsProvider : CodeStyleSettingsProvider() { + override fun createCustomSettings(settings: CodeStyleSettings): CustomCodeStyleSettings = + AtCodeStyleSettings(settings) + + override fun getConfigurableDisplayName(): @NlsContexts.ConfigurableName String? = AtLanguage.displayName + + override fun createConfigurable( + settings: CodeStyleSettings, + modelSettings: CodeStyleSettings + ): CodeStyleConfigurable { + return object : CodeStyleAbstractConfigurable(settings, modelSettings, configurableDisplayName) { + override fun createPanel(settings: CodeStyleSettings): CodeStyleAbstractPanel { + return AtCodeStyleSettingsConfigurable(currentSettings, settings) + } + } + } +} + +class AtCodeStyleSettingsConfigurable(currentSettings: CodeStyleSettings, settings: CodeStyleSettings) : + TabbedLanguageCodeStylePanel(AtLanguage, currentSettings, settings) + +class AtLanguageCodeStyleSettingsProvider : LanguageCodeStyleSettingsProvider() { + + override fun getLanguage(): Language = AtLanguage + + override fun customizeSettings(consumer: CodeStyleSettingsCustomizable, settingsType: SettingsType) { + if (settingsType == SettingsType.SPACING_SETTINGS) { + consumer.showCustomOption( + AtCodeStyleSettings::class.java, + "SPACE_BEFORE_ENTRY_COMMENT", + "Space before entry comment", + "Spacing and alignment" + ) + consumer.showCustomOption( + AtCodeStyleSettings::class.java, + "ALIGN_ENTRY_CLASS_AND_MEMBER", + "Align entry class name and member", + "Spacing and alignment" + ) + } + } + + override fun getCodeSample(settingsType: SettingsType): String? = """ + # Some header comment + + public net.minecraft.client.Minecraft pickBlock()V# This is an entry comment + public net.minecraft.client.Minecraft userProperties()Lcom/mojang/authlib/minecraft/UserApiService${'$'}UserProperties; + + # Each group can be aligned independently + protected net.minecraft.client.gui.screens.inventory.AbstractContainerScreen clickedSlot + protected-f net.minecraft.client.gui.screens.inventory.AbstractContainerScreen playerInventoryTitle + protected net.minecraft.client.gui.screens.inventory.AbstractContainerScreen findSlot(DD)Lnet/minecraft/world/inventory/Slot; + """.trimIndent() +} diff --git a/src/main/kotlin/platform/mcp/at/format/AtFormattingModelBuilder.kt b/src/main/kotlin/platform/mcp/at/format/AtFormattingModelBuilder.kt new file mode 100644 index 000000000..f3a942fed --- /dev/null +++ b/src/main/kotlin/platform/mcp/at/format/AtFormattingModelBuilder.kt @@ -0,0 +1,65 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.mcp.at.format + +import com.demonwav.mcdev.platform.mcp.at.AtLanguage +import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtTypes +import com.intellij.formatting.Alignment +import com.intellij.formatting.FormattingContext +import com.intellij.formatting.FormattingModel +import com.intellij.formatting.FormattingModelBuilder +import com.intellij.formatting.FormattingModelProvider +import com.intellij.formatting.SpacingBuilder +import com.intellij.formatting.Wrap +import com.intellij.formatting.WrapType +import com.intellij.psi.codeStyle.CodeStyleSettings + +class AtFormattingModelBuilder : FormattingModelBuilder { + + private fun createSpaceBuilder(settings: CodeStyleSettings): SpacingBuilder { + val atSettings = settings.getCustomSettings(AtCodeStyleSettings::class.java) + return SpacingBuilder(settings, AtLanguage) + .between(AtTypes.ENTRY, AtTypes.COMMENT).spaceIf(atSettings.SPACE_BEFORE_ENTRY_COMMENT) + // Removes alignment spaces if it is disabled + .between(AtTypes.KEYWORD, AtTypes.CLASS_NAME).spaces(1) + .between(AtTypes.CLASS_NAME, AtTypes.FIELD_NAME).spaces(1) + .between(AtTypes.CLASS_NAME, AtTypes.FUNCTION).spaces(1) + .between(AtTypes.CLASS_NAME, AtTypes.ASTERISK).spaces(1) + } + + override fun createModel(formattingContext: FormattingContext): FormattingModel { + val codeStyleSettings = formattingContext.codeStyleSettings + val rootBlock = AtBlock( + formattingContext.node, + Wrap.createWrap(WrapType.NONE, false), + Alignment.createAlignment(), + createSpaceBuilder(codeStyleSettings), + codeStyleSettings, + Alignment.createAlignment(true), + Alignment.createAlignment(true), + ) + return FormattingModelProvider.createFormattingModelForPsiFile( + formattingContext.containingFile, + rootBlock, + codeStyleSettings + ) + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index a0486813e..607e1b62d 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -622,10 +622,12 @@ + + + - diff --git a/src/test/kotlin/platform/mcp/at/AtFormatterTest.kt b/src/test/kotlin/platform/mcp/at/AtFormatterTest.kt new file mode 100644 index 000000000..f9d6c5255 --- /dev/null +++ b/src/test/kotlin/platform/mcp/at/AtFormatterTest.kt @@ -0,0 +1,93 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.mcp.at + +import com.demonwav.mcdev.framework.BaseMinecraftTest +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.psi.codeStyle.CodeStyleManager +import org.intellij.lang.annotations.Language +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("Access Transformer Tests") +class AtFormatterTest : BaseMinecraftTest() { + + private fun doTest( + @Language("Access Transformers") before: String, + @Language("Access Transformers") after: String, + ) { + + fixture.configureByText(AtFileType, before) + WriteCommandAction.runWriteCommandAction(fixture.project) { + CodeStyleManager.getInstance(project).reformat(fixture.file) + } + + fixture.checkResult(after) + } + + @Test + @DisplayName("Entry Comment Spacing") + fun entryCommentSpacing() { + doTest("public Test field# A comment", "public Test field # A comment") + } + + @Test + @DisplayName("Single Group Alignment") + fun singleGroupAlignment() { + doTest( + """ + public Test field # A comment + public+f AnotherTest method()V + """.trimIndent(), + """ + public Test field # A comment + public+f AnotherTest method()V + """.trimIndent() + ) + } + + @Test + @DisplayName("Multiple Groups Alignments") + fun multipleGroupsAlignments() { + doTest( + """ + public net.minecraft.Group1A field + protected net.minecraft.Group1BCD method()V + + public net.minecraft.server.Group2A anotherField + public-f net.minecraft.server.Group2BCD someMethod()V + # A comment in the middle should not join the two groups + protected net.minecraft.world.Group3A anotherField + protected-f net.minecraft.world.Group2BCD someMethod()V + """.trimIndent(), + """ + public net.minecraft.Group1A field + protected net.minecraft.Group1BCD method()V + + public net.minecraft.server.Group2A anotherField + public-f net.minecraft.server.Group2BCD someMethod()V + # A comment in the middle should not join the two groups + protected net.minecraft.world.Group3A anotherField + protected-f net.minecraft.world.Group2BCD someMethod()V + """.trimIndent() + ) + } +} From 299c451c55844666540086215c23e7734c035b67 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Thu, 29 Aug 2024 01:35:08 +0200 Subject: [PATCH 12/20] Small cleanup --- .../mcp/actions/CopyNeoForgeAtAction.kt | 13 +------ .../platform/mcp/at/AtReferenceContributor.kt | 29 +++++---------- src/main/kotlin/platform/mcp/at/at-utils.kt | 37 +++++++++++++++++++ 3 files changed, 49 insertions(+), 30 deletions(-) create mode 100644 src/main/kotlin/platform/mcp/at/at-utils.kt diff --git a/src/main/kotlin/platform/mcp/actions/CopyNeoForgeAtAction.kt b/src/main/kotlin/platform/mcp/actions/CopyNeoForgeAtAction.kt index 11e42e23f..0eda2f8ff 100644 --- a/src/main/kotlin/platform/mcp/actions/CopyNeoForgeAtAction.kt +++ b/src/main/kotlin/platform/mcp/actions/CopyNeoForgeAtAction.kt @@ -20,13 +20,10 @@ package com.demonwav.mcdev.platform.mcp.actions -import com.demonwav.mcdev.platform.mcp.McpModuleType import com.demonwav.mcdev.platform.mcp.actions.SrgActionBase.Companion.showBalloon import com.demonwav.mcdev.platform.mcp.actions.SrgActionBase.Companion.showSuccessBalloon +import com.demonwav.mcdev.platform.mcp.at.usesSrgMemberNames import com.demonwav.mcdev.platform.mixin.handlers.ShadowHandler -import com.demonwav.mcdev.platform.neoforge.NeoForgeModuleType -import com.demonwav.mcdev.util.MinecraftVersions -import com.demonwav.mcdev.util.SemanticVersion import com.demonwav.mcdev.util.descriptor import com.demonwav.mcdev.util.getDataFromActionEvent import com.intellij.openapi.actionSystem.ActionUpdateThread @@ -52,13 +49,7 @@ class CopyNeoForgeAtAction : AnAction() { private fun isAvailable(e: AnActionEvent): Boolean { val data = getDataFromActionEvent(e) ?: return false - if (!data.instance.isOfType(NeoForgeModuleType)) { - return false - } - - val mcpModule = data.instance.getModuleOfType(McpModuleType) ?: return false - val mcVersion = mcpModule.getSettings().minecraftVersion?.let(SemanticVersion::tryParse) ?: return false - return mcVersion >= MinecraftVersions.MC1_20_2 + return !data.instance.usesSrgMemberNames() } override fun actionPerformed(e: AnActionEvent) { diff --git a/src/main/kotlin/platform/mcp/at/AtReferenceContributor.kt b/src/main/kotlin/platform/mcp/at/AtReferenceContributor.kt index 95195d441..d7f9638e6 100644 --- a/src/main/kotlin/platform/mcp/at/AtReferenceContributor.kt +++ b/src/main/kotlin/platform/mcp/at/AtReferenceContributor.kt @@ -27,10 +27,7 @@ import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtEntry import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtFieldName import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtFunction import com.demonwav.mcdev.platform.mcp.at.psi.AtElement -import com.demonwav.mcdev.platform.neoforge.NeoForgeModuleType import com.demonwav.mcdev.util.MemberReference -import com.demonwav.mcdev.util.MinecraftVersions -import com.demonwav.mcdev.util.SemanticVersion import com.demonwav.mcdev.util.findMethods import com.demonwav.mcdev.util.findModule import com.demonwav.mcdev.util.findQualifiedClass @@ -177,12 +174,12 @@ abstract class AtClassMemberReference(element: E, range: TextRang val entry = element.parent as? AtEntry ?: return ArrayUtil.EMPTY_OBJECT_ARRAY val module = element.findModule() ?: return ArrayUtil.EMPTY_OBJECT_ARRAY - val instance = MinecraftFacet.getInstance(module) - val mcpModule = instance?.getModuleOfType(McpModuleType) ?: return ArrayUtil.EMPTY_OBJECT_ARRAY - val isNeoForge = instance.isOfType(NeoForgeModuleType) - val (mapField, mapMethod) = if (isNeoForge) { + val instance = MinecraftFacet.getInstance(module) ?: return ArrayUtil.EMPTY_OBJECT_ARRAY + val useSrg = instance.usesSrgMemberNames() + val (mapField, mapMethod) = if (!useSrg) { { it: PsiField -> it.memberReference } to { it: PsiMethod -> it.memberReference } } else { + val mcpModule = instance.getModuleOfType(McpModuleType)!! val srgMap = mcpModule.mappingsManager?.mappingsNow ?: return ArrayUtil.EMPTY_OBJECT_ARRAY { it: PsiField -> srgMap.getIntermediaryField(it) } to { it: PsiMethod -> srgMap.getIntermediaryMethod(it) } } @@ -197,8 +194,8 @@ abstract class AtClassMemberReference(element: E, range: TextRang .withPsiElement(field) .withPresentableText(field.name) .withIcon(PlatformIcons.FIELD_ICON) - .withTailText(" (${memberReference.name})".takeUnless { isNeoForge }, true) - .withInsertHandler(AtClassMemberInsertionHandler(field.name.takeUnless { isNeoForge })) + .withTailText(" (${memberReference.name})".takeIf { useSrg }, true) + .withInsertHandler(AtClassMemberInsertionHandler(field.name.takeIf { useSrg })) results.add(PrioritizedLookupElement.withPriority(lookupElement, 1.0)) } @@ -209,8 +206,8 @@ abstract class AtClassMemberReference(element: E, range: TextRang .withPsiElement(method) .withPresentableText(method.nameAndParameterTypes) .withIcon(PlatformIcons.METHOD_ICON) - .withTailText(" (${memberReference.name})".takeUnless { isNeoForge }, true) - .withInsertHandler(AtClassMemberInsertionHandler(method.name.takeUnless { isNeoForge })) + .withTailText(" (${memberReference.name})".takeIf { useSrg }, true) + .withInsertHandler(AtClassMemberInsertionHandler(method.name.takeIf { useSrg })) results.add(PrioritizedLookupElement.withPriority(lookupElement, 0.0)) } @@ -237,10 +234,7 @@ class AtFieldNameReference(element: AtFieldName) : val instance = MinecraftFacet.getInstance(module) ?: return null val mcpModule = instance.getModuleOfType(McpModuleType) ?: return null - return if (instance.isOfType(NeoForgeModuleType) && - mcpModule.getSettings().minecraftVersion?.let(SemanticVersion::tryParse) - ?.let { it >= MinecraftVersions.MC1_20_2 } == true - ) { + return if (!instance.usesSrgMemberNames()) { entryClass.findFieldByName(element.text, false) } else { val srgMap = mcpModule.mappingsManager?.mappingsNow ?: return null @@ -278,10 +272,7 @@ class AtFuncNameReference(element: AtFunction) : val instance = MinecraftFacet.getInstance(module) ?: return null val mcpModule = instance.getModuleOfType(McpModuleType) ?: return null - return if (instance.isOfType(NeoForgeModuleType) && - mcpModule.getSettings().minecraftVersion?.let(SemanticVersion::tryParse) - ?.let { it >= MinecraftVersions.MC1_20_2 } == true - ) { + return if (!instance.usesSrgMemberNames()) { val memberReference = MemberReference.parse(element.text) ?: return null entryClass.findMethods(memberReference).firstOrNull() } else { diff --git a/src/main/kotlin/platform/mcp/at/at-utils.kt b/src/main/kotlin/platform/mcp/at/at-utils.kt new file mode 100644 index 000000000..f28f84066 --- /dev/null +++ b/src/main/kotlin/platform/mcp/at/at-utils.kt @@ -0,0 +1,37 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.mcp.at + +import com.demonwav.mcdev.facet.MinecraftFacet +import com.demonwav.mcdev.platform.mcp.McpModuleType +import com.demonwav.mcdev.platform.neoforge.NeoForgeModuleType +import com.demonwav.mcdev.util.MinecraftVersions +import com.demonwav.mcdev.util.SemanticVersion + +fun MinecraftFacet.usesSrgMemberNames(): Boolean { + if (!this.isOfType(NeoForgeModuleType)) { + return true + } + + val mcpModule = this.getModuleOfType(McpModuleType) ?: return true + val mcVersion = mcpModule.getSettings().minecraftVersion?.let(SemanticVersion::tryParse) ?: return true + return mcVersion < MinecraftVersions.MC1_20_2 +} From 471d67df0df4982dae800eac1f111686aed0d983 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Thu, 29 Aug 2024 01:48:09 +0200 Subject: [PATCH 13/20] Move AT inspection-related stuff into its own package --- .../at/{ => inspections}/AtDuplicateEntryInspection.kt | 2 +- .../mcp/at/{ => inspections}/AtInspectionSuppressor.kt | 4 +++- .../{ => inspections}/AtUnresolvedReferenceInspection.kt | 2 +- .../mcp/at/{ => inspections}/AtUsageInspection.kt | 3 ++- .../platform/mcp/at/{ => inspections}/RemoveAtEntryFix.kt | 2 +- src/main/resources/META-INF/plugin.xml | 8 ++++---- .../{ => inspections}/AtDuplicateEntryInspectionTest.kt | 2 +- .../at/{ => inspections}/AtInspectionSuppressorTest.kt | 3 ++- .../mcp/at/{ => inspections}/AtUsageInspectionTest.kt | 2 +- 9 files changed, 16 insertions(+), 12 deletions(-) rename src/main/kotlin/platform/mcp/at/{ => inspections}/AtDuplicateEntryInspection.kt (97%) rename src/main/kotlin/platform/mcp/at/{ => inspections}/AtInspectionSuppressor.kt (97%) rename src/main/kotlin/platform/mcp/at/{ => inspections}/AtUnresolvedReferenceInspection.kt (96%) rename src/main/kotlin/platform/mcp/at/{ => inspections}/AtUsageInspection.kt (97%) rename src/main/kotlin/platform/mcp/at/{ => inspections}/RemoveAtEntryFix.kt (97%) rename src/test/kotlin/platform/mcp/at/{ => inspections}/AtDuplicateEntryInspectionTest.kt (98%) rename src/test/kotlin/platform/mcp/at/{ => inspections}/AtInspectionSuppressorTest.kt (97%) rename src/test/kotlin/platform/mcp/at/{ => inspections}/AtUsageInspectionTest.kt (98%) diff --git a/src/main/kotlin/platform/mcp/at/AtDuplicateEntryInspection.kt b/src/main/kotlin/platform/mcp/at/inspections/AtDuplicateEntryInspection.kt similarity index 97% rename from src/main/kotlin/platform/mcp/at/AtDuplicateEntryInspection.kt rename to src/main/kotlin/platform/mcp/at/inspections/AtDuplicateEntryInspection.kt index 52bde2633..a4c4160b1 100644 --- a/src/main/kotlin/platform/mcp/at/AtDuplicateEntryInspection.kt +++ b/src/main/kotlin/platform/mcp/at/inspections/AtDuplicateEntryInspection.kt @@ -18,7 +18,7 @@ * along with this program. If not, see . */ -package com.demonwav.mcdev.platform.mcp.at +package com.demonwav.mcdev.platform.mcp.at.inspections import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtEntry import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtVisitor diff --git a/src/main/kotlin/platform/mcp/at/AtInspectionSuppressor.kt b/src/main/kotlin/platform/mcp/at/inspections/AtInspectionSuppressor.kt similarity index 97% rename from src/main/kotlin/platform/mcp/at/AtInspectionSuppressor.kt rename to src/main/kotlin/platform/mcp/at/inspections/AtInspectionSuppressor.kt index 8e7d885de..eca8fe115 100644 --- a/src/main/kotlin/platform/mcp/at/AtInspectionSuppressor.kt +++ b/src/main/kotlin/platform/mcp/at/inspections/AtInspectionSuppressor.kt @@ -18,8 +18,10 @@ * along with this program. If not, see . */ -package com.demonwav.mcdev.platform.mcp.at +package com.demonwav.mcdev.platform.mcp.at.inspections +import com.demonwav.mcdev.platform.mcp.at.AtElementFactory +import com.demonwav.mcdev.platform.mcp.at.AtFile import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtEntry import com.intellij.codeInspection.InspectionSuppressor import com.intellij.codeInspection.LocalQuickFixOnPsiElement diff --git a/src/main/kotlin/platform/mcp/at/AtUnresolvedReferenceInspection.kt b/src/main/kotlin/platform/mcp/at/inspections/AtUnresolvedReferenceInspection.kt similarity index 96% rename from src/main/kotlin/platform/mcp/at/AtUnresolvedReferenceInspection.kt rename to src/main/kotlin/platform/mcp/at/inspections/AtUnresolvedReferenceInspection.kt index 0aec1d1b0..80d9bbe3e 100644 --- a/src/main/kotlin/platform/mcp/at/AtUnresolvedReferenceInspection.kt +++ b/src/main/kotlin/platform/mcp/at/inspections/AtUnresolvedReferenceInspection.kt @@ -18,7 +18,7 @@ * along with this program. If not, see . */ -package com.demonwav.mcdev.platform.mcp.at +package com.demonwav.mcdev.platform.mcp.at.inspections import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtVisitor import com.intellij.codeInspection.LocalInspectionTool diff --git a/src/main/kotlin/platform/mcp/at/AtUsageInspection.kt b/src/main/kotlin/platform/mcp/at/inspections/AtUsageInspection.kt similarity index 97% rename from src/main/kotlin/platform/mcp/at/AtUsageInspection.kt rename to src/main/kotlin/platform/mcp/at/inspections/AtUsageInspection.kt index 5fab48416..50145254a 100644 --- a/src/main/kotlin/platform/mcp/at/AtUsageInspection.kt +++ b/src/main/kotlin/platform/mcp/at/inspections/AtUsageInspection.kt @@ -18,8 +18,9 @@ * along with this program. If not, see . */ -package com.demonwav.mcdev.platform.mcp.at +package com.demonwav.mcdev.platform.mcp.at.inspections +import com.demonwav.mcdev.platform.mcp.at.AtFileType import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtEntry import com.demonwav.mcdev.util.excludeFileTypes import com.intellij.codeInspection.LocalInspectionTool diff --git a/src/main/kotlin/platform/mcp/at/RemoveAtEntryFix.kt b/src/main/kotlin/platform/mcp/at/inspections/RemoveAtEntryFix.kt similarity index 97% rename from src/main/kotlin/platform/mcp/at/RemoveAtEntryFix.kt rename to src/main/kotlin/platform/mcp/at/inspections/RemoveAtEntryFix.kt index 9aabe329d..0221a1495 100644 --- a/src/main/kotlin/platform/mcp/at/RemoveAtEntryFix.kt +++ b/src/main/kotlin/platform/mcp/at/inspections/RemoveAtEntryFix.kt @@ -18,7 +18,7 @@ * along with this program. If not, see . */ -package com.demonwav.mcdev.platform.mcp.at +package com.demonwav.mcdev.platform.mcp.at.inspections import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtEntry import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtTypes diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 607e1b62d..075fea717 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -629,7 +629,7 @@ - + @@ -997,14 +997,14 @@ level="WARNING" editorAttributes="NOT_USED_ELEMENT_ATTRIBUTES" hasStaticDescription="true" - implementationClass="com.demonwav.mcdev.platform.mcp.at.AtUsageInspection"/> + implementationClass="com.demonwav.mcdev.platform.mcp.at.inspections.AtUsageInspection"/> + implementationClass="com.demonwav.mcdev.platform.mcp.at.inspections.AtUnresolvedReferenceInspection"/> + implementationClass="com.demonwav.mcdev.platform.mcp.at.inspections.AtDuplicateEntryInspection"/> . */ -package com.demonwav.mcdev.platform.mcp.at +package com.demonwav.mcdev.platform.mcp.at.inspections import com.demonwav.mcdev.framework.BaseMinecraftTest import org.junit.jupiter.api.DisplayName diff --git a/src/test/kotlin/platform/mcp/at/AtInspectionSuppressorTest.kt b/src/test/kotlin/platform/mcp/at/inspections/AtInspectionSuppressorTest.kt similarity index 97% rename from src/test/kotlin/platform/mcp/at/AtInspectionSuppressorTest.kt rename to src/test/kotlin/platform/mcp/at/inspections/AtInspectionSuppressorTest.kt index 3ec497fa0..8b8385fa1 100644 --- a/src/test/kotlin/platform/mcp/at/AtInspectionSuppressorTest.kt +++ b/src/test/kotlin/platform/mcp/at/inspections/AtInspectionSuppressorTest.kt @@ -18,10 +18,11 @@ * along with this program. If not, see . */ -package com.demonwav.mcdev.platform.mcp.at +package com.demonwav.mcdev.platform.mcp.at.inspections import com.demonwav.mcdev.framework.BaseMinecraftTest import com.demonwav.mcdev.framework.testInspectionFix +import com.demonwav.mcdev.platform.mcp.at.AtFileType import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test diff --git a/src/test/kotlin/platform/mcp/at/AtUsageInspectionTest.kt b/src/test/kotlin/platform/mcp/at/inspections/AtUsageInspectionTest.kt similarity index 98% rename from src/test/kotlin/platform/mcp/at/AtUsageInspectionTest.kt rename to src/test/kotlin/platform/mcp/at/inspections/AtUsageInspectionTest.kt index 753f1db0b..0d32728ac 100644 --- a/src/test/kotlin/platform/mcp/at/AtUsageInspectionTest.kt +++ b/src/test/kotlin/platform/mcp/at/inspections/AtUsageInspectionTest.kt @@ -18,7 +18,7 @@ * along with this program. If not, see . */ -package com.demonwav.mcdev.platform.mcp.at +package com.demonwav.mcdev.platform.mcp.at.inspections import com.demonwav.mcdev.facet.MinecraftFacet import com.demonwav.mcdev.framework.BaseMinecraftTest From 7cafab765a0d93293f31f769fd75a79b36797298 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Thu, 29 Aug 2024 16:04:02 +0200 Subject: [PATCH 14/20] Tweak AtUnresolvedReferenceInspection description --- .../mcp/at/inspections/AtUnresolvedReferenceInspection.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/platform/mcp/at/inspections/AtUnresolvedReferenceInspection.kt b/src/main/kotlin/platform/mcp/at/inspections/AtUnresolvedReferenceInspection.kt index 80d9bbe3e..9fa0dfe3f 100644 --- a/src/main/kotlin/platform/mcp/at/inspections/AtUnresolvedReferenceInspection.kt +++ b/src/main/kotlin/platform/mcp/at/inspections/AtUnresolvedReferenceInspection.kt @@ -29,7 +29,7 @@ import com.intellij.psi.PsiElementVisitor class AtUnresolvedReferenceInspection : LocalInspectionTool() { - override fun getStaticDescription(): String? = "Unresolved reference" + override fun getStaticDescription(): String? = "Reports unresolved AT targets." override fun buildVisitor( holder: ProblemsHolder, From 78ebabed6e5c7915d526043c19a66e0c1fb4ba91 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Thu, 29 Aug 2024 16:42:33 +0200 Subject: [PATCH 15/20] Remove unnecessary Alignment and Wrap objects Also fixes initial entry indent being 8 spaces instead of none --- src/main/kotlin/platform/mcp/at/format/AtBlock.kt | 5 +++-- .../platform/mcp/at/format/AtFormattingModelBuilder.kt | 6 ++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/platform/mcp/at/format/AtBlock.kt b/src/main/kotlin/platform/mcp/at/format/AtBlock.kt index 8b46576f6..e0d4e76e6 100644 --- a/src/main/kotlin/platform/mcp/at/format/AtBlock.kt +++ b/src/main/kotlin/platform/mcp/at/format/AtBlock.kt @@ -27,7 +27,6 @@ import com.intellij.formatting.Indent import com.intellij.formatting.Spacing import com.intellij.formatting.SpacingBuilder import com.intellij.formatting.Wrap -import com.intellij.formatting.WrapType import com.intellij.lang.ASTNode import com.intellij.lang.tree.util.children import com.intellij.psi.TokenType @@ -83,7 +82,7 @@ class AtBlock( blocks.add( AtBlock( child, - Wrap.createWrap(WrapType.NONE, false), + null, alignment, spacingBuilder, codeStyleSettings, @@ -98,6 +97,8 @@ class AtBlock( override fun getIndent(): Indent? = Indent.getNoneIndent() + override fun getChildIndent(): Indent? = Indent.getNoneIndent() + override fun getSpacing(child1: Block?, child2: Block): Spacing? = spacingBuilder.getSpacing(this, child1, child2) override fun isLeaf(): Boolean = node.firstChildNode == null diff --git a/src/main/kotlin/platform/mcp/at/format/AtFormattingModelBuilder.kt b/src/main/kotlin/platform/mcp/at/format/AtFormattingModelBuilder.kt index f3a942fed..a15db9f86 100644 --- a/src/main/kotlin/platform/mcp/at/format/AtFormattingModelBuilder.kt +++ b/src/main/kotlin/platform/mcp/at/format/AtFormattingModelBuilder.kt @@ -28,8 +28,6 @@ import com.intellij.formatting.FormattingModel import com.intellij.formatting.FormattingModelBuilder import com.intellij.formatting.FormattingModelProvider import com.intellij.formatting.SpacingBuilder -import com.intellij.formatting.Wrap -import com.intellij.formatting.WrapType import com.intellij.psi.codeStyle.CodeStyleSettings class AtFormattingModelBuilder : FormattingModelBuilder { @@ -49,8 +47,8 @@ class AtFormattingModelBuilder : FormattingModelBuilder { val codeStyleSettings = formattingContext.codeStyleSettings val rootBlock = AtBlock( formattingContext.node, - Wrap.createWrap(WrapType.NONE, false), - Alignment.createAlignment(), + null, + null, createSpaceBuilder(codeStyleSettings), codeStyleSettings, Alignment.createAlignment(true), From 48da17b86f55a1723025c1d081ddb63eb26e8f04 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Thu, 29 Aug 2024 16:51:39 +0200 Subject: [PATCH 16/20] Update changelog --- changelog.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 85e5b0c03..e27312e42 100644 --- a/changelog.md +++ b/changelog.md @@ -13,9 +13,10 @@ - class names and member names now have their own references, replacing the custom Goto handler - SRG names are no longer used on NeoForge 1.20.2+ and a new copy action is available for it - the usage inspection no longer incorrectly reports methods overridden in your code or entries covering super methods - - suppressing inspections is now possible by adding `# Suppress:AtInspectionName` after an entry, or using the built-in suppress action + - suppressing inspections is now possible by adding `# Suppress:AtInspectionName` after an entry or at the start of the file, or using the built-in suppress action - added an inspection to report unresolved references, to help find out old, superfluous entries - added an inspection to report duplicate entries in the same file + - added formatting support, class and member names are configured to align by default ## [1.8.1] - 2024-08-10 From 5713a123794756d70c8269c41c7205817b7a4612 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Thu, 29 Aug 2024 16:56:34 +0200 Subject: [PATCH 17/20] Fix AtLexer import --- src/main/grammars/AtLexer.flex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/grammars/AtLexer.flex b/src/main/grammars/AtLexer.flex index 6ffc9d78f..fb5e522eb 100644 --- a/src/main/grammars/AtLexer.flex +++ b/src/main/grammars/AtLexer.flex @@ -21,7 +21,7 @@ package com.demonwav.mcdev.platform.mcp.at.gen; import com.intellij.lexer.*; -import com.intellij.psi.TokenType;import com.intellij.psi.tree.IElementType; +import com.intellij.psi.tree.IElementType; import static com.demonwav.mcdev.platform.mcp.at.gen.psi.AtTypes.*; import static com.intellij.psi.TokenType.*; From f53706e5cff7bf75dd31f90629ea62107e6e990a Mon Sep 17 00:00:00 2001 From: RedNesto Date: Thu, 29 Aug 2024 18:15:28 +0200 Subject: [PATCH 18/20] Fix compiler errors --- .../kotlin/platform/mcp/at/format/AtBlock.kt | 2 +- src/main/kotlin/util/ast-utils.kt | 25 +++++++++++++++++++ .../platform/mcp/at/AtCompletionTest.kt | 2 +- 3 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/util/ast-utils.kt diff --git a/src/main/kotlin/platform/mcp/at/format/AtBlock.kt b/src/main/kotlin/platform/mcp/at/format/AtBlock.kt index e0d4e76e6..8fc9748be 100644 --- a/src/main/kotlin/platform/mcp/at/format/AtBlock.kt +++ b/src/main/kotlin/platform/mcp/at/format/AtBlock.kt @@ -21,6 +21,7 @@ package com.demonwav.mcdev.platform.mcp.at.format import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtTypes +import com.demonwav.mcdev.util.children import com.intellij.formatting.Alignment import com.intellij.formatting.Block import com.intellij.formatting.Indent @@ -28,7 +29,6 @@ import com.intellij.formatting.Spacing import com.intellij.formatting.SpacingBuilder import com.intellij.formatting.Wrap import com.intellij.lang.ASTNode -import com.intellij.lang.tree.util.children import com.intellij.psi.TokenType import com.intellij.psi.codeStyle.CodeStyleSettings import com.intellij.psi.formatter.common.AbstractBlock diff --git a/src/main/kotlin/util/ast-utils.kt b/src/main/kotlin/util/ast-utils.kt new file mode 100644 index 000000000..099c70cbf --- /dev/null +++ b/src/main/kotlin/util/ast-utils.kt @@ -0,0 +1,25 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.util + +import com.intellij.lang.ASTNode + +fun ASTNode.children(): Sequence = generateSequence(firstChildNode) { it.treeNext } diff --git a/src/test/kotlin/platform/mcp/at/AtCompletionTest.kt b/src/test/kotlin/platform/mcp/at/AtCompletionTest.kt index 329cff22e..6c6f08d8c 100644 --- a/src/test/kotlin/platform/mcp/at/AtCompletionTest.kt +++ b/src/test/kotlin/platform/mcp/at/AtCompletionTest.kt @@ -27,8 +27,8 @@ import com.demonwav.mcdev.platform.PlatformType import com.demonwav.mcdev.platform.mcp.McpModuleSettings import com.demonwav.mcdev.platform.mcp.McpModuleType import com.demonwav.mcdev.platform.mcp.at.AtElementFactory.Keyword -import com.demonwav.mcdev.util.runWriteActionAndWait import com.intellij.codeInsight.lookup.Lookup +import com.intellij.openapi.application.runWriteActionAndWait import org.intellij.lang.annotations.Language import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.BeforeEach From a3c33d309098694bb27adf7993fbecdc15ec43d8 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Sun, 22 Sep 2024 20:51:07 +0200 Subject: [PATCH 19/20] Fix multiline string indentation --- src/test/kotlin/framework/test-util.kt | 46 +++++++++++++------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/src/test/kotlin/framework/test-util.kt b/src/test/kotlin/framework/test-util.kt index e365d1287..984a427ca 100644 --- a/src/test/kotlin/framework/test-util.kt +++ b/src/test/kotlin/framework/test-util.kt @@ -155,37 +155,37 @@ fun assertEqualsUnordered(expected: Collection, actual: Collection) { if (notExpected.isNotEmpty() && notFound.isNotEmpty()) { val message = """| - |Expecting actual: - | $actual - |to contain exactly in any order: - | $expected - |elements not found: - | $notFound - |and elements not expected: - | $notExpected - """.trimMargin() + |Expecting actual: + | $actual + |to contain exactly in any order: + | $expected + |elements not found: + | $notFound + |and elements not expected: + | $notExpected + """.trimMargin() throw AssertionFailedError(message, expected, actual) } if (notFound.isNotEmpty()) { val message = """| - |Expecting actual: - | $actual - |to contain exactly in any order: - | $expected - |but could not find the following elements: - | $notFound - """.trimMargin() + |Expecting actual: + | $actual + |to contain exactly in any order: + | $expected + |but could not find the following elements: + | $notFound + """.trimMargin() throw AssertionFailedError(message, expected, actual) } if (notExpected.isNotEmpty()) { val message = """| - |Expecting actual: - | $actual - |to contain exactly in any order: - | $expected - |but the following elements were unexpected: - | $notExpected - """.trimMargin() + |Expecting actual: + | $actual + |to contain exactly in any order: + | $expected + |but the following elements were unexpected: + | $notExpected + """.trimMargin() throw AssertionFailedError(message, expected, actual) } } From c712627076cd29a549471ec9163df383de2c4e81 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Mon, 23 Sep 2024 11:36:32 +0200 Subject: [PATCH 20/20] Make usesSrgMemberNames return Boolean? --- .../kotlin/platform/mcp/actions/CopyNeoForgeAtAction.kt | 2 +- .../kotlin/platform/mcp/at/AtReferenceContributor.kt | 6 +++--- src/main/kotlin/platform/mcp/at/at-utils.kt | 9 +++++---- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/platform/mcp/actions/CopyNeoForgeAtAction.kt b/src/main/kotlin/platform/mcp/actions/CopyNeoForgeAtAction.kt index 0eda2f8ff..f89c9b183 100644 --- a/src/main/kotlin/platform/mcp/actions/CopyNeoForgeAtAction.kt +++ b/src/main/kotlin/platform/mcp/actions/CopyNeoForgeAtAction.kt @@ -49,7 +49,7 @@ class CopyNeoForgeAtAction : AnAction() { private fun isAvailable(e: AnActionEvent): Boolean { val data = getDataFromActionEvent(e) ?: return false - return !data.instance.usesSrgMemberNames() + return data.instance.usesSrgMemberNames() == false } override fun actionPerformed(e: AnActionEvent) { diff --git a/src/main/kotlin/platform/mcp/at/AtReferenceContributor.kt b/src/main/kotlin/platform/mcp/at/AtReferenceContributor.kt index d7f9638e6..6725921f3 100644 --- a/src/main/kotlin/platform/mcp/at/AtReferenceContributor.kt +++ b/src/main/kotlin/platform/mcp/at/AtReferenceContributor.kt @@ -175,7 +175,7 @@ abstract class AtClassMemberReference(element: E, range: TextRang val module = element.findModule() ?: return ArrayUtil.EMPTY_OBJECT_ARRAY val instance = MinecraftFacet.getInstance(module) ?: return ArrayUtil.EMPTY_OBJECT_ARRAY - val useSrg = instance.usesSrgMemberNames() + val useSrg = instance.usesSrgMemberNames() == true val (mapField, mapMethod) = if (!useSrg) { { it: PsiField -> it.memberReference } to { it: PsiMethod -> it.memberReference } } else { @@ -234,7 +234,7 @@ class AtFieldNameReference(element: AtFieldName) : val instance = MinecraftFacet.getInstance(module) ?: return null val mcpModule = instance.getModuleOfType(McpModuleType) ?: return null - return if (!instance.usesSrgMemberNames()) { + return if (instance.usesSrgMemberNames() != true) { entryClass.findFieldByName(element.text, false) } else { val srgMap = mcpModule.mappingsManager?.mappingsNow ?: return null @@ -272,7 +272,7 @@ class AtFuncNameReference(element: AtFunction) : val instance = MinecraftFacet.getInstance(module) ?: return null val mcpModule = instance.getModuleOfType(McpModuleType) ?: return null - return if (!instance.usesSrgMemberNames()) { + return if (instance.usesSrgMemberNames() != true) { val memberReference = MemberReference.parse(element.text) ?: return null entryClass.findMethods(memberReference).firstOrNull() } else { diff --git a/src/main/kotlin/platform/mcp/at/at-utils.kt b/src/main/kotlin/platform/mcp/at/at-utils.kt index f28f84066..dcfddd696 100644 --- a/src/main/kotlin/platform/mcp/at/at-utils.kt +++ b/src/main/kotlin/platform/mcp/at/at-utils.kt @@ -21,17 +21,18 @@ package com.demonwav.mcdev.platform.mcp.at import com.demonwav.mcdev.facet.MinecraftFacet +import com.demonwav.mcdev.platform.forge.ForgeModuleType import com.demonwav.mcdev.platform.mcp.McpModuleType import com.demonwav.mcdev.platform.neoforge.NeoForgeModuleType import com.demonwav.mcdev.util.MinecraftVersions import com.demonwav.mcdev.util.SemanticVersion -fun MinecraftFacet.usesSrgMemberNames(): Boolean { +fun MinecraftFacet.usesSrgMemberNames(): Boolean? { if (!this.isOfType(NeoForgeModuleType)) { - return true + return this.isOfType(ForgeModuleType) } - val mcpModule = this.getModuleOfType(McpModuleType) ?: return true - val mcVersion = mcpModule.getSettings().minecraftVersion?.let(SemanticVersion::tryParse) ?: return true + val mcpModule = this.getModuleOfType(McpModuleType) ?: return null + val mcVersion = mcpModule.getSettings().minecraftVersion?.let(SemanticVersion::tryParse) ?: return null return mcVersion < MinecraftVersions.MC1_20_2 }