diff --git a/changelog.md b/changelog.md index 6eeb565ef..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 diff --git a/src/main/grammars/AtLexer.flex b/src/main/grammars/AtLexer.flex index d5e1ad8f0..fb5e522eb 100644 --- a/src/main/grammars/AtLexer.flex +++ b/src/main/grammars/AtLexer.flex @@ -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" 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/actions/CopyNeoForgeAtAction.kt b/src/main/kotlin/platform/mcp/actions/CopyNeoForgeAtAction.kt new file mode 100644 index 000000000..f89c9b183 --- /dev/null +++ b/src/main/kotlin/platform/mcp/actions/CopyNeoForgeAtAction.kt @@ -0,0 +1,98 @@ +/* + * 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.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.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 + return data.instance.usesSrgMemberNames() == false + } + + 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") + } +} diff --git a/src/main/kotlin/platform/mcp/at/AtFile.kt b/src/main/kotlin/platform/mcp/at/AtFile.kt index d99d7666c..a18ec4570 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().map { 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/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/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/main/kotlin/platform/mcp/at/AtReferenceContributor.kt b/src/main/kotlin/platform/mcp/at/AtReferenceContributor.kt new file mode 100644 index 000000000..6725921f3 --- /dev/null +++ b/src/main/kotlin/platform/mcp/at/AtReferenceContributor.kt @@ -0,0 +1,315 @@ +/* + * 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.util.MemberReference +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) ?: return ArrayUtil.EMPTY_OBJECT_ARRAY + val useSrg = instance.usesSrgMemberNames() == true + 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) } + } + + 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})".takeIf { useSrg }, true) + .withInsertHandler(AtClassMemberInsertionHandler(field.name.takeIf { useSrg })) + 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})".takeIf { useSrg }, true) + .withInsertHandler(AtClassMemberInsertionHandler(method.name.takeIf { useSrg })) + 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.usesSrgMemberNames() != 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.usesSrgMemberNames() != 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/AtUsageInspection.kt b/src/main/kotlin/platform/mcp/at/AtUsageInspection.kt deleted file mode 100644 index ccba93f39..000000000 --- a/src/main/kotlin/platform/mcp/at/AtUsageInspection.kt +++ /dev/null @@ -1,80 +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.AtEntry -import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtFieldName -import com.demonwav.mcdev.platform.mcp.at.gen.psi.AtFunction -import com.intellij.codeInspection.LocalInspectionTool -import com.intellij.codeInspection.ProblemHighlightType -import com.intellij.codeInspection.ProblemsHolder -import com.intellij.openapi.module.ModuleUtilCore -import com.intellij.psi.PsiElement -import com.intellij.psi.PsiElementVisitor -import com.intellij.psi.search.GlobalSearchScope -import com.intellij.psi.search.searches.ReferencesSearch - -class AtUsageInspection : LocalInspectionTool() { - - override fun getStaticDescription(): String { - return "The declared access transformer is never used" - } - - override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { - return object : PsiElementVisitor() { - override fun visitElement(element: PsiElement) { - if (element !is AtEntry) { - 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 -> - 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, - ) - } - } - } -} 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..dcfddd696 --- /dev/null +++ b/src/main/kotlin/platform/mcp/at/at-utils.kt @@ -0,0 +1,38 @@ +/* + * 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.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? { + if (!this.isOfType(NeoForgeModuleType)) { + return this.isOfType(ForgeModuleType) + } + + val mcpModule = this.getModuleOfType(McpModuleType) ?: return null + val mcVersion = mcpModule.getSettings().minecraftVersion?.let(SemanticVersion::tryParse) ?: return null + return mcVersion < MinecraftVersions.MC1_20_2 +} 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/format/AtBlock.kt b/src/main/kotlin/platform/mcp/at/format/AtBlock.kt new file mode 100644 index 000000000..8fc9748be --- /dev/null +++ b/src/main/kotlin/platform/mcp/at/format/AtBlock.kt @@ -0,0 +1,105 @@ +/* + * 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.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 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, + null, + alignment, + spacingBuilder, + codeStyleSettings, + 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/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..a15db9f86 --- /dev/null +++ b/src/main/kotlin/platform/mcp/at/format/AtFormattingModelBuilder.kt @@ -0,0 +1,63 @@ +/* + * 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.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, + null, + null, + createSpaceBuilder(codeStyleSettings), + codeStyleSettings, + Alignment.createAlignment(true), + Alignment.createAlignment(true), + ) + return FormattingModelProvider.createFormattingModelForPsiFile( + formattingContext.containingFile, + rootBlock, + codeStyleSettings + ) + } +} diff --git a/src/main/kotlin/platform/mcp/at/inspections/AtDuplicateEntryInspection.kt b/src/main/kotlin/platform/mcp/at/inspections/AtDuplicateEntryInspection.kt new file mode 100644 index 000000000..653fdcbd0 --- /dev/null +++ b/src/main/kotlin/platform/mcp/at/inspections/AtDuplicateEntryInspection.kt @@ -0,0 +1,51 @@ +/* + * 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.inspections + +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 runForWholeFile(): Boolean = true + + 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/inspections/AtInspectionSuppressor.kt b/src/main/kotlin/platform/mcp/at/inspections/AtInspectionSuppressor.kt new file mode 100644 index 000000000..eca8fe115 --- /dev/null +++ b/src/main/kotlin/platform/mcp/at/inspections/AtInspectionSuppressor.kt @@ -0,0 +1,145 @@ +/* + * 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.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 +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 AtInspectionSuppressor : InspectionSuppressor { + + override fun isSuppressedFor(element: PsiElement, toolId: String): Boolean { + val entry = element.parentOfType(withSelf = true) ?: return false + 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 + } + + override fun getSuppressActions( + element: PsiElement?, + toolId: String + ): Array { + if (element == null) { + return SuppressQuickFix.EMPTY_ARRAY + } + + 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 { + + 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" + + override fun invoke( + project: Project, + file: PsiFile, + startElement: PsiElement, + endElement: PsiElement + ) { + 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") + 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: 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 + ): Boolean = context.isValid + + override fun isSuppressAll(): Boolean = false + } +} diff --git a/src/main/kotlin/platform/mcp/at/inspections/AtUnresolvedReferenceInspection.kt b/src/main/kotlin/platform/mcp/at/inspections/AtUnresolvedReferenceInspection.kt new file mode 100644 index 000000000..9fa0dfe3f --- /dev/null +++ b/src/main/kotlin/platform/mcp/at/inspections/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.inspections + +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? = "Reports unresolved AT targets." + + 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/kotlin/platform/mcp/at/inspections/AtUsageInspection.kt b/src/main/kotlin/platform/mcp/at/inspections/AtUsageInspection.kt new file mode 100644 index 000000000..602f1504a --- /dev/null +++ b/src/main/kotlin/platform/mcp/at/inspections/AtUsageInspection.kt @@ -0,0 +1,134 @@ +/* + * 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.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 + +class AtUsageInspection : LocalInspectionTool() { + + override fun getStaticDescription(): String { + return "Reports unused Access Transformer entries" + } + + override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { + return object : AtVisitor() { + + private val fixProvider = { it: AtEntry -> RemoveAtEntryFix.forWholeLine(it, true) } + + override fun visitEntry(entry: AtEntry) { + val function = entry.function + if (function != null) { + checkElement(entry, function, holder, AtFileType, fixProvider) { file, toSkip -> + file.children.asSequence() + .filterIsInstance() + .filter { it != toSkip } + .mapNotNull { it.function?.reference } + } + return + } + + val fieldName = entry.fieldName + if (fieldName != null) { + checkElement(entry, fieldName, holder, AtFileType, fixProvider) + return + } + + // Only check class names if it is the target of the entry + 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 + } + + 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 (reference in entriesReferenceProvider(entry.containingFile, entry)) { + val otherResolved = 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 + } + } + } + + holder.registerProblem(entry, "Entry is never used", fixProvider(entry)) + } + } +} diff --git a/src/main/kotlin/platform/mcp/at/inspections/RemoveAtEntryFix.kt b/src/main/kotlin/platform/mcp/at/inspections/RemoveAtEntryFix.kt new file mode 100644 index 000000000..0221a1495 --- /dev/null +++ b/src/main/kotlin/platform/mcp/at/inspections/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.inspections + +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/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/AtEntryMixin.kt b/src/main/kotlin/platform/mcp/at/psi/mixins/AtEntryMixin.kt index 45599bdb6..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,8 @@ 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 { @@ -35,6 +37,9 @@ interface AtEntryMixin : AtElement { val fieldName: AtFieldName? val function: AtFunction? val keyword: AtKeyword + val comment: PsiComment? + val commentText: String? + val memberReference: MemberReference? fun setEntry(entry: String) fun setKeyword(keyword: AtElementFactory.Keyword) @@ -42,6 +47,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/AtClassNameImplMixin.kt b/src/main/kotlin/platform/mcp/at/psi/mixins/impl/AtClassNameImplMixin.kt index f9a5a8aa0..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 @@ -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.lastOrNull() + } + + override fun getReferences(): Array { + return ReferenceProvidersRegistry.getReferencesFromProviders(this) + } } 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..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,12 +21,26 @@ 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 +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 val memberReference: MemberReference? + get() = (function ?: fieldName ?: asterisk)?.let { AtMemberReference.get(this as AtEntry, it) } + override fun setEntry(entry: String) { replace(AtElementFactory.createEntry(project, entry)) } @@ -53,4 +67,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/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/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..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 @@ -20,7 +20,23 @@ 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 { + val owner = targetClassName?.replace('/', '.') ?: return@resettableLazy null + MemberReference("", owner = owner) + } + + 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..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 @@ -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,17 @@ abstract class AwFieldEntryImplMixin(node: ASTNode) : AwEntryImplMixin(node), Aw override val fieldDescriptor: String? get() = findChildByType(AwTypes.FIELD_DESC)?.text + + private val lazyMemberReference = resettableLazy { + val name = fieldName ?: return@resettableLazy null + val owner = targetClassName?.replace('/', '.') ?: return@resettableLazy null + MemberReference(name, owner = owner) + } + + 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..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 @@ -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,18 @@ abstract class AwMethodEntryImplMixin(node: ASTNode) : AwEntryImplMixin(node), A override val methodDescriptor: String? get() = findChildByType(AwTypes.METHOD_DESC)?.text + + private val lazyMemberReference = resettableLazy { + 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 + + override fun subtreeChanged() { + super.subtreeChanged() + lazyMemberReference.reset() + } } 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/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/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 1e860e1f1..1d70a202f 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -622,11 +622,21 @@ + + + - + + + + + + + + + + implementationClass="com.demonwav.mcdev.platform.mcp.at.inspections.AtUsageInspection"/> + + + + + @@ -1548,6 +1601,11 @@ description="Copy the reference to clipboard in Access Transformer format"> + + + 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/framework/test-util.kt b/src/test/kotlin/framework/test-util.kt index 8242d9de8..984a427ca 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 @@ -40,6 +41,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 +133,59 @@ fun testInspectionFix(fixture: JavaCodeInsightTestFixture, basePath: String, fix fixture.launchAction(intention) 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() + 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..6c6f08d8c --- /dev/null +++ b/src/test/kotlin/platform/mcp/at/AtCompletionTest.kt @@ -0,0 +1,183 @@ +/* + * 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.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.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 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/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() + ) + } +} 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..5d00519f0 --- /dev/null +++ b/src/test/kotlin/platform/mcp/at/AtReferencesTest.kt @@ -0,0 +1,161 @@ +/* + * 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.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.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 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) + } + } +} diff --git a/src/test/kotlin/platform/mcp/at/inspections/AtDuplicateEntryInspectionTest.kt b/src/test/kotlin/platform/mcp/at/inspections/AtDuplicateEntryInspectionTest.kt new file mode 100644 index 000000000..157fa1124 --- /dev/null +++ b/src/test/kotlin/platform/mcp/at/inspections/AtDuplicateEntryInspectionTest.kt @@ -0,0 +1,67 @@ +/* + * 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.inspections + +import com.demonwav.mcdev.framework.BaseMinecraftTest +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("Access Transformer Duplicate Entry Inspection Tests") +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() + } +} diff --git a/src/test/kotlin/platform/mcp/at/inspections/AtInspectionSuppressorTest.kt b/src/test/kotlin/platform/mcp/at/inspections/AtInspectionSuppressorTest.kt new file mode 100644 index 000000000..8b8385fa1 --- /dev/null +++ b/src/test/kotlin/platform/mcp/at/inspections/AtInspectionSuppressorTest.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.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 + +@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() + ) + } +} diff --git a/src/test/kotlin/platform/mcp/at/inspections/AtUsageInspectionTest.kt b/src/test/kotlin/platform/mcp/at/inspections/AtUsageInspectionTest.kt new file mode 100644 index 000000000..9abbc978d --- /dev/null +++ b/src/test/kotlin/platform/mcp/at/inspections/AtUsageInspectionTest.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.inspections + +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() + } +} 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() + } +}