From 35297d8d5c09a6578f814d11b45325c744a1de60 Mon Sep 17 00:00:00 2001 From: Marti Raudsepp Date: Sun, 5 Dec 2021 20:54:21 +0200 Subject: [PATCH] Create intention to suppress Mypy errors --- gradle.properties | 2 +- .../pycharm/mypy/checker/Problem.java | 4 +- .../mypy/intentions/TypeIgnoreIntention.java | 144 ++++++++++++++++++ .../pycharm/mypy/MypyBundle.properties | 1 + .../TypeIgnoreIntention/description.html | 6 + .../intentions/TypeIgnoreIntentionTest.java | 92 +++++++++++ 6 files changed, 247 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/leinardi/pycharm/mypy/intentions/TypeIgnoreIntention.java create mode 100644 src/main/resources/intentionDescriptions/TypeIgnoreIntention/description.html create mode 100644 src/test/java/com/leinardi/pycharm/mypy/intentions/TypeIgnoreIntentionTest.java diff --git a/gradle.properties b/gradle.properties index d8ab203..9e06787 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,7 +15,7 @@ # version=0.11.2 ideVersion=PC-2021.2.3 -pythonPlugin=PythonCore:212.5457.59 +pythonPlugin=python-ce sinceBuild=181.5684 untilBuild= downloadIdeaSources=true diff --git a/src/main/java/com/leinardi/pycharm/mypy/checker/Problem.java b/src/main/java/com/leinardi/pycharm/mypy/checker/Problem.java index ff4c275..9889282 100644 --- a/src/main/java/com/leinardi/pycharm/mypy/checker/Problem.java +++ b/src/main/java/com/leinardi/pycharm/mypy/checker/Problem.java @@ -21,6 +21,7 @@ import com.intellij.lang.annotation.HighlightSeverity; import com.intellij.psi.PsiElement; import com.leinardi.pycharm.mypy.MypyBundle; +import com.leinardi.pycharm.mypy.intentions.TypeIgnoreIntention; import com.leinardi.pycharm.mypy.mpapi.SeverityLevel; import org.apache.commons.lang.builder.EqualsBuilder; import org.apache.commons.lang.builder.HashCodeBuilder; @@ -56,7 +57,8 @@ public void createAnnotation(@NotNull AnnotationHolder holder, @NotNull Highligh String message = MypyBundle.message("inspection.message", getMessage()); AnnotationBuilder annotation = holder .newAnnotation(severity, message) - .range(target.getTextRange()); + .range(target.getTextRange()) + .withFix(new TypeIgnoreIntention()); if (isAfterEndOfLine()) { annotation = annotation.afterEndOfLine(); } diff --git a/src/main/java/com/leinardi/pycharm/mypy/intentions/TypeIgnoreIntention.java b/src/main/java/com/leinardi/pycharm/mypy/intentions/TypeIgnoreIntention.java new file mode 100644 index 0000000..36d23ad --- /dev/null +++ b/src/main/java/com/leinardi/pycharm/mypy/intentions/TypeIgnoreIntention.java @@ -0,0 +1,144 @@ +package com.leinardi.pycharm.mypy.intentions; + +import com.intellij.codeInsight.intention.IntentionAction; +import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiComment; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiWhiteSpace; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.util.IncorrectOperationException; +import com.jetbrains.python.PyTokenTypes; +import com.jetbrains.python.psi.LanguageLevel; +import com.jetbrains.python.psi.PyElementGenerator; +import com.leinardi.pycharm.mypy.MypyBundle; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Intention action to append `# type: ignore` comment to suppress Mypy annotations. + */ +public class TypeIgnoreIntention extends PsiElementBaseIntentionAction implements IntentionAction { + @NotNull + public String getText() { + return MypyBundle.message("intention.type-ignore.text"); + } + + /** + * This string is also used for the directory name containing the intention description. + */ + @NotNull + public String getFamilyName() { + return "TypeIgnoreIntention"; + } + + @NotNull + String getCommentText() { + return "# type: ignore"; + } + + /** + * Checks whether this intention is available at the caret offset in file. + */ + public boolean isAvailable(@NotNull Project project, Editor editor, @Nullable PsiElement element) { + if (element == null) { + return false; + } + + PsiElement lastNode = findElementBeforeNewline(element); + if (lastNode == null) { + return false; + } + + if (!isComment(lastNode)) { + // No comment - we can add one. Make sure it has a parent. + return lastNode.getParent() != null; + } else { + PsiComment oldComment = (PsiComment) lastNode; + // Extract the first part of comment, e.g. + // "# type: ignore # Bla bla" -> "# type: ignore" + String firstCommentPart = oldComment.getText().split("(?The Mypy rules file could not be read mypy.exception=The scan failed due to an exception: {0} mypy.exception-with-root-cause=The scan failed due to an exception: {0}
Root cause:
{1} diff --git a/src/main/resources/intentionDescriptions/TypeIgnoreIntention/description.html b/src/main/resources/intentionDescriptions/TypeIgnoreIntention/description.html new file mode 100644 index 0000000..f686553 --- /dev/null +++ b/src/main/resources/intentionDescriptions/TypeIgnoreIntention/description.html @@ -0,0 +1,6 @@ + + +This intention suppresses Mypy type checker errors for the given line by appending a # type: ignore +comment. + + diff --git a/src/test/java/com/leinardi/pycharm/mypy/intentions/TypeIgnoreIntentionTest.java b/src/test/java/com/leinardi/pycharm/mypy/intentions/TypeIgnoreIntentionTest.java new file mode 100644 index 0000000..d629462 --- /dev/null +++ b/src/test/java/com/leinardi/pycharm/mypy/intentions/TypeIgnoreIntentionTest.java @@ -0,0 +1,92 @@ +package com.leinardi.pycharm.mypy.intentions; + +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.testFramework.fixtures.BasePlatformTestCase; +import com.jetbrains.python.PythonLanguage; + +public class TypeIgnoreIntentionTest extends BasePlatformTestCase { + private void assertFixerReplacement(String input, int caretPosition, String output) { + PsiFile file = createLightFile("test.py", PythonLanguage.INSTANCE, input); + PsiElement el = file.findElementAt(caretPosition); + assertNotNull(el); + + TypeIgnoreIntention fixer = new TypeIgnoreIntention(); + assertTrue(fixer.isAvailable(file.getProject(), null, el)); + + fixer.invoke(getProject(), null, el); + assertEquals(output, file.getText()); + } + + @SuppressWarnings("SameParameterValue") + private void assertNotAvailable(String input, int caretPosition) { + PsiFile file = createLightFile("test.py", PythonLanguage.INSTANCE, input); + PsiElement el = file.findElementAt(caretPosition); + assertNotNull(el); + + TypeIgnoreIntention fixer = new TypeIgnoreIntention(); + assertFalse(fixer.isAvailable(file.getProject(), null, el)); + } + + public void testVariableAssignment() { + String input = "import os\n" + + "\n" + + "foo: str = 123\n" + + "print()\n"; + String output = "import os\n" + + "\n" + + "foo: str = 123 # type: ignore\n" + + "print()\n"; + assertFixerReplacement(input, input.indexOf("123"), output); + } + + public void testMultilineCall() { + String input = "print(\n" + + " foo,\n" + + " bar,\n" + + ")\n"; + String output = "print(\n" + + " foo,\n" + + " bar, # type: ignore\n" + + ")\n"; + assertFixerReplacement(input, input.indexOf("bar"), output); + } + + public void testMultilineString() { + // Trying to append comment to multiline string line would cause a syntax error. + String input = "foo: int = \"\"\"\n" + + "\"\"\"\n"; + assertNotAvailable(input, 0); + } + + public void testPrependComment() { + String input = "print() # Hi I'm a comment\n"; + String output = "print() # type: ignore # Hi I'm a comment\n"; + assertFixerReplacement(input, 0, output); + } + + public void testCaretAtEndOfLine() { + String input = "print()\n\n"; + String output = "print() # type: ignore\n\n"; + assertFixerReplacement(input, input.indexOf("\n"), output); + } + + public void testNotAvailable() { + // `type: ignore` comment already present + String input = "print() # type: ignore #Hi I'm a comment\n"; + assertNotAvailable(input, 0); + } + + public void testAlmostEmptyFile() { + String input = "\n"; + assertNotAvailable(input, 0); + } + + public void testEmptyFile() { + String input = ""; + // Can't use assertNotAvailable(): findElementAt() would fail. + PsiFile file = createLightFile("test.py", PythonLanguage.INSTANCE, input); + TypeIgnoreIntention fixer = new TypeIgnoreIntention(); + assertFalse(fixer.isAvailable(file.getProject(), null, file)); + } +}