From 1d436e0ada662327774d0874d712d048260944f3 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Sun, 25 Aug 2024 14:57:55 +0200 Subject: [PATCH 01/22] 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 02ae8368b873f97918649f3988dfc2f502bce0c4 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Mon, 26 Aug 2024 16:35:35 +0200 Subject: [PATCH 02/22] 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 6eeb565ef..3475ce5de 100644 --- a/changelog.md +++ b/changelog.md @@ -49,6 +49,12 @@ - [#1813](https://github.com/minecraft-dev/MinecraftDev/issues/1813) Single character Accessor targets aren't inferred correctly - [#1886](https://github.com/minecraft-dev/MinecraftDev/issues/1886) Sync error in ForgeGradle composite builds +### 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 1e860e1f1..efb06a4e4 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 fe850d872618882fee7b0a1b4728dedc29f89ac9 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Mon, 26 Aug 2024 16:45:00 +0200 Subject: [PATCH 03/22] 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 3475ce5de..96db29384 100644 --- a/changelog.md +++ b/changelog.md @@ -54,6 +54,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 d5656ed03e09e2f6dce384eb181b24f117b7dabc Mon Sep 17 00:00:00 2001 From: RedNesto Date: Tue, 27 Aug 2024 00:05:26 +0200 Subject: [PATCH 04/22] 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 be210bc31e2f368cb2a3d55d1e848ddafee7572e Mon Sep 17 00:00:00 2001 From: RedNesto Date: Tue, 27 Aug 2024 13:44:18 +0200 Subject: [PATCH 05/22] 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 efb06a4e4..64271d203 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -1000,6 +1000,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 5ef40feefb91f9dfebcf909d6ae26ef7fc40e48c Mon Sep 17 00:00:00 2001 From: RedNesto Date: Tue, 27 Aug 2024 13:45:03 +0200 Subject: [PATCH 06/22] 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 64271d203..858a63b09 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 cd02b5ca6f5ae12aa6de7b79e1a1d4e225985143 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Tue, 27 Aug 2024 13:51:59 +0200 Subject: [PATCH 07/22] 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 6ef22d41541640eae5c988e5677a13538ca5ba61 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Tue, 27 Aug 2024 14:58:35 +0200 Subject: [PATCH 08/22] 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 858a63b09..b6994e652 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -1004,6 +1004,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/22] 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 96db29384..ce0ab821f 100644 --- a/changelog.md +++ b/changelog.md @@ -55,6 +55,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 b6994e652..00a6c7ebc 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -1011,6 +1011,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 61a09d19557080ae1452deb74782828431502bb3 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Wed, 28 Aug 2024 18:02:54 +0200 Subject: [PATCH 10/22] 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 32b8d2889ca6d11fc3b9e3a238e12e7cb5e339f7 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Wed, 28 Aug 2024 21:32:55 +0200 Subject: [PATCH 11/22] 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 00a6c7ebc..0dcb7940a 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 0f61c16e1ff6d382fb5a6eb638265d4c537eac20 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Thu, 29 Aug 2024 01:35:08 +0200 Subject: [PATCH 12/22] 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 3425555580c62f18c40db85aed94172702e1b584 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Thu, 29 Aug 2024 01:48:09 +0200 Subject: [PATCH 13/22] 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 0dcb7940a..fd4009daf 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -629,7 +629,7 @@ - + @@ -1005,14 +1005,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 f64391dc773c74833e4319abc5ffe77df4fe319a Mon Sep 17 00:00:00 2001 From: RedNesto Date: Thu, 29 Aug 2024 16:04:02 +0200 Subject: [PATCH 14/22] 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 7f32a9faa2322fbb96fa21e60a02443d3c304f3c Mon Sep 17 00:00:00 2001 From: RedNesto Date: Thu, 29 Aug 2024 16:42:33 +0200 Subject: [PATCH 15/22] 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 6d8a443879705d338247d418bbd849d7129698a2 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Thu, 29 Aug 2024 16:51:39 +0200 Subject: [PATCH 16/22] Update changelog --- changelog.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index ce0ab821f..410a625ee 100644 --- a/changelog.md +++ b/changelog.md @@ -56,9 +56,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 658347a3712d04f103f77f3af1d79b0a4d6a3d3e Mon Sep 17 00:00:00 2001 From: RedNesto Date: Thu, 29 Aug 2024 16:56:34 +0200 Subject: [PATCH 17/22] 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 a79bbc704f1cd5c0c493320daca61c048be2f0bc Mon Sep 17 00:00:00 2001 From: RedNesto Date: Thu, 29 Aug 2024 18:15:28 +0200 Subject: [PATCH 18/22] 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 d043bba0ea57b5b3a9474650097cbcee1d978f7f Mon Sep 17 00:00:00 2001 From: RedNesto Date: Sun, 22 Sep 2024 20:51:07 +0200 Subject: [PATCH 19/22] 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 312831c033b3a51627ac6f29ce0ea435a6c42687 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Mon, 23 Sep 2024 11:36:32 +0200 Subject: [PATCH 20/22] 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 } From 63864d2f9a37cce1a4006b07a298f5ebd2b41a6c Mon Sep 17 00:00:00 2001 From: RedNesto Date: Mon, 16 Dec 2024 16:55:09 +0100 Subject: [PATCH 21/22] Enhance AW support too Pretty much identical to what was done to AT support --- changelog.md | 24 +-- src/main/grammars/AwLexer.flex | 7 +- src/main/grammars/AwParser.bnf | 48 +++--- .../FabricModJsonResolveScopeEnlarger.kt | 35 +++- src/main/kotlin/platform/mcp/at/AtFile.kt | 2 +- .../inspections/AtDuplicateEntryInspection.kt | 4 +- .../mcp/at/inspections/AtUsageInspection.kt | 116 +++++++------ .../kotlin/platform/mcp/aw/AwAnnotator.kt | 15 +- .../mcp/aw/AwCompletionContributor.kt | 16 +- .../platform/mcp/aw/AwElementFactory.kt | 68 ++++++++ src/main/kotlin/platform/mcp/aw/AwFile.kt | 39 ++++- .../platform/mcp/aw/AwParserDefinition.kt | 19 ++- .../mcp/aw/fixes/CreateAwHeaderFix.kt | 42 +++++ .../platform/mcp/aw/fixes/RemoveAwEntryFix.kt | 62 +++++++ .../kotlin/platform/mcp/aw/format/AwBlock.kt | 110 ++++++++++++ .../mcp/aw/format/AwCodeStyleSettings.kt | 97 +++++++++++ .../mcp/aw/format/AwFormattingModelBuilder.kt | 68 ++++++++ .../mcp/aw/inspections/AwHeaderInspection.kt | 56 ++++++ .../aw/inspections/AwInspectionSuppressor.kt | 146 ++++++++++++++++ .../AwUnresolvedReferenceInspection.kt | 48 ++++++ .../mcp/aw/inspections/AwUsageInspection.kt | 66 ++++++++ .../inspections/DuplicateAwEntryInspection.kt | 65 +++---- .../mcp/aw/psi/mixins/AwEntryMixin.kt | 9 +- .../psi/mixins/impl/AwClassEntryImplMixin.kt | 17 +- .../psi/mixins/impl/AwClassNameImplMixin.kt | 2 +- .../psi/mixins/impl/AwDescElementImplMixin.kt | 11 +- .../aw/psi/mixins/impl/AwEntryImplMixin.kt | 29 +++- .../psi/mixins/impl/AwFieldEntryImplMixin.kt | 16 ++ .../psi/mixins/impl/AwMemberNameImplMixin.kt | 42 +++-- .../psi/mixins/impl/AwMethodEntryImplMixin.kt | 17 ++ src/main/kotlin/util/utils.kt | 3 + src/main/resources/META-INF/plugin.xml | 27 +++ src/test/kotlin/framework/ProjectBuilder.kt | 6 + .../at/inspections/AtUsageInspectionTest.kt | 6 +- .../kotlin/platform/mcp/aw/AwCommenterTest.kt | 124 ++++++++++++++ .../platform/mcp/aw/AwCompletionTest.kt | 160 ++++++++++++++++++ .../kotlin/platform/mcp/aw/AwFormatterTest.kt | 97 +++++++++++ .../platform/mcp/aw/AwReferencesTest.kt | 133 +++++++++++++++ .../AwDuplicateEntryInspectionTest.kt | 59 +++++++ .../inspections/AwInspectionSuppressorTest.kt | 132 +++++++++++++++ .../aw/inspections/AwUsageInspectionTest.kt | 88 ++++++++++ 41 files changed, 1953 insertions(+), 178 deletions(-) create mode 100644 src/main/kotlin/platform/mcp/aw/AwElementFactory.kt create mode 100644 src/main/kotlin/platform/mcp/aw/fixes/CreateAwHeaderFix.kt create mode 100644 src/main/kotlin/platform/mcp/aw/fixes/RemoveAwEntryFix.kt create mode 100644 src/main/kotlin/platform/mcp/aw/format/AwBlock.kt create mode 100644 src/main/kotlin/platform/mcp/aw/format/AwCodeStyleSettings.kt create mode 100644 src/main/kotlin/platform/mcp/aw/format/AwFormattingModelBuilder.kt create mode 100644 src/main/kotlin/platform/mcp/aw/inspections/AwHeaderInspection.kt create mode 100644 src/main/kotlin/platform/mcp/aw/inspections/AwInspectionSuppressor.kt create mode 100644 src/main/kotlin/platform/mcp/aw/inspections/AwUnresolvedReferenceInspection.kt create mode 100644 src/main/kotlin/platform/mcp/aw/inspections/AwUsageInspection.kt create mode 100644 src/test/kotlin/platform/mcp/aw/AwCommenterTest.kt create mode 100644 src/test/kotlin/platform/mcp/aw/AwCompletionTest.kt create mode 100644 src/test/kotlin/platform/mcp/aw/AwFormatterTest.kt create mode 100644 src/test/kotlin/platform/mcp/aw/AwReferencesTest.kt create mode 100644 src/test/kotlin/platform/mcp/aw/inspections/AwDuplicateEntryInspectionTest.kt create mode 100644 src/test/kotlin/platform/mcp/aw/inspections/AwInspectionSuppressorTest.kt create mode 100644 src/test/kotlin/platform/mcp/aw/inspections/AwUsageInspectionTest.kt diff --git a/changelog.md b/changelog.md index 410a625ee..075016627 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,18 @@ ## [Unreleased] +### Changed + +- Overhauled Access Transformer and Access Widener 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 + - 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 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 + ### Added - [#2391](https://github.com/minecraft-dev/MinecraftDev/issues/2391) Project creator template repo and maven repo authorization @@ -49,18 +61,6 @@ - [#1813](https://github.com/minecraft-dev/MinecraftDev/issues/1813) Single character Accessor targets aren't inferred correctly - [#1886](https://github.com/minecraft-dev/MinecraftDev/issues/1886) Sync error in ForgeGradle composite builds -### 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 - - 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 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 ### Added diff --git a/src/main/grammars/AwLexer.flex b/src/main/grammars/AwLexer.flex index 0a9763ada..a047469b0 100644 --- a/src/main/grammars/AwLexer.flex +++ b/src/main/grammars/AwLexer.flex @@ -56,19 +56,23 @@ CLASS_ELEMENT=class METHOD_ELEMENT=method FIELD_ELEMENT=field NAME_ELEMENT=\w+| -CLASS_NAME_ELEMENT=(\w+\/)*\w+(\$\w+)* +CLASS_NAME_ELEMENT=[\w/$]+ COMMENT=#.* CRLF=\n|\r|\r\n WHITE_SPACE=\s %% +{COMMENT} { return COMMENT; } + { {HEADER_NAME} { yybegin(HEADER); return HEADER_NAME; } {ACCESS_ELEMENT} { return ACCESS_ELEMENT; } {CLASS_ELEMENT} { yybegin(CLASS_NAME); return CLASS_ELEMENT; } {METHOD_ELEMENT} { yybegin(CLASS_NAME); return METHOD_ELEMENT; } {FIELD_ELEMENT} { yybegin(CLASS_NAME); return FIELD_ELEMENT; } + // Fallback to avoid breaking code highlighting at the access or target kind while editing + \S+ { return NAME_ELEMENT; } }
{ @@ -94,5 +98,4 @@ WHITE_SPACE=\s {CRLF} { yybegin(YYINITIAL); return CRLF; } {WHITE_SPACE} { return WHITE_SPACE; } -{COMMENT} { return COMMENT; } [^] { return BAD_CHARACTER; } diff --git a/src/main/grammars/AwParser.bnf b/src/main/grammars/AwParser.bnf index 339c0be92..c677023d4 100644 --- a/src/main/grammars/AwParser.bnf +++ b/src/main/grammars/AwParser.bnf @@ -32,56 +32,52 @@ elementTypeClass="com.demonwav.mcdev.platform.mcp.aw.psi.AwElementType" tokenTypeClass="com.demonwav.mcdev.platform.mcp.aw.psi.AwTokenType" - consumeTokenMethod="consumeTokenFast" + consumeTokenMethod(".*_recover")="consumeTokenFast" } -aw_file ::= header_line line* +aw_file ::= header_line? line* private header_line ::= !<> header COMMENT? end_line -private line ::= !<> entry? COMMENT? end_line -private end_line ::= crlf | <> +private line ::= !<> line_content end_line +private line_recover ::= !(end_line | COMMENT) +private end_line ::= CRLF | <> + +private line_content ::= entry? COMMENT? { + recoverWhile=line_recover +} header ::= HEADER_NAME HEADER_VERSION_ELEMENT HEADER_NAMESPACE_ELEMENT { mixin="com.demonwav.mcdev.platform.mcp.aw.psi.mixins.impl.AwHeaderImplMixin" implements="com.demonwav.mcdev.platform.mcp.aw.psi.mixins.AwHeaderMixin" } -private entry ::= class_entry | method_entry | field_entry { +entry ::= class_entry | method_entry | field_entry { mixin="com.demonwav.mcdev.platform.mcp.aw.psi.mixins.impl.AwEntryImplMixin" implements="com.demonwav.mcdev.platform.mcp.aw.psi.mixins.AwEntryMixin" - recoverWhile = line_recover } -class_entry ::= access class_literal class_name { +class_entry ::= ACCESS_ELEMENT CLASS_ELEMENT class_name { + extends=entry mixin="com.demonwav.mcdev.platform.mcp.aw.psi.mixins.impl.AwClassEntryImplMixin" implements="com.demonwav.mcdev.platform.mcp.aw.psi.mixins.AwClassEntryMixin" + pin=2 } -method_entry ::= access method_literal class_name member_name method_desc{ +method_entry ::= ACCESS_ELEMENT METHOD_ELEMENT class_name member_name method_desc { + extends=entry mixin="com.demonwav.mcdev.platform.mcp.aw.psi.mixins.impl.AwMethodEntryImplMixin" implements="com.demonwav.mcdev.platform.mcp.aw.psi.mixins.AwMethodEntryMixin" + pin=2 } -field_entry ::= access field_literal class_name member_name field_desc{ +field_entry ::= ACCESS_ELEMENT FIELD_ELEMENT class_name member_name field_desc { + extends=entry mixin="com.demonwav.mcdev.platform.mcp.aw.psi.mixins.impl.AwFieldEntryImplMixin" implements="com.demonwav.mcdev.platform.mcp.aw.psi.mixins.AwFieldEntryMixin" + pin=2 } -private line_recover ::= !(end_line | COMMENT) - -access ::= ACCESS_ELEMENT { - methods=[ - accessElement="ACCESS_ELEMENT" - ] -} - -class_literal ::= CLASS_ELEMENT - -method_literal ::= METHOD_ELEMENT - -field_literal ::= FIELD_ELEMENT - class_name ::= CLASS_NAME_ELEMENT { mixin="com.demonwav.mcdev.platform.mcp.aw.psi.mixins.impl.AwClassNameImplMixin" implements="com.demonwav.mcdev.platform.mcp.aw.psi.mixins.AwClassNameMixin" @@ -98,7 +94,9 @@ member_name ::= NAME_ELEMENT { ] } -method_desc ::= OPEN_PAREN desc_element* CLOSE_PAREN desc_element +method_desc ::= OPEN_PAREN desc_element* CLOSE_PAREN desc_element { + pin=1 +} field_desc ::= desc_element @@ -109,4 +107,4 @@ desc_element ::= PRIMITIVE | CLASS_VALUE { primitive="PRIMITIVE" classValue="CLASS_VALUE" ] -} \ No newline at end of file +} diff --git a/src/main/kotlin/platform/fabric/reference/FabricModJsonResolveScopeEnlarger.kt b/src/main/kotlin/platform/fabric/reference/FabricModJsonResolveScopeEnlarger.kt index d08030ba2..357c5e0ef 100644 --- a/src/main/kotlin/platform/fabric/reference/FabricModJsonResolveScopeEnlarger.kt +++ b/src/main/kotlin/platform/fabric/reference/FabricModJsonResolveScopeEnlarger.kt @@ -21,21 +21,46 @@ package com.demonwav.mcdev.platform.fabric.reference import com.demonwav.mcdev.platform.fabric.util.FabricConstants +import com.demonwav.mcdev.platform.mcp.aw.AwFileType +import com.demonwav.mcdev.platform.mcp.fabricloom.FabricLoomData +import com.intellij.openapi.module.ModuleManager import com.intellij.openapi.module.ModuleUtilCore import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.ResolveScopeEnlarger import com.intellij.psi.search.SearchScope +import org.jetbrains.plugins.gradle.util.GradleUtil class FabricModJsonResolveScopeEnlarger : ResolveScopeEnlarger() { override fun getAdditionalResolveScope(file: VirtualFile, project: Project): SearchScope? { - if (file.name != FabricConstants.FABRIC_MOD_JSON) { - return null + if (file.name == FabricConstants.FABRIC_MOD_JSON) { + val module = ModuleUtilCore.findModuleForFile(file, project) + ?: return null + return module.moduleWithDependentsScope.union(module.moduleTestsWithDependentsScope) } - val module = ModuleUtilCore.findModuleForFile(file, project) - ?: return null - return module.moduleWithDependentsScope.union(module.moduleTestsWithDependentsScope) + if (file.fileType is AwFileType) { + var module = ModuleUtilCore.findModuleForFile(file, project) + ?: return null + + val loomData = GradleUtil.findGradleModuleData(module)?.children + ?.find { it.key == FabricLoomData.KEY }?.data as? FabricLoomData + ?: return null + + var moduleManager = ModuleManager.getInstance(project) + var baseModuleName = module.name.substringBeforeLast('.') + var scope = module.moduleWithLibrariesScope + for ((_, sourceSets) in loomData.modSourceSets.orEmpty()) { + for (name in sourceSets) { + val otherModule = moduleManager.findModuleByName("$baseModuleName.$name") ?: continue + scope = scope.union(otherModule.moduleWithLibrariesScope) + } + } + + return scope + } + + return null } } diff --git a/src/main/kotlin/platform/mcp/at/AtFile.kt b/src/main/kotlin/platform/mcp/at/AtFile.kt index b1980e3cc..a18ec4570 100644 --- a/src/main/kotlin/platform/mcp/at/AtFile.kt +++ b/src/main/kotlin/platform/mcp/at/AtFile.kt @@ -54,7 +54,7 @@ class AtFile(viewProvider: FileViewProvider) : PsiFileBase(viewProvider, AtLangu } fun addHeadComment(text: String) { - val toAdd = text.lines().flatMap { listOf(AtElementFactory.createComment(project, it)) } + val toAdd = text.lines().map { AtElementFactory.createComment(project, it) } val lastHeadComment = headComments.lastOrNull() if (lastHeadComment == null) { for (comment in toAdd.reversed()) { diff --git a/src/main/kotlin/platform/mcp/at/inspections/AtDuplicateEntryInspection.kt b/src/main/kotlin/platform/mcp/at/inspections/AtDuplicateEntryInspection.kt index a4c4160b1..653fdcbd0 100644 --- a/src/main/kotlin/platform/mcp/at/inspections/AtDuplicateEntryInspection.kt +++ b/src/main/kotlin/platform/mcp/at/inspections/AtDuplicateEntryInspection.kt @@ -29,7 +29,9 @@ import com.intellij.psi.PsiElementVisitor class AtDuplicateEntryInspection : LocalInspectionTool() { - override fun getStaticDescription(): String? = "Reports duplicate AT entries in the same file" + override fun runForWholeFile(): Boolean = true + + override fun getStaticDescription(): String = "Reports duplicate AT entries in the same file" override fun buildVisitor( holder: ProblemsHolder, diff --git a/src/main/kotlin/platform/mcp/at/inspections/AtUsageInspection.kt b/src/main/kotlin/platform/mcp/at/inspections/AtUsageInspection.kt index 50145254a..602f1504a 100644 --- a/src/main/kotlin/platform/mcp/at/inspections/AtUsageInspection.kt +++ b/src/main/kotlin/platform/mcp/at/inspections/AtUsageInspection.kt @@ -22,13 +22,18 @@ 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.platform.mcp.at.gen.psi.AtVisitor import com.demonwav.mcdev.util.excludeFileTypes import com.intellij.codeInspection.LocalInspectionTool +import com.intellij.codeInspection.LocalQuickFix import com.intellij.codeInspection.ProblemsHolder +import com.intellij.openapi.fileTypes.FileType 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.PsiReference import com.intellij.psi.search.GlobalSearchScope import com.intellij.psi.search.searches.OverridingMethodsSearch import com.intellij.psi.search.searches.ReferencesSearch @@ -40,81 +45,90 @@ class AtUsageInspection : LocalInspectionTool() { } override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { - return object : PsiElementVisitor() { - override fun visitElement(element: PsiElement) { - if (element !is AtEntry) { - return - } + return object : AtVisitor() { + + private val fixProvider = { it: AtEntry -> RemoveAtEntryFix.forWholeLine(it, true) } - val function = element.function + override fun visitEntry(entry: AtEntry) { + val function = entry.function if (function != null) { - checkElement(element, function) + checkElement(entry, function, holder, AtFileType, fixProvider) { file, toSkip -> + file.children.asSequence() + .filterIsInstance() + .filter { it != toSkip } + .mapNotNull { it.function?.reference } + } return } - val fieldName = element.fieldName + val fieldName = entry.fieldName if (fieldName != null) { - checkElement(element, fieldName) + checkElement(entry, fieldName, holder, AtFileType, fixProvider) return } // Only check class names if it is the target of the entry - checkElement(element, element.className) + checkElement(entry, entry.className, holder, AtFileType, fixProvider) + } + } + } + + companion object { + + @JvmStatic + fun checkElement( + entry: E, + element: PsiElement, + holder: ProblemsHolder, + fileType: FileType, + fixProvider: (entry: E) -> LocalQuickFix, + entriesReferenceProvider: (PsiFile, toSkip: E) -> Sequence = { _, _ -> emptySequence() } + ) { + val referenced = element.reference?.resolve() ?: return + val scope = GlobalSearchScope.projectScope(element.project) + .excludeFileTypes(element.project, fileType) + val query = ReferencesSearch.search(referenced, scope, true) + if (query.any()) { + return } - 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()) { + if (referenced is PsiMethod) { + // The regular references search doesn't cover overridden methods + val overridingQuery = OverridingMethodsSearch.search(referenced, scope, true) + if (overridingQuery.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()) { + // Also ignore if other entries cover super methods + val superMethods = referenced.findSuperMethods() + for (reference in entriesReferenceProvider(entry.containingFile, entry)) { + val otherResolved = reference.resolve() + if (superMethods.contains(otherResolved)) { 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 - } + 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 (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 - } + } + for (innerClass in referenced.innerClasses) { + if (ReferencesSearch.search(innerClass, scope, true).any()) { + return } } - - val fix = RemoveAtEntryFix.forWholeLine(entry, true) - holder.registerProblem(entry, "Access Transformer entry is never used", fix) } + + holder.registerProblem(entry, "Entry is never used", fixProvider(entry)) } } } diff --git a/src/main/kotlin/platform/mcp/aw/AwAnnotator.kt b/src/main/kotlin/platform/mcp/aw/AwAnnotator.kt index 07bf01c3a..7929965d8 100644 --- a/src/main/kotlin/platform/mcp/aw/AwAnnotator.kt +++ b/src/main/kotlin/platform/mcp/aw/AwAnnotator.kt @@ -20,11 +20,8 @@ package com.demonwav.mcdev.platform.mcp.aw -import com.demonwav.mcdev.platform.mcp.aw.gen.psi.AwAccess -import com.demonwav.mcdev.platform.mcp.aw.gen.psi.AwClassLiteral -import com.demonwav.mcdev.platform.mcp.aw.gen.psi.AwFieldLiteral import com.demonwav.mcdev.platform.mcp.aw.gen.psi.AwHeader -import com.demonwav.mcdev.platform.mcp.aw.gen.psi.AwMethodLiteral +import com.demonwav.mcdev.platform.mcp.aw.gen.psi.AwTypes import com.demonwav.mcdev.util.childOfType import com.google.common.collect.HashMultimap import com.google.common.collect.Multimaps @@ -34,23 +31,25 @@ import com.intellij.lang.annotation.HighlightSeverity import com.intellij.psi.PsiElement import com.intellij.psi.PsiWhiteSpace import com.intellij.psi.util.PsiTreeUtil +import com.intellij.psi.util.elementType +import org.jetbrains.plugins.groovy.util.TokenSet class AwAnnotator : Annotator { override fun annotate(element: PsiElement, holder: AnnotationHolder) { - if (element is AwAccess) { + if (element.elementType == AwTypes.ACCESS_ELEMENT) { val access = element.text val target = PsiTreeUtil.skipSiblingsForward(element, PsiWhiteSpace::class.java)?.text if (!compatibleByAccessMap.get(access).contains(target)) { holder.newAnnotation(HighlightSeverity.ERROR, "Access '$access' cannot be used on '$target'").create() } - if (element.accessElement.text.startsWith("transitive-") && + if (element.text.startsWith("transitive-") && element.containingFile?.childOfType()?.versionString == "v1" ) { holder.newAnnotation(HighlightSeverity.ERROR, "Transitive accesses were introduced in v2").create() } - } else if (element is AwFieldLiteral || element is AwMethodLiteral || element is AwClassLiteral) { + } else if (element.elementType in targetLiterals) { val target = element.text val access = PsiTreeUtil.skipSiblingsBackward(element, PsiWhiteSpace::class.java)?.text if (!compatibleByTargetMap.get(target).contains(access)) { @@ -64,6 +63,8 @@ class AwAnnotator : Annotator { val compatibleByAccessMap = HashMultimap.create() val compatibleByTargetMap = HashMultimap.create() + val targetLiterals = TokenSet(AwTypes.FIELD_ELEMENT, AwTypes.METHOD_ELEMENT, AwTypes.CLASS_ELEMENT) + init { compatibleByAccessMap.putAll("accessible", setOf("class", "method", "field")) compatibleByAccessMap.putAll("transitive-accessible", setOf("class", "method", "field")) diff --git a/src/main/kotlin/platform/mcp/aw/AwCompletionContributor.kt b/src/main/kotlin/platform/mcp/aw/AwCompletionContributor.kt index c9bc5e656..e82f6acda 100644 --- a/src/main/kotlin/platform/mcp/aw/AwCompletionContributor.kt +++ b/src/main/kotlin/platform/mcp/aw/AwCompletionContributor.kt @@ -27,7 +27,9 @@ import com.intellij.codeInsight.completion.CompletionParameters import com.intellij.codeInsight.completion.CompletionProvider import com.intellij.codeInsight.completion.CompletionResultSet import com.intellij.codeInsight.completion.CompletionType +import com.intellij.codeInsight.completion.InsertHandler import com.intellij.codeInsight.completion.InsertionContext +import com.intellij.codeInsight.lookup.LookupElement import com.intellij.codeInsight.lookup.LookupElementBuilder import com.intellij.openapi.application.runReadAction import com.intellij.patterns.PlatformPatterns @@ -46,9 +48,9 @@ class AwCompletionContributor : CompletionContributor() { extend(null, namespacePattern, AwNamespaceCompletionProvider) val accessPattern = PlatformPatterns.psiElement().afterLeaf(PlatformPatterns.psiElement(AwTypes.CRLF)) extend(null, accessPattern, AwAccessCompletionProvider) - val targetPattern = PlatformPatterns.psiElement() + val targetKindPattern = PlatformPatterns.psiElement() .afterLeafSkipping(whitespace, PlatformPatterns.psiElement(AwTypes.ACCESS_ELEMENT)) - extend(null, targetPattern, AwTargetCompletionProvider) + extend(null, targetKindPattern, AwTargetKindCompletionProvider) } } @@ -107,7 +109,7 @@ object AwAccessCompletionProvider : CompletionProvider() { } } -object AwTargetCompletionProvider : CompletionProvider() { +object AwTargetKindCompletionProvider : CompletionProvider() { override fun addCompletions( parameters: CompletionParameters, @@ -121,3 +123,11 @@ object AwTargetCompletionProvider : CompletionProvider() { result.addAllElements(elements) } } + +object DeleteEndOfLineInsertionHandler : InsertHandler { + + override fun handleInsert(context: InsertionContext, item: LookupElement) { + val line = context.document.getLineNumber(context.tailOffset) + context.document.deleteString(context.tailOffset, context.document.getLineEndOffset(line)) + } +} diff --git a/src/main/kotlin/platform/mcp/aw/AwElementFactory.kt b/src/main/kotlin/platform/mcp/aw/AwElementFactory.kt new file mode 100644 index 000000000..d4cbb25b4 --- /dev/null +++ b/src/main/kotlin/platform/mcp/aw/AwElementFactory.kt @@ -0,0 +1,68 @@ +/* + * 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.aw + +import com.demonwav.mcdev.platform.mcp.aw.AwElementFactory.Access.entries +import com.demonwav.mcdev.platform.mcp.aw.gen.psi.AwEntry +import com.demonwav.mcdev.platform.mcp.aw.gen.psi.AwHeader +import com.demonwav.mcdev.platform.mcp.aw.gen.psi.AwTypes +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiComment +import com.intellij.psi.PsiFileFactory + +object AwElementFactory { + + fun createFile(project: Project, text: String): AwFile { + return PsiFileFactory.getInstance(project).createFileFromText("name", AwFileType, text) as AwFile + } + + fun createEntry(project: Project, entry: String): AwEntry { + val file = createFile(project, entry) + return file.firstChild as AwEntry + } + + fun createComment(project: Project, comment: String): PsiComment { + val line = "# $comment" + val file = createFile(project, line) + + return file.node.findChildByType(AwTypes.COMMENT)!!.psi as PsiComment + } + + fun createHeader(project: Project): AwHeader { + val file = createFile(project, "accessWidener v2 named\n") + return file.firstChild as AwHeader + } + + enum class Access(val text: String) { + EXTENDABLE("extendable"), + ACCESSIBLE("accessible"), + MUTABLE("mutable"), + TRANSITIVE_EXTENDABLE("transitive-extendable"), + TRANSITIVE_ACCESSIBLE("transitive-accessible"), + TRANSITIVE_MUTABLE("transitive-mutable"), + ; + + companion object { + fun match(s: String) = entries.firstOrNull { it.text == s } + fun softMatch(s: String) = entries.filter { it.text.contains(s) } + } + } +} diff --git a/src/main/kotlin/platform/mcp/aw/AwFile.kt b/src/main/kotlin/platform/mcp/aw/AwFile.kt index 986e7759d..4349a965c 100644 --- a/src/main/kotlin/platform/mcp/aw/AwFile.kt +++ b/src/main/kotlin/platform/mcp/aw/AwFile.kt @@ -21,20 +21,53 @@ package com.demonwav.mcdev.platform.mcp.aw import com.demonwav.mcdev.asset.PlatformAssets +import com.demonwav.mcdev.platform.mcp.aw.gen.psi.AwEntry import com.demonwav.mcdev.platform.mcp.aw.gen.psi.AwHeader -import com.demonwav.mcdev.platform.mcp.aw.psi.mixins.AwEntryMixin import com.demonwav.mcdev.util.childrenOfType import com.intellij.extapi.psi.PsiFileBase import com.intellij.psi.FileViewProvider +import com.intellij.psi.PsiComment +import com.intellij.psi.PsiElement class AwFile(viewProvider: FileViewProvider) : PsiFileBase(viewProvider, AwLanguage) { val header: AwHeader? - get() = children.first { it is AwHeader } as? AwHeader + get() = children.firstOrNull { it is AwHeader } as? AwHeader - val entries: Collection + val entries: Collection get() = childrenOfType() + val headComments: List + get() { + val comments = mutableListOf() + for (child in children) { + if (child is AwEntry) { + break + } + + if (child is PsiComment) { + comments.add(child) + } + } + + return comments + } + + fun addHeadComment(text: String) { + val toAdd = text.lines().map { AwElementFactory.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) + } + } + } + override fun getFileType() = AwFileType override fun toString() = "Access Widener File" override fun getIcon(flags: Int) = PlatformAssets.MCP_ICON diff --git a/src/main/kotlin/platform/mcp/aw/AwParserDefinition.kt b/src/main/kotlin/platform/mcp/aw/AwParserDefinition.kt index 487b98d58..18a917765 100644 --- a/src/main/kotlin/platform/mcp/aw/AwParserDefinition.kt +++ b/src/main/kotlin/platform/mcp/aw/AwParserDefinition.kt @@ -41,18 +41,33 @@ class AwParserDefinition : ParserDefinition { override fun createLexer(project: Project): Lexer = AwLexerAdapter() override fun createParser(project: Project): PsiParser = AwParser() override fun getFileNodeType(): IFileElementType = FILE - override fun getWhitespaceTokens(): TokenSet = WHITE_SPACES override fun getCommentTokens(): TokenSet = COMMENTS override fun getStringLiteralElements(): TokenSet = TokenSet.EMPTY override fun createElement(node: ASTNode): PsiElement = AwTypes.Factory.createElement(node) override fun createFile(viewProvider: FileViewProvider): PsiFile = AwFile(viewProvider) override fun spaceExistenceTypeBetweenTokens(left: ASTNode, right: ASTNode): ParserDefinition.SpaceRequirements { + var leftType = left.elementType + val rightType = right.elementType + + if (leftType == AwTypes.CRLF || rightType == AwTypes.CRLF) { + return ParserDefinition.SpaceRequirements.MAY + } + + // Always add a line break after a comment + if (leftType == AwTypes.COMMENT) { + return ParserDefinition.SpaceRequirements.MUST_LINE_BREAK + } + + // Add a comment before an end of line comment + if (rightType == AwTypes.COMMENT && leftType != AwTypes.CRLF && leftType != TokenType.WHITE_SPACE) { + return ParserDefinition.SpaceRequirements.MUST + } + return LanguageUtil.canStickTokensTogetherByLexer(left, right, AwLexerAdapter()) } companion object { - private val WHITE_SPACES = TokenSet.create(TokenType.WHITE_SPACE) private val COMMENTS = TokenSet.create(AwTypes.COMMENT) private val FILE = IFileElementType(Language.findInstance(AwLanguage::class.java)) diff --git a/src/main/kotlin/platform/mcp/aw/fixes/CreateAwHeaderFix.kt b/src/main/kotlin/platform/mcp/aw/fixes/CreateAwHeaderFix.kt new file mode 100644 index 000000000..29aecdf11 --- /dev/null +++ b/src/main/kotlin/platform/mcp/aw/fixes/CreateAwHeaderFix.kt @@ -0,0 +1,42 @@ +/* + * 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.aw.fixes + +import com.demonwav.mcdev.platform.mcp.aw.AwElementFactory +import com.intellij.codeInspection.LocalQuickFix +import com.intellij.codeInspection.ProblemDescriptor +import com.intellij.codeInspection.util.IntentionFamilyName +import com.intellij.codeInspection.util.IntentionName +import com.intellij.openapi.project.Project + +class CreateAwHeaderFix : LocalQuickFix { + + override fun getFamilyName(): @IntentionFamilyName String = "Create header" + + override fun getName(): @IntentionName String = familyName + + override fun startInWriteAction(): Boolean = true + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + var toInsert = AwElementFactory.createFile(project, "accessWidener v2 named\n\n") + descriptor.psiElement.containingFile.addRangeAfter(toInsert.firstChild, toInsert.lastChild, null) + } +} diff --git a/src/main/kotlin/platform/mcp/aw/fixes/RemoveAwEntryFix.kt b/src/main/kotlin/platform/mcp/aw/fixes/RemoveAwEntryFix.kt new file mode 100644 index 000000000..5e3f940e1 --- /dev/null +++ b/src/main/kotlin/platform/mcp/aw/fixes/RemoveAwEntryFix.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.aw.fixes + +import com.demonwav.mcdev.platform.mcp.aw.gen.psi.AwEntry +import com.demonwav.mcdev.platform.mcp.aw.gen.psi.AwTypes +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 RemoveAwEntryFix(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: AwEntry, inBatchMode: Boolean): RemoveAwEntryFix { + val start = entry.siblings(forward = false, withSelf = false) + .firstOrNull { it.elementType == AwTypes.CRLF }?.nextSibling + val end = entry.siblings(forward = true, withSelf = true) + .firstOrNull { it.elementType == AwTypes.CRLF } + return RemoveAwEntryFix(start ?: entry, end ?: entry, inBatchMode) + } + } +} diff --git a/src/main/kotlin/platform/mcp/aw/format/AwBlock.kt b/src/main/kotlin/platform/mcp/aw/format/AwBlock.kt new file mode 100644 index 000000000..8d56f1d4a --- /dev/null +++ b/src/main/kotlin/platform/mcp/aw/format/AwBlock.kt @@ -0,0 +1,110 @@ +/* + * 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.aw.format + +import com.demonwav.mcdev.platform.mcp.aw.gen.psi.AwTypes +import com.demonwav.mcdev.util.children +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.lang.ASTNode +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 AwBlock( + node: ASTNode, + wrap: Wrap?, + alignment: Alignment?, + val spacingBuilder: SpacingBuilder, + val codeStyleSettings: CodeStyleSettings, + val targetKindAlignment: Alignment? = null, + val entryClassAlignment: Alignment? = null, + val entryMemberAlignment: Alignment? = null, +) : AbstractBlock(node, wrap, alignment) { + + override fun buildChildren(): List { + val blocks = mutableListOf() + + var targetKindAlignment: Alignment? = targetKindAlignment + var entryClassAlignment: Alignment? = entryClassAlignment + var entryMemberAlignment: Alignment? = entryMemberAlignment + + var newlineCount = 0 + val alignGroups = node.elementType is IFileElementType && + codeStyleSettings.getCustomSettings(AwCodeStyleSettings::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 == AwTypes.CRLF) { + newlineCount++ + continue + } else if (childType != AwTypes.COMMENT) { + if (newlineCount >= 2) { + // Align different groups separately, comments are not counted towards any group + targetKindAlignment = Alignment.createAlignment(true) + entryClassAlignment = Alignment.createAlignment(true) + entryMemberAlignment = Alignment.createAlignment(true) + } + newlineCount = 0 + } + } + + val alignment = when (childType) { + AwTypes.CLASS_ELEMENT, AwTypes.FIELD_ELEMENT, AwTypes.METHOD_ELEMENT -> targetKindAlignment + AwTypes.CLASS_NAME -> entryClassAlignment + AwTypes.MEMBER_NAME, AwTypes.FIELD_DESC, AwTypes.METHOD_DESC -> entryMemberAlignment + else -> null + } + + blocks.add( + AwBlock( + child, + null, + alignment, + spacingBuilder, + codeStyleSettings, + targetKindAlignment, + entryClassAlignment, + entryMemberAlignment + ) + ) + } + + return blocks + } + + 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/aw/format/AwCodeStyleSettings.kt b/src/main/kotlin/platform/mcp/aw/format/AwCodeStyleSettings.kt new file mode 100644 index 000000000..dfc15940f --- /dev/null +++ b/src/main/kotlin/platform/mcp/aw/format/AwCodeStyleSettings.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.aw.format + +import com.demonwav.mcdev.platform.mcp.aw.AwLanguage +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 AwCodeStyleSettings(val settings: CodeStyleSettings) : CustomCodeStyleSettings("AwCodeStyleSettings", settings) { + @JvmField + var SPACE_BEFORE_ENTRY_COMMENT = true + + @JvmField + var ALIGN_ENTRY_CLASS_AND_MEMBER = true +} + +class AwCodeStyleSettingsProvider : CodeStyleSettingsProvider() { + override fun createCustomSettings(settings: CodeStyleSettings): CustomCodeStyleSettings = + AwCodeStyleSettings(settings) + + override fun getConfigurableDisplayName(): @NlsContexts.ConfigurableName String? = AwLanguage.displayName + + override fun createConfigurable( + settings: CodeStyleSettings, + modelSettings: CodeStyleSettings + ): CodeStyleConfigurable { + return object : CodeStyleAbstractConfigurable(settings, modelSettings, configurableDisplayName) { + override fun createPanel(settings: CodeStyleSettings): CodeStyleAbstractPanel { + return AwCodeStyleSettingsConfigurable(currentSettings, settings) + } + } + } +} + +class AwCodeStyleSettingsConfigurable(currentSettings: CodeStyleSettings, settings: CodeStyleSettings) : + TabbedLanguageCodeStylePanel(AwLanguage, currentSettings, settings) + +class AwLanguageCodeStyleSettingsProvider : LanguageCodeStyleSettingsProvider() { + + override fun getLanguage(): Language = AwLanguage + + override fun customizeSettings(consumer: CodeStyleSettingsCustomizable, settingsType: SettingsType) { + if (settingsType == SettingsType.SPACING_SETTINGS) { + consumer.showCustomOption( + AwCodeStyleSettings::class.java, + "SPACE_BEFORE_ENTRY_COMMENT", + "Space before entry comment", + "Spacing and alignment" + ) + consumer.showCustomOption( + AwCodeStyleSettings::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 + + accessible method net/minecraft/client/Minecraft pickBlock ()V # This is an entry comment + accessible method net/minecraft/client/Minecraft userProperties ()Lcom/mojang/authlib/minecraft/UserApiService${'$'}UserProperties; + + # Each group can be aligned independently + accessible field net/minecraft/client/gui/screens/inventory/AbstractContainerScreen clickedSlot I + accessible field net/minecraft/client/gui/screens/inventory/AbstractContainerScreen playerInventoryTitle Ljava/lang/String; + extendable method net/minecraft/client/gui/screens/inventory/AbstractContainerScreen findSlot (DD)Lnet/minecraft/world/inventory/Slot; + """.trimIndent() +} diff --git a/src/main/kotlin/platform/mcp/aw/format/AwFormattingModelBuilder.kt b/src/main/kotlin/platform/mcp/aw/format/AwFormattingModelBuilder.kt new file mode 100644 index 000000000..b0f213555 --- /dev/null +++ b/src/main/kotlin/platform/mcp/aw/format/AwFormattingModelBuilder.kt @@ -0,0 +1,68 @@ +/* + * 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.aw.format + +import com.demonwav.mcdev.platform.mcp.aw.AwLanguage +import com.demonwav.mcdev.platform.mcp.aw.gen.psi.AwTypes +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.psi.codeStyle.CodeStyleSettings +import com.intellij.psi.tree.TokenSet + +class AwFormattingModelBuilder : FormattingModelBuilder { + + private fun createSpaceBuilder(settings: CodeStyleSettings): SpacingBuilder { + val atSettings = settings.getCustomSettings(AwCodeStyleSettings::class.java) + var targetKindTokens = TokenSet.create(AwTypes.CLASS_ELEMENT, AwTypes.METHOD_ELEMENT, AwTypes.FIELD_ELEMENT) + var entryTokens = TokenSet.create(AwTypes.CLASS_ENTRY, AwTypes.METHOD_ENTRY, AwTypes.FIELD_ENTRY) + return SpacingBuilder(settings, AwLanguage) + .between(entryTokens, AwTypes.COMMENT).spaceIf(atSettings.SPACE_BEFORE_ENTRY_COMMENT) + // Removes alignment spaces if it is disabled + .between(AwTypes.ACCESS_ELEMENT, targetKindTokens).spaces(1) + .between(targetKindTokens, AwTypes.CLASS_ELEMENT).spaces(1) + .between(AwTypes.CLASS_ELEMENT, AwTypes.MEMBER_NAME).spaces(1) + .between(AwTypes.MEMBER_NAME, AwTypes.FIELD_DESC).spaces(1) + .between(AwTypes.MEMBER_NAME, AwTypes.METHOD_DESC).spaces(1) + } + + override fun createModel(formattingContext: FormattingContext): FormattingModel { + val codeStyleSettings = formattingContext.codeStyleSettings + val rootBlock = AwBlock( + formattingContext.node, + null, + null, + createSpaceBuilder(codeStyleSettings), + codeStyleSettings, + Alignment.createAlignment(true), + Alignment.createAlignment(true), + Alignment.createAlignment(true), + ) + return FormattingModelProvider.createFormattingModelForPsiFile( + formattingContext.containingFile, + rootBlock, + codeStyleSettings + ) + } +} diff --git a/src/main/kotlin/platform/mcp/aw/inspections/AwHeaderInspection.kt b/src/main/kotlin/platform/mcp/aw/inspections/AwHeaderInspection.kt new file mode 100644 index 000000000..d4915f40a --- /dev/null +++ b/src/main/kotlin/platform/mcp/aw/inspections/AwHeaderInspection.kt @@ -0,0 +1,56 @@ +/* + * 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.aw.inspections + +import com.demonwav.mcdev.platform.mcp.aw.AwFile +import com.demonwav.mcdev.platform.mcp.aw.fixes.CreateAwHeaderFix +import com.intellij.codeInspection.InspectionManager +import com.intellij.codeInspection.LocalInspectionTool +import com.intellij.codeInspection.ProblemDescriptor +import com.intellij.codeInspection.ProblemHighlightType +import com.intellij.psi.PsiFile +import org.jetbrains.annotations.Nls + +class AwHeaderInspection : LocalInspectionTool() { + + @Nls + override fun getStaticDescription(): String? = "Reports problems about Access Widener headers" + + override fun checkFile( + file: PsiFile, + manager: InspectionManager, + isOnTheFly: Boolean + ): Array? { + if ((file as? AwFile)?.header == null) { + return arrayOf( + manager.createProblemDescriptor( + file.firstChild ?: file, + "Missing header", + CreateAwHeaderFix(), + ProblemHighlightType.GENERIC_ERROR_OR_WARNING, + isOnTheFly + ) + ) + } + + return null + } +} diff --git a/src/main/kotlin/platform/mcp/aw/inspections/AwInspectionSuppressor.kt b/src/main/kotlin/platform/mcp/aw/inspections/AwInspectionSuppressor.kt new file mode 100644 index 000000000..eefd9a62b --- /dev/null +++ b/src/main/kotlin/platform/mcp/aw/inspections/AwInspectionSuppressor.kt @@ -0,0 +1,146 @@ +/* + * 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.aw.inspections + +import com.demonwav.mcdev.platform.mcp.aw.AwElementFactory +import com.demonwav.mcdev.platform.mcp.aw.AwFile +import com.demonwav.mcdev.platform.mcp.aw.gen.psi.AwEntry +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.PsiTreeUtil +import com.intellij.psi.util.parentOfType + +class AwInspectionSuppressor : InspectionSuppressor { + + override fun isSuppressedFor(element: PsiElement, toolId: String): Boolean { + val entry = element.parentOfType(withSelf = true) + val entryComment = entry?.commentText + if (entryComment != null) { + if (isSuppressing(entryComment, toolId)) { + return true + } + } + + val file = element.containingFile as AwFile + 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 + } + + override fun getSuppressActions( + element: PsiElement?, + toolId: String + ): Array { + if (element == null) { + return SuppressQuickFix.EMPTY_ARRAY + } + + val entry = element as? AwEntry + ?: element.parentOfType(withSelf = true) + ?: PsiTreeUtil.getPrevSiblingOfType(element, AwEntry::class.java) // For when we are at a CRLF + return if (entry != null) { + arrayOf(AwSuppressQuickFix(entry, toolId), AwSuppressQuickFix(element.containingFile, toolId)) + } else { + arrayOf(AwSuppressQuickFix(element.containingFile, toolId)) + } + } + + class AwSuppressQuickFix(element: PsiElement, val toolId: String) : + LocalQuickFixOnPsiElement(element), SuppressQuickFix { + + override fun getText(): @IntentionName String = when (startElement) { + is AwEntry -> "Suppress $toolId for entry" + is AwFile -> "Suppress $toolId for file" + else -> "Suppress $toolId" + } + + override fun getFamilyName(): @IntentionFamilyName String = "Suppress inspection" + + override fun invoke( + project: Project, + file: PsiFile, + startElement: PsiElement, + endElement: PsiElement + ) { + when (startElement) { + is AwEntry -> suppressForEntry(startElement) + is AwFile -> suppressForFile(startElement) + } + } + + private fun suppressForEntry(entry: AwEntry) { + 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) + } + + private fun suppressForFile(file: AwFile) { + 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 = AwElementFactory.createComment(file.project, newCommentText) + existingSuppressComment.replace(newComment) + } + + override fun isAvailable( + project: Project, + context: PsiElement + ): Boolean = context.isValid + + override fun isSuppressAll(): Boolean = false + } + +} diff --git a/src/main/kotlin/platform/mcp/aw/inspections/AwUnresolvedReferenceInspection.kt b/src/main/kotlin/platform/mcp/aw/inspections/AwUnresolvedReferenceInspection.kt new file mode 100644 index 000000000..deeba15d2 --- /dev/null +++ b/src/main/kotlin/platform/mcp/aw/inspections/AwUnresolvedReferenceInspection.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.aw.inspections + +import com.demonwav.mcdev.platform.mcp.aw.gen.psi.AwVisitor +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 AwUnresolvedReferenceInspection : LocalInspectionTool() { + + override fun getStaticDescription(): String? = "Reports unresolved AW targets." + + override fun buildVisitor( + holder: ProblemsHolder, + isOnTheFly: Boolean + ): PsiElementVisitor = object : AwVisitor() { + 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/kotlin/platform/mcp/aw/inspections/AwUsageInspection.kt b/src/main/kotlin/platform/mcp/aw/inspections/AwUsageInspection.kt new file mode 100644 index 000000000..4f4b1be42 --- /dev/null +++ b/src/main/kotlin/platform/mcp/aw/inspections/AwUsageInspection.kt @@ -0,0 +1,66 @@ +/* + * 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.aw.inspections + +import com.demonwav.mcdev.platform.mcp.at.inspections.AtUsageInspection +import com.demonwav.mcdev.platform.mcp.aw.AwFileType +import com.demonwav.mcdev.platform.mcp.aw.fixes.RemoveAwEntryFix +import com.demonwav.mcdev.platform.mcp.aw.gen.psi.AwClassEntry +import com.demonwav.mcdev.platform.mcp.aw.gen.psi.AwEntry +import com.demonwav.mcdev.platform.mcp.aw.gen.psi.AwFieldEntry +import com.demonwav.mcdev.platform.mcp.aw.gen.psi.AwMethodEntry +import com.demonwav.mcdev.platform.mcp.aw.gen.psi.AwVisitor +import com.intellij.codeInspection.LocalInspectionTool +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.psi.PsiElementVisitor + +class AwUsageInspection : LocalInspectionTool() { + + override fun getStaticDescription(): String { + return "Reports unused Access Widener entries" + } + + override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { + return object : AwVisitor() { + + private val fixProvider = { it: AwEntry -> RemoveAwEntryFix.forWholeLine(it, true) } + + override fun visitClassEntry(entry: AwClassEntry) { + entry.className?.let { AtUsageInspection.checkElement(entry, it, holder, AwFileType, fixProvider) } + } + + override fun visitFieldEntry(entry: AwFieldEntry) { + entry.memberName?.let { AtUsageInspection.checkElement(entry, it, holder, AwFileType, fixProvider) } + } + + override fun visitMethodEntry(entry: AwMethodEntry) { + entry.memberName?.let { memberName -> + AtUsageInspection.checkElement(entry, memberName, holder, AwFileType, fixProvider) { file, toSkip -> + file.children.asSequence() + .filterIsInstance() + .filter { it != toSkip } + .mapNotNull { it.memberName?.reference } + } + } + } + } + } +} diff --git a/src/main/kotlin/platform/mcp/aw/inspections/DuplicateAwEntryInspection.kt b/src/main/kotlin/platform/mcp/aw/inspections/DuplicateAwEntryInspection.kt index b33646196..e3863f5e1 100644 --- a/src/main/kotlin/platform/mcp/aw/inspections/DuplicateAwEntryInspection.kt +++ b/src/main/kotlin/platform/mcp/aw/inspections/DuplicateAwEntryInspection.kt @@ -21,61 +21,36 @@ package com.demonwav.mcdev.platform.mcp.aw.inspections import com.demonwav.mcdev.platform.mcp.aw.AwFile -import com.demonwav.mcdev.platform.mcp.aw.psi.mixins.AwEntryMixin -import com.demonwav.mcdev.platform.mcp.aw.psi.mixins.AwMemberNameMixin -import com.demonwav.mcdev.util.childOfType +import com.demonwav.mcdev.platform.mcp.aw.fixes.RemoveAwEntryFix import com.intellij.codeInspection.InspectionManager import com.intellij.codeInspection.LocalInspectionTool import com.intellij.codeInspection.ProblemDescriptor import com.intellij.codeInspection.ProblemHighlightType -import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile -import com.intellij.psi.PsiNamedElement -import com.jetbrains.rd.util.getOrCreate -import org.jetbrains.plugins.groovy.codeInspection.fixes.RemoveElementQuickFix class DuplicateAwEntryInspection : LocalInspectionTool() { - override fun checkFile(file: PsiFile, manager: InspectionManager, isOnTheFly: Boolean): Array? { - if (file !is AwFile) { - return null - } - val collected = HashMap, MutableList>() - file.entries.forEach { - val target = it.childOfType()?.resolve() - val accessKind = it.accessKind - if (target != null && accessKind != null) { - (collected.getOrCreate(Pair(target, accessKind)) { ArrayList() }) += it - } - } - val problems = ArrayList() - collected.forEach { (sort, matches) -> - if (sort.first is PsiNamedElement) { - if (matches.size > 1) { - for (match in matches) - problems += manager.createProblemDescriptor( - match, - "Duplicate entry for \"${sort.second} ${(sort.first as PsiNamedElement).name}\"", - RemoveElementQuickFix("Remove duplicate"), - ProblemHighlightType.WARNING, - isOnTheFly, - ) - } - } - } - return problems.toTypedArray() - } + override fun runForWholeFile(): Boolean = true - override fun runForWholeFile(): Boolean { - return true - } + override fun getDisplayName(): String = "Duplicate AW entry" - override fun getDisplayName(): String { - return "Duplicate AW entry" - } + override fun getStaticDescription(): String = "Warns when the same element has its accessibility, mutability, " + + "or extensibility changed multiple times in one file." - override fun getStaticDescription(): String { - return "Warns when the same element has its accessibility, mutability, " + - "or extensibility changed multiple times in one file." + override fun checkFile(file: PsiFile, manager: InspectionManager, isOnTheFly: Boolean): Array { + return (file as AwFile).entries + .groupBy { it.accessKind to it.memberReference } + .filter { (key, matches) -> key.second != null && matches.size > 1 } + .flatMap { (_, matches) -> + matches.asSequence().map { match -> + manager.createProblemDescriptor( + match, + "Duplicate entry", + RemoveAwEntryFix.forWholeLine(match, false), + ProblemHighlightType.GENERIC_ERROR_OR_WARNING, + isOnTheFly, + ) + } + }.toTypedArray() } } diff --git a/src/main/kotlin/platform/mcp/aw/psi/mixins/AwEntryMixin.kt b/src/main/kotlin/platform/mcp/aw/psi/mixins/AwEntryMixin.kt index ff8746346..17b75c91b 100644 --- a/src/main/kotlin/platform/mcp/aw/psi/mixins/AwEntryMixin.kt +++ b/src/main/kotlin/platform/mcp/aw/psi/mixins/AwEntryMixin.kt @@ -21,9 +21,16 @@ package com.demonwav.mcdev.platform.mcp.aw.psi.mixins import com.demonwav.mcdev.platform.mcp.aw.psi.AwElement +import com.demonwav.mcdev.util.MemberReference +import com.intellij.psi.PsiComment interface AwEntryMixin : AwElement { - val accessKind: String? + val accessKind: String val targetClassName: String? + val comment: PsiComment? + val commentText: String? + val memberReference: MemberReference? + + fun setComment(text: String?) } diff --git a/src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwClassEntryImplMixin.kt b/src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwClassEntryImplMixin.kt index a77edadd5..13425bfdf 100644 --- a/src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwClassEntryImplMixin.kt +++ b/src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwClassEntryImplMixin.kt @@ -20,7 +20,22 @@ package com.demonwav.mcdev.platform.mcp.aw.psi.mixins.impl +import com.demonwav.mcdev.platform.mcp.aw.gen.psi.AwClassEntry import com.demonwav.mcdev.platform.mcp.aw.psi.mixins.AwClassEntryMixin +import com.demonwav.mcdev.util.MemberReference import com.intellij.lang.ASTNode +import com.intellij.util.resettableLazy -abstract class AwClassEntryImplMixin(node: ASTNode) : AwEntryImplMixin(node), AwClassEntryMixin +abstract class AwClassEntryImplMixin(node: ASTNode) : AwEntryImplMixin(node), AwClassEntry, AwClassEntryMixin { + + private val lazyMemberReference = resettableLazy { + MemberReference("", owner = checkNotNull(targetClassName) { "Expected targetClassName" }.replace('/', '.')) + } + + override val memberReference: MemberReference? by lazyMemberReference + + override fun subtreeChanged() { + super.subtreeChanged() + lazyMemberReference.reset() + } +} diff --git a/src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwClassNameImplMixin.kt b/src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwClassNameImplMixin.kt index 1ae676e98..bf597c8a8 100644 --- a/src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwClassNameImplMixin.kt +++ b/src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwClassNameImplMixin.kt @@ -55,7 +55,7 @@ abstract class AwClassNameImplMixin(node: ASTNode) : ASTWrapperPsiElement(node), } override fun isReferenceTo(element: PsiElement): Boolean { - return element is PsiClass && element.qualifiedName == text.replace('/', '.') + return element is PsiClass && element.qualifiedName == text.replace('/', '.').replace('$', '.') } override fun isSoft(): Boolean = false diff --git a/src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwDescElementImplMixin.kt b/src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwDescElementImplMixin.kt index 294676184..3004d1c10 100644 --- a/src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwDescElementImplMixin.kt +++ b/src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwDescElementImplMixin.kt @@ -23,6 +23,7 @@ package com.demonwav.mcdev.platform.mcp.aw.psi.mixins.impl import com.demonwav.mcdev.platform.mcp.aw.psi.mixins.AwDescElementMixin import com.demonwav.mcdev.util.cached import com.demonwav.mcdev.util.findQualifiedClass +import com.demonwav.mcdev.util.toTextRange import com.intellij.extapi.psi.ASTWrapperPsiElement import com.intellij.lang.ASTNode import com.intellij.openapi.util.TextRange @@ -36,14 +37,14 @@ abstract class AwDescElementImplMixin(node: ASTNode) : ASTWrapperPsiElement(node override fun getElement(): PsiElement = this - override fun getReference(): PsiReference? = this + override fun getReference(): PsiReference? = if (textContains('L')) this else null override fun resolve(): PsiElement? = cached(PsiModificationTracker.MODIFICATION_COUNT) { val name = asQualifiedName() ?: return@cached null return@cached findQualifiedClass(name, this) } - override fun getRangeInElement(): TextRange = TextRange(0, text.length) + override fun getRangeInElement(): TextRange = getQualifiedNameRange().toTextRange() override fun getCanonicalText(): String = text @@ -59,9 +60,13 @@ abstract class AwDescElementImplMixin(node: ASTNode) : ASTWrapperPsiElement(node return element is PsiClass && element.qualifiedName == asQualifiedName() } + private fun getQualifiedNameRange(): IntRange { + return (text.indexOf('L') + 1)..(textLength - 2) + } + private fun asQualifiedName(): String? = if (text.length > 1) { - text.substring(1, text.length - 1).replace('/', '.') + text.substring(getQualifiedNameRange()).replace('/', '.').replace('$', '.') } else { null } diff --git a/src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwEntryImplMixin.kt b/src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwEntryImplMixin.kt index d95103558..bddc8a02c 100644 --- a/src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwEntryImplMixin.kt +++ b/src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwEntryImplMixin.kt @@ -20,17 +20,42 @@ package com.demonwav.mcdev.platform.mcp.aw.psi.mixins.impl +import com.demonwav.mcdev.platform.mcp.aw.AwElementFactory import com.demonwav.mcdev.platform.mcp.aw.gen.psi.AwTypes import com.demonwav.mcdev.platform.mcp.aw.psi.mixins.AwEntryMixin import com.intellij.extapi.psi.ASTWrapperPsiElement import com.intellij.lang.ASTNode +import com.intellij.psi.PsiComment import com.intellij.psi.PsiElement +import com.intellij.psi.util.PsiTreeUtil abstract class AwEntryImplMixin(node: ASTNode) : ASTWrapperPsiElement(node), AwEntryMixin { - override val accessKind: String? - get() = findChildByType(AwTypes.ACCESS)?.text + override val accessKind: String + get() = findNotNullChildByType(AwTypes.ACCESS_ELEMENT).text override val targetClassName: String? get() = findChildByType(AwTypes.CLASS_NAME)?.text + + override val comment: PsiComment? + get() = PsiTreeUtil.skipWhitespacesForward(this) as? PsiComment + + override val commentText: String? + get() = comment?.text?.substring(1) + + override fun setComment(text: String?) { + if (text == null) { + comment?.delete() + return + } + + val newComment = AwElementFactory.createComment(project, text) + val existingComment = comment + if (existingComment == null) { + parent.addAfter(newComment, this) + return + } + + existingComment.replace(newComment) + } } diff --git a/src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwFieldEntryImplMixin.kt b/src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwFieldEntryImplMixin.kt index 27cfc3d29..8f263300b 100644 --- a/src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwFieldEntryImplMixin.kt +++ b/src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwFieldEntryImplMixin.kt @@ -22,8 +22,10 @@ package com.demonwav.mcdev.platform.mcp.aw.psi.mixins.impl import com.demonwav.mcdev.platform.mcp.aw.gen.psi.AwTypes import com.demonwav.mcdev.platform.mcp.aw.psi.mixins.AwFieldEntryMixin +import com.demonwav.mcdev.util.MemberReference import com.intellij.lang.ASTNode import com.intellij.psi.PsiElement +import com.intellij.util.resettableLazy abstract class AwFieldEntryImplMixin(node: ASTNode) : AwEntryImplMixin(node), AwFieldEntryMixin { override val fieldName: String? @@ -31,4 +33,18 @@ abstract class AwFieldEntryImplMixin(node: ASTNode) : AwEntryImplMixin(node), Aw override val fieldDescriptor: String? get() = findChildByType(AwTypes.FIELD_DESC)?.text + + private val lazyMemberReference = resettableLazy { + MemberReference( + checkNotNull(fieldName) { "Expected fieldName" }, + owner = checkNotNull(targetClassName) { "Expected targetClassName" }.replace('/', '.') + ) + } + + override val memberReference: MemberReference? by lazyMemberReference + + override fun subtreeChanged() { + super.subtreeChanged() + lazyMemberReference.reset() + } } diff --git a/src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwMemberNameImplMixin.kt b/src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwMemberNameImplMixin.kt index af5b0ecb5..3d8f29d9b 100644 --- a/src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwMemberNameImplMixin.kt +++ b/src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwMemberNameImplMixin.kt @@ -20,26 +20,29 @@ package com.demonwav.mcdev.platform.mcp.aw.psi.mixins.impl +import com.demonwav.mcdev.platform.mcp.aw.DeleteEndOfLineInsertionHandler +import com.demonwav.mcdev.platform.mcp.aw.gen.psi.AwEntry import com.demonwav.mcdev.platform.mcp.aw.gen.psi.AwFieldEntry import com.demonwav.mcdev.platform.mcp.aw.gen.psi.AwMethodEntry -import com.demonwav.mcdev.platform.mcp.aw.psi.mixins.AwEntryMixin import com.demonwav.mcdev.platform.mcp.aw.psi.mixins.AwMemberNameMixin import com.demonwav.mcdev.util.MemberReference import com.demonwav.mcdev.util.cached -import com.intellij.codeInsight.completion.JavaLookupElementBuilder +import com.demonwav.mcdev.util.descriptor +import com.intellij.codeInsight.lookup.LookupElementBuilder import com.intellij.extapi.psi.ASTWrapperPsiElement import com.intellij.lang.ASTNode import com.intellij.openapi.util.TextRange import com.intellij.psi.JavaPsiFacade -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 com.intellij.psi.PsiSubstitutor import com.intellij.psi.util.PsiModificationTracker import com.intellij.psi.util.parentOfType import com.intellij.util.ArrayUtil import com.intellij.util.IncorrectOperationException +import com.intellij.util.PlatformIcons import com.intellij.util.containers.map2Array abstract class AwMemberNameImplMixin(node: ASTNode) : ASTWrapperPsiElement(node), AwMemberNameMixin { @@ -49,7 +52,7 @@ abstract class AwMemberNameImplMixin(node: ASTNode) : ASTWrapperPsiElement(node) override fun getReference(): PsiReference? = this override fun resolve(): PsiElement? = cached(PsiModificationTracker.MODIFICATION_COUNT) { - val entry = this.parentOfType() ?: return@cached null + val entry = this.parentOfType() ?: return@cached null val owner = entry.targetClassName?.replace('/', '.') return@cached when (entry) { is AwMethodEntry -> { @@ -69,7 +72,7 @@ abstract class AwMemberNameImplMixin(node: ASTNode) : ASTWrapperPsiElement(node) } override fun getVariants(): Array<*> { - val entry = this.parentOfType() ?: return ArrayUtil.EMPTY_OBJECT_ARRAY + val entry = this.parentOfType() ?: return ArrayUtil.EMPTY_OBJECT_ARRAY val targetClassName = entry.targetClassName?.replace('/', '.')?.replace('$', '.') ?: return ArrayUtil.EMPTY_OBJECT_ARRAY val targetClass = JavaPsiFacade.getInstance(project)?.findClass(targetClassName, resolveScope) @@ -77,13 +80,29 @@ abstract class AwMemberNameImplMixin(node: ASTNode) : ASTWrapperPsiElement(node) return when (entry) { is AwMethodEntry -> targetClass.methods.map2Array(::methodLookupElement) - is AwFieldEntry -> targetClass.fields + is AwFieldEntry -> targetClass.fields.map2Array(::fieldLookupElement) else -> ArrayUtil.EMPTY_OBJECT_ARRAY } } - private fun methodLookupElement(it: PsiMethod) = - JavaLookupElementBuilder.forMethod(it, if (it.isConstructor) "" else it.name, PsiSubstitutor.EMPTY, null) + private fun methodLookupElement(method: PsiMethod): LookupElementBuilder { + var methodName = if (method.isConstructor) "" else method.name + return LookupElementBuilder.create("$methodName ${method.descriptor}") + .withPsiElement(method) + .withPresentableText(method.name) + .withTailText("(${method.parameterList.parameters.joinToString(", ") { it.type.presentableText }})", true) + .withIcon(PlatformIcons.METHOD_ICON) + .withInsertHandler(DeleteEndOfLineInsertionHandler) + } + + private fun fieldLookupElement(field: PsiField): LookupElementBuilder { + return LookupElementBuilder.create("${field.name} ${field.descriptor}") + .withPsiElement(field) + .withPresentableText(field.name) + .withIcon(PlatformIcons.FIELD_ICON) + .withTypeText(field.type.presentableText, true) + .withInsertHandler(DeleteEndOfLineInsertionHandler) + } override fun getRangeInElement(): TextRange = TextRange(0, text.length) @@ -98,7 +117,10 @@ abstract class AwMemberNameImplMixin(node: ASTNode) : ASTWrapperPsiElement(node) } override fun isReferenceTo(element: PsiElement): Boolean { - return element is PsiClass && element.qualifiedName == text.replace('/', '.') + return when (val memberName = text) { + "" -> element is PsiMethod && element.isConstructor + else -> element is PsiMember && element.name == memberName + } } override fun isSoft(): Boolean = false diff --git a/src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwMethodEntryImplMixin.kt b/src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwMethodEntryImplMixin.kt index 962b5d19e..d44b8b1e1 100644 --- a/src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwMethodEntryImplMixin.kt +++ b/src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwMethodEntryImplMixin.kt @@ -22,8 +22,10 @@ package com.demonwav.mcdev.platform.mcp.aw.psi.mixins.impl import com.demonwav.mcdev.platform.mcp.aw.gen.psi.AwTypes import com.demonwav.mcdev.platform.mcp.aw.psi.mixins.AwMethodEntryMixin +import com.demonwav.mcdev.util.MemberReference import com.intellij.lang.ASTNode import com.intellij.psi.PsiElement +import com.intellij.util.resettableLazy abstract class AwMethodEntryImplMixin(node: ASTNode) : AwEntryImplMixin(node), AwMethodEntryMixin { override val methodName: String? @@ -31,4 +33,19 @@ abstract class AwMethodEntryImplMixin(node: ASTNode) : AwEntryImplMixin(node), A override val methodDescriptor: String? get() = findChildByType(AwTypes.METHOD_DESC)?.text + + private val lazyMemberReference = resettableLazy { + MemberReference( + checkNotNull(methodName) { "Expected methodName" }, + descriptor = checkNotNull(methodDescriptor) { "Expected methodDescriptor" }, + owner = checkNotNull(targetClassName) { "Expected targetClassName" }.replace('/', '.') + ) + } + + override val memberReference: MemberReference? by lazyMemberReference + + override fun subtreeChanged() { + super.subtreeChanged() + lazyMemberReference.reset() + } } diff --git a/src/main/kotlin/util/utils.kt b/src/main/kotlin/util/utils.kt index cb984ef09..38ee7ec2a 100644 --- a/src/main/kotlin/util/utils.kt +++ b/src/main/kotlin/util/utils.kt @@ -42,6 +42,7 @@ import com.intellij.openapi.roots.libraries.LibraryKindRegistry import com.intellij.openapi.util.Computable import com.intellij.openapi.util.Condition import com.intellij.openapi.util.Ref +import com.intellij.openapi.util.TextRange import com.intellij.openapi.util.ThrowableComputable import com.intellij.openapi.util.text.StringUtil import com.intellij.pom.java.LanguageLevel @@ -425,3 +426,5 @@ inline fun > enumValueOfOrNull(str: String): T? { null } } + +fun IntRange.toTextRange() = TextRange(this.start, this.last + 1) diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index fd4009daf..1d70a202f 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -648,7 +648,11 @@ implementationClass="com.demonwav.mcdev.platform.mcp.aw.AwSyntaxHighlighterFactory"/> + + + + + + + diff --git a/src/test/kotlin/framework/ProjectBuilder.kt b/src/test/kotlin/framework/ProjectBuilder.kt index 5919ca032..a634a0ed6 100644 --- a/src/test/kotlin/framework/ProjectBuilder.kt +++ b/src/test/kotlin/framework/ProjectBuilder.kt @@ -51,6 +51,12 @@ class ProjectBuilder(private val fixture: JavaCodeInsightTestFixture, private va configure: Boolean = true, allowAst: Boolean = false, ) = file(path, code, "_at.cfg", configure, allowAst) + fun aw( + path: String, + @Language("Access Widener") code: String, + configure: Boolean = true, + allowAst: Boolean = false, + ) = file(path, code, "accesswidener", configure, allowAst) fun lang( path: String, @Language("MCLang") code: String, diff --git a/src/test/kotlin/platform/mcp/at/inspections/AtUsageInspectionTest.kt b/src/test/kotlin/platform/mcp/at/inspections/AtUsageInspectionTest.kt index 0d32728ac..9abbc978d 100644 --- a/src/test/kotlin/platform/mcp/at/inspections/AtUsageInspectionTest.kt +++ b/src/test/kotlin/platform/mcp/at/inspections/AtUsageInspectionTest.kt @@ -75,10 +75,10 @@ class AtUsageInspectionTest : BaseMinecraftTest(PlatformType.MCP, PlatformType.N """ public net.minecraft.Used public net.minecraft.Used usedField - public net.minecraft.Used unusedField + public net.minecraft.Used unusedField public net.minecraft.Used usedMethod()V - public net.minecraft.Used unusedMethod()V - public net.minecraft.server.Unused + public net.minecraft.Used unusedMethod()V + public net.minecraft.server.Unused """.trimIndent() ) } diff --git a/src/test/kotlin/platform/mcp/aw/AwCommenterTest.kt b/src/test/kotlin/platform/mcp/aw/AwCommenterTest.kt new file mode 100644 index 000000000..400c1d297 --- /dev/null +++ b/src/test/kotlin/platform/mcp/aw/AwCommenterTest.kt @@ -0,0 +1,124 @@ +/* + * 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.aw + +import com.demonwav.mcdev.framework.CommenterTest +import com.demonwav.mcdev.framework.EdtInterceptor +import com.demonwav.mcdev.framework.ProjectBuilder +import org.intellij.lang.annotations.Language +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(EdtInterceptor::class) +@DisplayName("Access Widener Commenter Tests") +class AwCommenterTest : CommenterTest() { + + private fun doTest( + @Language("Access Widener") before: String, + @Language("Access Widener") after: String, + ) { + doTest(before, after, ".accesswidener", ProjectBuilder::aw) + } + + @Test + @DisplayName("Single Line Comment Test") + fun singleLineCommentTest() = doTest( + """ + accessWidener v2 named + accessible field net/minecraft/entity/Entity fire Z + accessible field net/minecraft/entity/Entity nextEntityID I + """, + """ + accessWidener v2 named + #accessible field net/minecraft/entity/Entity fire Z + accessible field net/minecraft/entity/Entity nextEntityID I + """, + ) + + @Test + @DisplayName("Multi Line Comment Test") + fun multiLineCommentTest() = doTest( + """ + accessWidener v2 named + accessible method net/minecraft/command/CommandHandler dropFirstString ([Ljava/lang/String;)[Ljava/lang/String; + accessible method net/minecraft/command/CommandHandler getUsernameIndex (Lnet/minecraft/command/ICommand;[Ljava/lang/String;)I + accessible method net/minecraft/command/EntitySelector getArgumentMap (Ljava/lang/String;)Ljava/util/Map; + """, + """ + accessWidener v2 named + #accessible method net/minecraft/command/CommandHandler dropFirstString ([Ljava/lang/String;)[Ljava/lang/String; + #accessible method net/minecraft/command/CommandHandler getUsernameIndex (Lnet/minecraft/command/ICommand;[Ljava/lang/String;)I + accessible method net/minecraft/command/EntitySelector getArgumentMap (Ljava/lang/String;)Ljava/util/Map; + """, + ) + + @Test + @DisplayName("Single Line Uncomment Test") + fun singleLineUncommentTest() = doTest( + """ + accessible field net/minecraft/entity/Entity nextEntityID I + accessible method net/minecraft/entity/Entity func_190531_bD ()I + #accessible method net/minecraft/entity/EntityHanging updateFacingWithBoundingBox (Lnet/minecraft/util/EnumFacing;)V + #accessible field net/minecraft/entity/EntityList stringToIDMapping Ljava/util/Map; + """, + """ + accessible field net/minecraft/entity/Entity nextEntityID I + #accessible method net/minecraft/entity/Entity func_190531_bD ()I + #accessible method net/minecraft/entity/EntityHanging updateFacingWithBoundingBox (Lnet/minecraft/util/EnumFacing;)V + #accessible field net/minecraft/entity/EntityList stringToIDMapping Ljava/util/Map; + """, + ) + + @Test + @DisplayName("Multi Line Uncomment") + fun multiLineUncommentTest() = doTest( + """ + #accessible field net/minecraft/entity/EntityLivingBase potionsNeedUpdate Z + #accessible field net/minecraft/entity/EntityLivingBase entityLivingToAttack Lnet/minecraft/entity/EntityLivingBase; + accessible method net/minecraft/entity/EntityLivingBase canBlockDamageSource (Lnet/minecraft/util/DamageSource;)Z + """, + """ + accessible field net/minecraft/entity/EntityLivingBase potionsNeedUpdate Z + accessible field net/minecraft/entity/EntityLivingBase entityLivingToAttack Lnet/minecraft/entity/EntityLivingBase; + accessible method net/minecraft/entity/EntityLivingBase canBlockDamageSource (Lnet/minecraft/util/DamageSource;)Z + """, + ) + + @Test + @DisplayName("Multi Line Comment With Comments Test") + fun multiLineCommentWithCommentsTest() = doTest( + """ + accessible field net/minecraft/entity/EntityLivingBase HAND_STATES I + #accessible field net/minecraft/entity/EntityLivingBase HEALTH F + accessible field net/minecraft/entity/EntityLivingBase POTION_EFFECTS Ljava/util/List; + #accessible field net/minecraft/entity/EntityLivingBase HIDE_PARTICLES Z + #accessible field net/minecraft/entity/EntityLivingBase ARROW_COUNT_IN_ENTITY # Some comment + """, + """ + accessible field net/minecraft/entity/EntityLivingBase HAND_STATES I + ##accessible field net/minecraft/entity/EntityLivingBase HEALTH F + #accessible field net/minecraft/entity/EntityLivingBase POTION_EFFECTS Ljava/util/List; + ##accessible field net/minecraft/entity/EntityLivingBase HIDE_PARTICLES Z + ##accessible field net/minecraft/entity/EntityLivingBase ARROW_COUNT_IN_ENTITY # Some comment + """, + ) +} diff --git a/src/test/kotlin/platform/mcp/aw/AwCompletionTest.kt b/src/test/kotlin/platform/mcp/aw/AwCompletionTest.kt new file mode 100644 index 000000000..61b547cfb --- /dev/null +++ b/src/test/kotlin/platform/mcp/aw/AwCompletionTest.kt @@ -0,0 +1,160 @@ +/* + * 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.aw + +import com.demonwav.mcdev.framework.BaseMinecraftTest +import com.demonwav.mcdev.framework.assertEqualsUnordered +import com.demonwav.mcdev.platform.PlatformType +import com.demonwav.mcdev.platform.mcp.aw.AwElementFactory.Access +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 +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("Access Widener Completion Tests") +class AwCompletionTest : BaseMinecraftTest(PlatformType.MCP, PlatformType.FABRIC) { + + @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() + ) + } + } + + private fun doCompletionTest( + @Language("Access Widener") before: String, + @Language("Access Widener") after: String, + lookupToUse: String? = null + ) { + fixture.configureByText("test.accesswidener", 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("Header Lookup Elements In Empty File") + fun headerLookupElements() { + fixture.configureByText("test.accesswidener", "") + val lookupElements = fixture.completeBasic() + val lookupStrings = lookupElements.map { it.lookupString } + val expectedStrings = setOf("accessWidener v1 named", "accessWidener v2 named") + assertEqualsUnordered(expectedStrings, lookupStrings) + } + + @Test + @DisplayName("Access Lookup Elements") + fun accessLookupElements() { + fixture.configureByText("test.accesswidener", "accessWidener v2 named\n") + val lookupElements = fixture.completeBasic() + val lookupStrings = lookupElements.map { it.lookupString } + val expectedStrings = Access.entries.map { it.text } + assertEqualsUnordered(expectedStrings, lookupStrings) + } + + @Test + @DisplayName("Target Kind Lookup Elements") + fun targetKindLookupElements() { + fixture.configureByText("test.accesswidener", "accessible ") + val lookupElements = fixture.completeBasic() + val lookupStrings = lookupElements.map { it.lookupString } + val expectedStrings = listOf("class", "method", "field") + assertEqualsUnordered(expectedStrings, lookupStrings) + } + + @Test + @DisplayName("Field Lookup Elements") + fun fieldLookupElements() { + fixture.configureByText("test.accesswidener", "accessible field net/minecraft/Minecraft ") + val lookupElements = fixture.completeBasic() + val lookupStrings = lookupElements.map { it.lookupString } + val expectedStrings = setOf("privString Ljava/lang/String;") + assertEqualsUnordered(expectedStrings, lookupStrings) + } + + @Test + @DisplayName("Method Lookup Elements") + fun methodLookupElements() { + fixture.configureByText("test.accesswidener", "accessible method net/minecraft/Minecraft ") + val lookupElements = fixture.completeBasic() + val lookupStrings = lookupElements.map { it.lookupString } + val expectedStrings = setOf("add (I)V", "copy (L;)V", "method ()V", "overloaded ()V", "overloaded (Ljava/lang/String;)V") + assertEqualsUnordered(expectedStrings, lookupStrings) + } + + @Test + @DisplayName("Field Name Completion") + fun fieldNameCompletion() { + doCompletionTest( + "accessible field net/minecraft/Minecraft privS", + "accessible field net/minecraft/Minecraft privString Ljava/lang/String;" + ) + } + + @Test + @DisplayName("Method Name Completion") + fun methodNameCompletion() { + doCompletionTest( + "accessible method net/minecraft/Minecraft add", + "accessible method net/minecraft/Minecraft add (I)V" + ) + } + + @Test + @DisplayName("Method Name Completion Cleaning End Of Line") + fun methodNameCompletionCleaningEndOfLine() { + doCompletionTest( + "accessible method net/minecraft/Minecraft overloaded (Ljava/some)V invalid; stuff", + "accessible method net/minecraft/Minecraft overloaded (Ljava/lang/String;)V", + "overloaded (Ljava/lang/String;)V" + ) + } +} diff --git a/src/test/kotlin/platform/mcp/aw/AwFormatterTest.kt b/src/test/kotlin/platform/mcp/aw/AwFormatterTest.kt new file mode 100644 index 000000000..edca611f6 --- /dev/null +++ b/src/test/kotlin/platform/mcp/aw/AwFormatterTest.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.aw + +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 Widener Tests") +class AwFormatterTest : BaseMinecraftTest() { + + private fun doTest( + @Language("Access Widener") before: String, + @Language("Access Widener") after: String, + ) { + + fixture.configureByText(AwFileType, before) + WriteCommandAction.runWriteCommandAction(fixture.project) { + CodeStyleManager.getInstance(project).reformat(fixture.file) + } + + fixture.checkResult(after) + } + + @Test + @DisplayName("Entry Comment Spacing") + fun entryCommentSpacing() { + doTest("accessible field Test field# A comment", "accessible field Test field # A comment") + } + + @Test + @DisplayName("Single Group Alignment") + fun singleGroupAlignment() { + doTest( + """ + accessible field Test field # A comment + transitive-accessible method AnotherTest method ()V + """.trimIndent(), + """ + accessible field Test field # A comment + transitive-accessible method AnotherTest method ()V + """.trimIndent() + ) + } + + @Test + @DisplayName("Multiple Groups Alignments") + fun multipleGroupsAlignments() { + doTest( + """ + accessWidener v2 named + + accessible field net/minecraft/Group1A field + transitive-extendable method net/minecraft/Group1BCD method ()V + + accessible field net/minecraft/server/Group2A anotherField + extendable method net/minecraft/server/Group2BCD someMethod ()V + # A comment in the middle should not join the two groups + accessible field net/minecraft/world/Group3A anotherField + transitive-extendable method net/minecraft/world/Group2BCD someMethod ()V + """.trimIndent(), + """ + accessWidener v2 named + + accessible field net/minecraft/Group1A field + transitive-extendable method net/minecraft/Group1BCD method ()V + + accessible field net/minecraft/server/Group2A anotherField + extendable method net/minecraft/server/Group2BCD someMethod ()V + # A comment in the middle should not join the two groups + accessible field net/minecraft/world/Group3A anotherField + transitive-extendable method net/minecraft/world/Group2BCD someMethod ()V + """.trimIndent() + ) + } +} diff --git a/src/test/kotlin/platform/mcp/aw/AwReferencesTest.kt b/src/test/kotlin/platform/mcp/aw/AwReferencesTest.kt new file mode 100644 index 000000000..c07a32901 --- /dev/null +++ b/src/test/kotlin/platform/mcp/aw/AwReferencesTest.kt @@ -0,0 +1,133 @@ +/* + * 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.aw + +import com.demonwav.mcdev.framework.BaseMinecraftTest +import com.demonwav.mcdev.platform.PlatformType +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 org.intellij.lang.annotations.Language +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 + +@DisplayName("Access Widener References Tests") +class AwReferencesTest : BaseMinecraftTest(PlatformType.MCP, PlatformType.FABRIC) { + + @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() + ) + } + } + + private inline fun testReferenceAtCaret( + @Language("Access Widener") at: String, + crossinline test: (element: E) -> Unit + ) { + fixture.configureByText("test.accesswidener", at) + runReadAction { + val ref = fixture.getReferenceAtCaretPositionWithAssertion() + val resolved = ref.resolve().also(::assertNotNull)!! + test(assertInstanceOf(E::class.java, resolved)) + } + } + + @Test + @DisplayName("Class Reference") + fun classReference() { + testReferenceAtCaret("accessible field com/demonwav/mcdev/mcp/test/TestLibrary privString Ljava/lang/String;") { clazz -> + val expectedClass = fixture.findClass("com.demonwav.mcdev.mcp.test.TestLibrary") + assertEquals(expectedClass, clazz) + } + } + + @Test + @DisplayName("Field Reference") + fun fieldReference() { + testReferenceAtCaret("accessible field com/demonwav/mcdev/mcp/test/TestLibrary privString Ljava/lang/String;") { 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("accessible method 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( + "accessible method 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( + "accessible method 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( + "accessible method com/demonwav/mcdev/mcp/test/TestLibrary copy (Ljava/lang/String;)V" + ) { clazz -> + val expectedClass = fixture.findClass(CommonClassNames.JAVA_LANG_STRING) + assertEquals(expectedClass, clazz) + } + } +} diff --git a/src/test/kotlin/platform/mcp/aw/inspections/AwDuplicateEntryInspectionTest.kt b/src/test/kotlin/platform/mcp/aw/inspections/AwDuplicateEntryInspectionTest.kt new file mode 100644 index 000000000..88955a9f4 --- /dev/null +++ b/src/test/kotlin/platform/mcp/aw/inspections/AwDuplicateEntryInspectionTest.kt @@ -0,0 +1,59 @@ +/* + * 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.aw.inspections + +import com.demonwav.mcdev.framework.BaseMinecraftTest +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("Access Widener Duplicate Entry Inspection Tests") +class AwDuplicateEntryInspectionTest : BaseMinecraftTest() { + + @Test + @DisplayName("Duplicate Entries") + fun duplicateEntries() { + buildProject { + aw( + "test.accesswidener", + """ + accessible class test/value/UniqueClass + accessible class test/value/DuplicateClass + accessible class test/value/DuplicateClass + + accessible field test/value/UniqueClass field I + accessible field test/value/DuplicateClass field I + accessible field test/value/DuplicateClass field I + + accessible method test/value/UniqueClass method()V + accessible method test/value/DuplicateClass method()V + accessible method test/value/DuplicateClass method()V + + accessible method test/value/UniqueClass method(II)V + accessible method test/value/DuplicateClass method(II)V + accessible method test/value/DuplicateClass method(II)V + """.trimIndent() + ) + } + + fixture.enableInspections(DuplicateAwEntryInspection::class.java) + fixture.checkHighlighting() + } +} diff --git a/src/test/kotlin/platform/mcp/aw/inspections/AwInspectionSuppressorTest.kt b/src/test/kotlin/platform/mcp/aw/inspections/AwInspectionSuppressorTest.kt new file mode 100644 index 000000000..ccced73d3 --- /dev/null +++ b/src/test/kotlin/platform/mcp/aw/inspections/AwInspectionSuppressorTest.kt @@ -0,0 +1,132 @@ +/* + * 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.aw.inspections + +import com.demonwav.mcdev.framework.BaseMinecraftTest +import com.demonwav.mcdev.framework.testInspectionFix +import com.demonwav.mcdev.platform.mcp.aw.AwFileType +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("Access Widener Inspection Suppressor Tests") +class AwInspectionSuppressorTest : BaseMinecraftTest() { + + @Test + @DisplayName("Entry-Level Suppress") + fun entryLevelSuppress() { + fixture.configureByText( + "test.accesswidener", + """ + accessible class Unresolved # Suppress:AwUnresolvedReference + accessible class Unresolved + """.trimIndent() + ) + + fixture.enableInspections(AwUnresolvedReferenceInspection::class.java) + fixture.checkHighlighting() + } + + @Test + @DisplayName("Entry-Level Suppress Fix") + fun entryLevelSuppressFix() { + fixture.enableInspections(AwUnresolvedReferenceInspection::class.java) + testInspectionFix( + fixture, + "Suppress AwUnresolvedReference for entry", + AwFileType, + "accessible class Unresolved", + "accessible class Unresolved # Suppress:AwUnresolvedReference" + ) + } + + @Test + @DisplayName("File-Level Suppress") + fun fileLevelSuppress() { + fixture.configureByText( + "test.accesswidener", + """ + # Suppress:AwUnresolvedReference + accessible class Unresolved + accessible class Unresolved + """.trimIndent() + ) + + fixture.enableInspections(AwUnresolvedReferenceInspection::class.java) + fixture.checkHighlighting() + } + + @Test + @DisplayName("File-Level Suppress Fix With No Existing Comments") + fun fileLevelSuppressFixNoComments() { + fixture.enableInspections(AwUnresolvedReferenceInspection::class.java) + testInspectionFix( + fixture, + "Suppress AwUnresolvedReference for file", + AwFileType, + "accessible class Unresolved", + """ + # Suppress:AwUnresolvedReference + accessible class Unresolved + """.trimIndent() + ) + } + + @Test + @DisplayName("File-Level Suppress Fix With Unrelated Comment") + fun fileLevelSuppressFixWithUnrelatedComment() { + fixture.enableInspections(AwUnresolvedReferenceInspection::class.java) + testInspectionFix( + fixture, + "Suppress AwUnresolvedReference for file", + AwFileType, + """ + # This is a header comment + accessible class Unresolved + """.trimIndent(), + """ + # This is a header comment + # Suppress:AwUnresolvedReference + accessible class Unresolved + """.trimIndent() + ) + } + + @Test + @DisplayName("File-Level Suppress Fix With Existing Suppress") + fun fileLevelSuppressFixWithExistingSuppress() { + fixture.enableInspections(AwUnresolvedReferenceInspection::class.java) + testInspectionFix( + fixture, + "Suppress AwUnresolvedReference for file", + AwFileType, + """ + # This is a header comment + # Suppress:AwUsage + accessible class Unresolved + """.trimIndent(), + """ + # This is a header comment + # Suppress:AwUsage,AwUnresolvedReference + accessible class Unresolved + """.trimIndent() + ) + } +} diff --git a/src/test/kotlin/platform/mcp/aw/inspections/AwUsageInspectionTest.kt b/src/test/kotlin/platform/mcp/aw/inspections/AwUsageInspectionTest.kt new file mode 100644 index 000000000..eb6ff0868 --- /dev/null +++ b/src/test/kotlin/platform/mcp/aw/inspections/AwUsageInspectionTest.kt @@ -0,0 +1,88 @@ +/* + * 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.aw.inspections + +import com.demonwav.mcdev.framework.BaseMinecraftTest +import com.demonwav.mcdev.platform.PlatformType +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("Access Widener Usage Inspection Tests") +class AwUsageInspectionTest : BaseMinecraftTest(PlatformType.MCP, PlatformType.FABRIC) { + + @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 + ) + aw( + "test.accesswidener", + """ + accessWidener v2 named + + accessible class net/minecraft/Used + accessible field net/minecraft/Used usedField I + accessible field net/minecraft/Used unusedField I + accessible method net/minecraft/Used usedMethod ()V + accessible method net/minecraft/Used unusedMethod ()V + accessible class net/minecraft/server/Unused + """.trimIndent() + ) + } + + fixture.enableInspections(AwUsageInspection::class.java) + fixture.checkHighlighting() + } +} From a6a4cacd65401a9486d216afd66fe8e28f4151c4 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Mon, 16 Dec 2024 20:39:45 +0100 Subject: [PATCH 22/22] Do not throw errors when computing AW entry MemberReference --- .../mcp/aw/psi/mixins/impl/AwClassEntryImplMixin.kt | 3 ++- .../mcp/aw/psi/mixins/impl/AwFieldEntryImplMixin.kt | 7 +++---- .../mcp/aw/psi/mixins/impl/AwMethodEntryImplMixin.kt | 9 ++++----- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwClassEntryImplMixin.kt b/src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwClassEntryImplMixin.kt index 13425bfdf..6a3f727e1 100644 --- a/src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwClassEntryImplMixin.kt +++ b/src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwClassEntryImplMixin.kt @@ -29,7 +29,8 @@ import com.intellij.util.resettableLazy abstract class AwClassEntryImplMixin(node: ASTNode) : AwEntryImplMixin(node), AwClassEntry, AwClassEntryMixin { private val lazyMemberReference = resettableLazy { - MemberReference("", owner = checkNotNull(targetClassName) { "Expected targetClassName" }.replace('/', '.')) + val owner = targetClassName?.replace('/', '.') ?: return@resettableLazy null + MemberReference("", owner = owner) } override val memberReference: MemberReference? by lazyMemberReference diff --git a/src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwFieldEntryImplMixin.kt b/src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwFieldEntryImplMixin.kt index 8f263300b..c613c0bf5 100644 --- a/src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwFieldEntryImplMixin.kt +++ b/src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwFieldEntryImplMixin.kt @@ -35,10 +35,9 @@ abstract class AwFieldEntryImplMixin(node: ASTNode) : AwEntryImplMixin(node), Aw get() = findChildByType(AwTypes.FIELD_DESC)?.text private val lazyMemberReference = resettableLazy { - MemberReference( - checkNotNull(fieldName) { "Expected fieldName" }, - owner = checkNotNull(targetClassName) { "Expected targetClassName" }.replace('/', '.') - ) + val name = fieldName ?: return@resettableLazy null + val owner = targetClassName?.replace('/', '.') ?: return@resettableLazy null + MemberReference(name, owner = owner) } override val memberReference: MemberReference? by lazyMemberReference diff --git a/src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwMethodEntryImplMixin.kt b/src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwMethodEntryImplMixin.kt index d44b8b1e1..c1c1551fd 100644 --- a/src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwMethodEntryImplMixin.kt +++ b/src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwMethodEntryImplMixin.kt @@ -35,11 +35,10 @@ abstract class AwMethodEntryImplMixin(node: ASTNode) : AwEntryImplMixin(node), A get() = findChildByType(AwTypes.METHOD_DESC)?.text private val lazyMemberReference = resettableLazy { - MemberReference( - checkNotNull(methodName) { "Expected methodName" }, - descriptor = checkNotNull(methodDescriptor) { "Expected methodDescriptor" }, - owner = checkNotNull(targetClassName) { "Expected targetClassName" }.replace('/', '.') - ) + val name = methodName ?: return@resettableLazy null + val desc = methodDescriptor ?: return@resettableLazy null + val owner = targetClassName?.replace('/', '.') ?: return@resettableLazy null + MemberReference(name, desc, owner) } override val memberReference: MemberReference? by lazyMemberReference