User: luna Date: 16 Oct 22 20:04 Revision: bd12553dfc310728823c17395a41288cfb604f98 Summary: Access widener improvements (#1899) * go-to support for access widener descriptor elements * method entries in AWs: use descs to disambiguate * add "copy AW entry" action * add "duplicate AW entry" inspection * imports * better balloon position * fix formatting, share balloon code * "Copy AW entry" fix for inaccessible references in fabric projects * cleanup: remove (pre-existing) redundant AwTypedHandlerDelegate.kt * fix formatting in BNF * enable copy AW action in Sponge projects TeamCity URL: https://ci.mcdev.io/viewModification.html?tab=vcsModificationFiles&modId=8162&personal=false Index: src/main/grammars/AwParser.bnf =================================================================== --- src/main/grammars/AwParser.bnf (revision cd35faf457749409437b94b77ff0c27e59057c7b) +++ src/main/grammars/AwParser.bnf (revision bd12553dfc310728823c17395a41288cfb604f98) @@ -88,20 +88,15 @@ ] } -method_desc ::= OPEN_PAREN argument* CLOSE_PAREN return_value +method_desc ::= OPEN_PAREN desc_element* CLOSE_PAREN desc_element -field_desc ::= argument +field_desc ::= desc_element -argument ::= PRIMITIVE | CLASS_VALUE { +desc_element ::= PRIMITIVE | CLASS_VALUE { + mixin= "com.demonwav.mcdev.platform.mcp.aw.psi.mixins.impl.AwDescElementImplMixin" + implements= "com.demonwav.mcdev.platform.mcp.aw.psi.mixins.AwDescElementMixin" methods=[ primitive="PRIMITIVE" classValue="CLASS_VALUE" ] -} +} \ No newline at end of file - -return_value ::= PRIMITIVE | CLASS_VALUE { - methods=[ - primitive="PRIMITIVE" - classValue="CLASS_VALUE" - ] -} Index: src/main/kotlin/platform/mcp/actions/CopyAwAction.kt =================================================================== --- src/main/kotlin/platform/mcp/actions/CopyAwAction.kt (revision bd12553dfc310728823c17395a41288cfb604f98) +++ src/main/kotlin/platform/mcp/actions/CopyAwAction.kt (revision bd12553dfc310728823c17395a41288cfb604f98) @@ -0,0 +1,93 @@ +/* + * Minecraft Dev for IntelliJ + * + * https://minecraftdev.org + * + * Copyright (c) 2022 minecraft-dev + * + * MIT License + */ + +package com.demonwav.mcdev.platform.mcp.actions + +import com.demonwav.mcdev.platform.mcp.actions.SrgActionBase.Companion.showBalloon +import com.demonwav.mcdev.platform.mcp.actions.SrgActionBase.Companion.showSuccessBalloon +import com.demonwav.mcdev.util.descriptor +import com.demonwav.mcdev.util.getDataFromActionEvent +import com.demonwav.mcdev.util.internalName +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.editor.Editor +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiField +import com.intellij.psi.PsiIdentifier +import com.intellij.psi.PsiMember +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiReference +import java.awt.Toolkit +import java.awt.datatransfer.StringSelection + +class CopyAwAction : AnAction() { + + override fun actionPerformed(e: AnActionEvent) { + val data = getDataFromActionEvent(e) ?: return showBalloon("Unknown failure", e) + val editor = data.editor + + val element = data.element + if (element !is PsiIdentifier) { + showBalloon("Invalid element", e) + return + } + + val target = when (val parent = element.parent) { + is PsiMember -> parent + is PsiReference -> parent.resolve() + else -> null + } ?: return showBalloon("Invalid element", e) + + doCopy(target, element, editor, e) + } + + companion object { + + fun doCopy(target: PsiElement, element: PsiElement, editor: Editor?, e: AnActionEvent?) { + when (target) { + is PsiClass -> { + val text = "accessible class ${target.internalName}" + copyToClipboard(editor, element, text) + } + is PsiField -> { + val containing = target.containingClass?.internalName + ?: return maybeShow("Could not get owner of field", e) + val desc = target.type.descriptor + val text = "accessible field $containing ${target.name} $desc" + copyToClipboard(editor, element, text) + } + is PsiMethod -> { + val containing = target.containingClass?.internalName + ?: return maybeShow("Could not get owner of method", e) + val desc = target.descriptor ?: return maybeShow("Could not get descriptor of method", e) + val text = "accessible method $containing ${target.name} $desc" + copyToClipboard(editor, element, text) + } + else -> maybeShow("Invalid element", e) + } + } + + private fun copyToClipboard(editor: Editor?, element: PsiElement, text: String) { + val stringSelection = StringSelection(text) + val clpbrd = Toolkit.getDefaultToolkit().systemClipboard + clpbrd.setContents(stringSelection, null) + if (editor != null) { + showSuccessBalloon(editor, element, "Copied: \"$text\"") + } + } + + private fun maybeShow(text: String, e: AnActionEvent?) { + if (e != null) { + showBalloon(text, e) + } + } + } +} Index: src/main/kotlin/platform/mcp/actions/SrgActionBase.kt =================================================================== --- src/main/kotlin/platform/mcp/actions/SrgActionBase.kt (revision cd35faf457749409437b94b77ff0c27e59057c7b) +++ src/main/kotlin/platform/mcp/actions/SrgActionBase.kt (revision bd12553dfc310728823c17395a41288cfb604f98) @@ -19,6 +19,7 @@ import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.VisualPosition import com.intellij.openapi.ui.popup.Balloon import com.intellij.openapi.ui.popup.JBPopupFactory import com.intellij.openapi.wm.WindowManager @@ -63,38 +64,49 @@ abstract fun withSrgTarget(parent: PsiElement, srgMap: McpSrgMap, e: AnActionEvent, data: ActionData) - protected fun showBalloon(message: String, e: AnActionEvent) { + companion object { + fun showBalloon(message: String, e: AnActionEvent) { - val balloon = JBPopupFactory.getInstance() - .createHtmlTextBalloonBuilder(message, null, LightColors.YELLOW, null) - .setHideOnAction(true) - .setHideOnClickOutside(true) - .setHideOnKeyOutside(true) - .createBalloon() + val balloon = JBPopupFactory.getInstance() + .createHtmlTextBalloonBuilder(message, null, LightColors.YELLOW, null) + .setHideOnAction(true) + .setHideOnClickOutside(true) + .setHideOnKeyOutside(true) + .createBalloon() - val project = e.project ?: return - val statusBar = WindowManager.getInstance().getStatusBar(project) + val project = e.project ?: return + val statusBar = WindowManager.getInstance().getStatusBar(project) - invokeLater { + invokeLater { - balloon.show(RelativePoint.getCenterOf(statusBar.component), Balloon.Position.atRight) + val element = getDataFromActionEvent(e)?.element + val editor = getDataFromActionEvent(e)?.editor + val at = if (element != null && editor != null) { + val pos = editor.offsetToVisualPosition(element.textRange.endOffset - element.textLength / 2) + RelativePoint( + editor.contentComponent, + editor.visualPositionToXY(VisualPosition(pos.line + 1, pos.column)) + ) + } else RelativePoint.getCenterOf(statusBar.component) + balloon.show(at, Balloon.Position.below) - } - } + } + } - protected fun showSuccessBalloon(editor: Editor, element: PsiElement, text: String) { + fun showSuccessBalloon(editor: Editor, element: PsiElement, text: String) { - val balloon = JBPopupFactory.getInstance() - .createHtmlTextBalloonBuilder(text, null, LightColors.SLIGHTLY_GREEN, null) - .setHideOnAction(true) - .setHideOnClickOutside(true) - .setHideOnKeyOutside(true) - .createBalloon() + val balloon = JBPopupFactory.getInstance() + .createHtmlTextBalloonBuilder(text, null, LightColors.SLIGHTLY_GREEN, null) + .setHideOnAction(true) + .setHideOnClickOutside(true) + .setHideOnKeyOutside(true) + .createBalloon() - invokeLater { + invokeLater { - balloon.show( - RelativePoint( + val pos = editor.offsetToVisualPosition(element.textRange.endOffset - element.textLength / 2) + val at = RelativePoint( editor.contentComponent, - editor.visualPositionToXY(editor.offsetToVisualPosition(element.textRange.endOffset)) - ), - Balloon.Position.atRight + editor.visualPositionToXY(VisualPosition(pos.line + 1, pos.column)) - ) + ) + + balloon.show(at, Balloon.Position.below) - } - } -} + } + } + } +} Index: src/main/kotlin/platform/mcp/aw/AwTypedHandlerDelegate.kt =================================================================== --- src/main/kotlin/platform/mcp/aw/AwTypedHandlerDelegate.kt (revision cd35faf457749409437b94b77ff0c27e59057c7b) +++ src/main/kotlin/platform/mcp/aw/AwTypedHandlerDelegate.kt (revision cd35faf457749409437b94b77ff0c27e59057c7b) @@ -1,25 +0,0 @@ -/* - * Minecraft Dev for IntelliJ - * - * https://minecraftdev.org - * - * Copyright (c) 2022 minecraft-dev - * - * MIT License - */ - -package com.demonwav.mcdev.platform.mcp.aw - -import com.intellij.codeInsight.editorActions.TypedHandlerDelegate -import com.intellij.openapi.editor.Editor -import com.intellij.openapi.project.Project -import com.intellij.psi.PsiFile - -class AwTypedHandlerDelegate : TypedHandlerDelegate() { - override fun checkAutoPopup(charTyped: Char, project: Project, editor: Editor, file: PsiFile): Result { - if (file.language == AwLanguage && charTyped == '$') { - return Result.CONTINUE - } - return super.checkAutoPopup(charTyped, project, editor, file) - } -} Index: src/main/kotlin/platform/mcp/aw/fixes/CopyAwAccessibleEntryFix.kt =================================================================== --- src/main/kotlin/platform/mcp/aw/fixes/CopyAwAccessibleEntryFix.kt (revision bd12553dfc310728823c17395a41288cfb604f98) +++ src/main/kotlin/platform/mcp/aw/fixes/CopyAwAccessibleEntryFix.kt (revision bd12553dfc310728823c17395a41288cfb604f98) @@ -0,0 +1,59 @@ +/* + * Minecraft Dev for IntelliJ + * + * https://minecraftdev.org + * + * Copyright (c) 2022 minecraft-dev + * + * MIT License + */ + +package com.demonwav.mcdev.platform.mcp.aw.fixes + +import com.demonwav.mcdev.facet.MinecraftFacet +import com.demonwav.mcdev.platform.fabric.FabricModuleType +import com.demonwav.mcdev.platform.mcp.actions.CopyAwAction +import com.demonwav.mcdev.platform.sponge.SpongeModuleType +import com.demonwav.mcdev.util.findModule +import com.intellij.codeInsight.daemon.QuickFixActionRegistrar +import com.intellij.codeInsight.intention.IntentionAction +import com.intellij.codeInsight.quickfix.UnresolvedReferenceQuickFixProvider +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiJavaCodeReferenceElement + +class CopyAwAccessibleEntryFix(val target: PsiElement, val element: PsiElement) : IntentionAction { + + class Provider : UnresolvedReferenceQuickFixProvider() { + + override fun registerFixes(ref: PsiJavaCodeReferenceElement, registrar: QuickFixActionRegistrar) { + val module = ref.findModule() ?: return + val isApplicable = MinecraftFacet.getInstance(module, FabricModuleType, SpongeModuleType) != null + if (!isApplicable) { + return + } + + val resolve = ref.advancedResolve(true) + val target = resolve.element + if (target != null && !resolve.isAccessible) { + registrar.register(CopyAwAccessibleEntryFix(target, ref)) + } + } + + override fun getReferenceClass(): Class = PsiJavaCodeReferenceElement::class.java + } + + override fun startInWriteAction(): Boolean = false + + override fun getText(): String = "Copy AW entry" + + override fun getFamilyName(): String = "Copy AW entry for inaccessible element" + + override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?): Boolean = true + + override fun invoke(project: Project, editor: Editor?, file: PsiFile?) { + CopyAwAction.doCopy(target, element, editor, null) + } +} Index: src/main/kotlin/platform/mcp/aw/inspections/DuplicateAwEntryInspection.kt =================================================================== --- src/main/kotlin/platform/mcp/aw/inspections/DuplicateAwEntryInspection.kt (revision bd12553dfc310728823c17395a41288cfb604f98) +++ src/main/kotlin/platform/mcp/aw/inspections/DuplicateAwEntryInspection.kt (revision bd12553dfc310728823c17395a41288cfb604f98) @@ -0,0 +1,67 @@ +/* + * Minecraft Dev for IntelliJ + * + * https://minecraftdev.org + * + * Copyright (c) 2022 minecraft-dev + * + * MIT License + */ + +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.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 { + return true + } + + override fun getDisplayName(): String { + return "Duplicate AW entry" + } + + override fun getStaticDescription(): String { + return "Warns when the same element has its accessibility, mutability, " + + "or extensibility changed multiple times in one file." + } +} Index: src/main/kotlin/platform/mcp/aw/psi/mixins/AwDescElementMixin.kt =================================================================== --- src/main/kotlin/platform/mcp/aw/psi/mixins/AwDescElementMixin.kt (revision bd12553dfc310728823c17395a41288cfb604f98) +++ src/main/kotlin/platform/mcp/aw/psi/mixins/AwDescElementMixin.kt (revision bd12553dfc310728823c17395a41288cfb604f98) @@ -0,0 +1,16 @@ +/* + * Minecraft Dev for IntelliJ + * + * https://minecraftdev.org + * + * Copyright (c) 2022 minecraft-dev + * + * MIT License + */ + +package com.demonwav.mcdev.platform.mcp.aw.psi.mixins + +import com.demonwav.mcdev.platform.mcp.aw.psi.AwElement +import com.intellij.psi.PsiReference + +interface AwDescElementMixin : AwElement, PsiReference Index: src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwDescElementImplMixin.kt =================================================================== --- src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwDescElementImplMixin.kt (revision bd12553dfc310728823c17395a41288cfb604f98) +++ src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwDescElementImplMixin.kt (revision bd12553dfc310728823c17395a41288cfb604f98) @@ -0,0 +1,58 @@ +/* + * Minecraft Dev for IntelliJ + * + * https://minecraftdev.org + * + * Copyright (c) 2022 minecraft-dev + * + * MIT License + */ + +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.intellij.extapi.psi.ASTWrapperPsiElement +import com.intellij.lang.ASTNode +import com.intellij.openapi.util.TextRange +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiReference +import com.intellij.psi.util.PsiModificationTracker +import com.intellij.util.IncorrectOperationException + +abstract class AwDescElementImplMixin(node: ASTNode) : ASTWrapperPsiElement(node), AwDescElementMixin { + + override fun getElement(): PsiElement = this + + override fun getReference(): PsiReference? = this + + 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 getCanonicalText(): String = text + + override fun handleElementRename(newElementName: String): PsiElement { + throw IncorrectOperationException() + } + + override fun bindToElement(element: PsiElement): PsiElement { + throw IncorrectOperationException() + } + + override fun isReferenceTo(element: PsiElement): Boolean { + return element is PsiClass && element.qualifiedName == asQualifiedName() + } + + private fun asQualifiedName(): String? = + if (text.length > 1) + text.substring(1, text.length - 1).replace('/', '.') + else null + + override fun isSoft(): Boolean = false +} Index: src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwMemberNameImplMixin.kt =================================================================== --- src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwMemberNameImplMixin.kt (revision cd35faf457749409437b94b77ff0c27e59057c7b) +++ src/main/kotlin/platform/mcp/aw/psi/mixins/impl/AwMemberNameImplMixin.kt (revision bd12553dfc310728823c17395a41288cfb604f98) @@ -40,15 +40,18 @@ override fun resolve(): PsiElement? = cached(PsiModificationTracker.MODIFICATION_COUNT) { val entry = this.parentOfType() ?: return@cached null + val owner = entry.targetClassName?.replace('/', '.') return@cached when (entry) { is AwMethodEntry -> { val name = entry.methodName ?: return@cached null - MemberReference(name, null, entry.targetClassName?.replace('/', '.')) - .resolveMember(project, resolveScope) + val desc = entry.methodDescriptor + MemberReference(name, desc, owner).resolveMember(project, resolveScope) + // fallback if descriptor is invalid + ?: MemberReference(name, null, owner).resolveMember(project, resolveScope) } is AwFieldEntry -> { val name = entry.fieldName ?: return@cached null - MemberReference(name, null, entry.targetClassName?.replace('/', '.')) + MemberReference(name, null, owner) .resolveMember(project, resolveScope) } else -> null Index: src/main/resources/META-INF/plugin.xml =================================================================== --- src/main/resources/META-INF/plugin.xml (revision cd35faf457749409437b94b77ff0c27e59057c7b) +++ src/main/resources/META-INF/plugin.xml (revision bd12553dfc310728823c17395a41288cfb604f98) @@ -308,9 +308,9 @@ - + @@ -575,6 +575,13 @@ level="WARNING" hasStaticDescription="true" implementationClass="com.demonwav.mcdev.platform.mcp.inspections.StackEmptyInspection"/> + @@ -981,5 +988,10 @@ text="Minecraft Class" description="Create skeleton classes used in Minecraft Mods"> + + +