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