User: joe Date: 12 Nov 26 00:37 Revision: 00e8c96bd7e8e8b7612ddee6b5f67b013ebac087 Summary: Disallow intention actions meant for the parent member from being listed TeamCity URL: http://ci.mcdev.io:80/viewModification.html?tab=vcsModificationFiles&modId=10719&personal=false Index: src/main/kotlin/platform/fabric/annotater/javadoc/AnnotaterJavadocIntentionActionFilter.kt =================================================================== --- src/main/kotlin/platform/fabric/annotater/javadoc/AnnotaterJavadocIntentionActionFilter.kt (revision cbd0999163ddd7aa30b9a226cf1a7ad3648e4b05) +++ src/main/kotlin/platform/fabric/annotater/javadoc/AnnotaterJavadocIntentionActionFilter.kt (revision 00e8c96bd7e8e8b7612ddee6b5f67b013ebac087) @@ -22,7 +22,13 @@ import com.intellij.codeInsight.daemon.impl.IntentionActionFilter import com.intellij.codeInsight.intention.IntentionAction +import com.intellij.codeInsight.intention.IntentionActionDelegate +import com.intellij.codeInsight.intention.IntentionManager +import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction +import com.intellij.modcommand.ActionContext +import com.intellij.openapi.editor.impl.ImaginaryEditor import com.intellij.psi.PsiFile +import com.intellij.psi.PsiNameIdentifierOwner import com.intellij.psi.javadoc.PsiDocComment import com.intellij.psi.util.PsiTreeUtil @@ -41,10 +47,54 @@ } val document = psiFile.viewProvider.document // The caret can be just after the end of a comment - return listOf(offset, offset - 1).any { candidate -> - val element = psiFile.findElementAt(candidate) ?: return@any false - val comment = PsiTreeUtil.getParentOfType(element, PsiDocComment::class.java, false) ?: return@any false - !AnnotaterJavadocInlineEditor.isGuarded(document, comment.textRange) + val comment = listOf(offset, offset - 1).firstNotNullOfOrNull { candidate -> + val element = psiFile.findElementAt(candidate) ?: return@firstNotNullOfOrNull null + PsiTreeUtil.getParentOfType(element, PsiDocComment::class.java, false) + ?.takeUnless { AnnotaterJavadocInlineEditor.isGuarded(document, it.textRange) } + } ?: return false + return !isDeclarationIntention(intentionAction, psiFile, comment) - } + } + + /** + * Java offers some intentions of a declaration anywhere in its doc comment, such as changing its access modifier. + * These are told apart from the doc comment's own intentions by also being available on the declaration's name. + */ + private fun isDeclarationIntention(action: IntentionAction, psiFile: PsiFile, comment: PsiDocComment): Boolean { + if (!isRegisteredIntention(action)) { + // keep quick fixes enabled + return false - } + } + val name = (comment.owner as? PsiNameIdentifierOwner)?.nameIdentifier ?: return false + val editor = NameEditor(psiFile) + editor.caretModel.moveToOffset(name.textRange.startOffset) + val unwrapped = IntentionActionDelegate.unwrap(action) + val modCommand = unwrapped.asModCommandAction() + return try { + when { + unwrapped is PsiElementBaseIntentionAction -> + unwrapped.checkFile(psiFile) && unwrapped.isAvailable(psiFile.project, editor, name) + modCommand != null -> modCommand.getPresentation(ActionContext.from(editor, psiFile)) != null + else -> unwrapped.isAvailable(psiFile.project, editor, psiFile) -} + } + } catch (_: UnsupportedOperationException) { + // The intention needs more of an editor than this has + false + } + } + + private class NameEditor(psiFile: PsiFile) : ImaginaryEditor(psiFile.project, psiFile.viewProvider.document) { + override fun isViewer() = false + + override fun isOneLineMode() = false + } + + private fun isRegisteredIntention(action: IntentionAction): Boolean { + val unwrapped = unwrap(action) + return IntentionManager.getInstance().intentionActions.any { unwrap(it) === unwrapped } + } + + private fun unwrap(action: IntentionAction): Any { + val unwrapped = IntentionActionDelegate.unwrap(action) + return unwrapped.asModCommandAction() ?: unwrapped + } +} Index: src/test/kotlin/platform/fabric/annotater/AnnotaterJavadocEditorTest.kt =================================================================== --- src/test/kotlin/platform/fabric/annotater/AnnotaterJavadocEditorTest.kt (revision cbd0999163ddd7aa30b9a226cf1a7ad3648e4b05) +++ src/test/kotlin/platform/fabric/annotater/AnnotaterJavadocEditorTest.kt (revision 00e8c96bd7e8e8b7612ddee6b5f67b013ebac087) @@ -32,6 +32,8 @@ import com.demonwav.mcdev.platform.fabric.annotater.javadoc.AnnotaterJavadocVirtualFile import com.intellij.application.options.CodeStyle import com.intellij.codeInsight.intention.IntentionAction +import com.intellij.codeInsight.intention.IntentionManager +import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction import com.intellij.codeInsight.lookup.Lookup import com.intellij.codeInsight.template.impl.TemplateManagerImpl import com.intellij.ide.DataManager @@ -49,11 +51,14 @@ 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.PsiFile import com.intellij.psi.PsiMember import com.intellij.psi.PsiMethod import com.intellij.psi.codeStyle.modifier.TransientCodeStyleSettings +import com.intellij.psi.javadoc.PsiDocComment import com.intellij.psi.search.GlobalSearchScope +import com.intellij.psi.util.PsiTreeUtil import java.nio.file.Path import java.util.concurrent.TimeUnit import kotlin.io.path.readText @@ -407,6 +412,34 @@ } @Test + @DisplayName("The documented declaration's intentions aren't offered in its Javadoc, unlike the Javadoc's own") + fun declarationIntentions() { + val javadocIntention = object : PsiElementBaseIntentionAction() { + override fun getText() = "Javadoc intention" + override fun getFamilyName() = text + override fun isAvailable(project: Project, editor: Editor, element: PsiElement) = + PsiTreeUtil.getParentOfType(element, PsiDocComment::class.java) != null + override fun invoke(project: Project, editor: Editor, element: PsiElement) {} + } + IntentionManager.getInstance().addAction(javadocIntention) + Disposer.register(fixture.testRootDisposable) { + IntentionManager.getInstance().unregisterIntention(javadocIntention) + } + fixture.enableInspections(AnnotaterJavadocUnqualifiedReferenceInspection::class) + fixture.openFileInEditor(AnnotaterJavadocVirtualFile(checkout, clampCopy("Clamps a [String] value."), clamp)) + val text = fixture.editor.document.text + fixture.editor.caretModel.moveToOffset(text.indexOf("Clamps")) + var intentions = fixture.availableIntentions.map { it.text } + assertTrue("Javadoc intention" in intentions, intentions.toString()) + assertFalse("Change access modifier" in intentions, intentions.toString()) + assertFalse("Generate overloaded method with default parameter values" in intentions, intentions.toString()) + fixture.editor.caretModel.moveToOffset(text.indexOf("String]")) + intentions = fixture.availableIntentions.map { it.text } + assertTrue("Replace with 'java.lang.String'" in intentions, intentions.toString()) + assertFalse("Change access modifier" in intentions, intentions.toString()) + } + + @Test @DisplayName("Unqualified class references are reported and fixed") fun unqualifiedReferences() { val copy = clampCopy(