From 49453df7b39f211f9b7928e2b0fb23b178ba8310 Mon Sep 17 00:00:00 2001 From: Marti Raudsepp Date: Fri, 26 Nov 2021 15:22:39 +0200 Subject: [PATCH 01/11] Fix display of error messages with colon Previously error message was truncated where a colon appeared. Before: ``` Dict entry 0 has incompatible type "int" ``` After ``` Dict entry 0 has incompatible type "int": "str"; expected "int": "int" [dict-item] ``` --- .../pycharm/mypy/mpapi/MypyRunner.java | 7 +++++-- .../pycharm/mypy/mpapi/MypyRunnerTest.java | 20 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 src/test/java/com/leinardi/pycharm/mypy/mpapi/MypyRunnerTest.java diff --git a/src/main/java/com/leinardi/pycharm/mypy/mpapi/MypyRunner.java b/src/main/java/com/leinardi/pycharm/mypy/mpapi/MypyRunner.java index 348be80..cfdfaa2 100644 --- a/src/main/java/com/leinardi/pycharm/mypy/mpapi/MypyRunner.java +++ b/src/main/java/com/leinardi/pycharm/mypy/mpapi/MypyRunner.java @@ -38,6 +38,7 @@ import com.leinardi.pycharm.mypy.util.FileTypes; import com.leinardi.pycharm.mypy.util.Notifications; import org.jdesktop.swingx.util.OS; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.BufferedReader; @@ -304,6 +305,7 @@ private static List runMypy(Project project, Set filesToScan, Str try { process = cmd.createProcess(); InputStream inputStream = process.getInputStream(); + assert (inputStream != null); //TODO check stderr for errors // process.waitFor(); return parseMypyOutput(inputStream); @@ -319,7 +321,8 @@ private static List runMypy(Project project, Set filesToScan, Str } } - private static List parseMypyOutput(InputStream inputStream) throws IOException { + @NotNull + public static List parseMypyOutput(@NotNull InputStream inputStream) throws IOException { ArrayList issues = new ArrayList<>(); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, UTF_8)); String rawLine; @@ -333,7 +336,7 @@ private static List parseMypyOutput(InputStream inputStream) throws IOExc String path = splitPosition[0].trim(); int line = splitPosition.length > 1 ? Integer.parseInt(splitPosition[1].trim()) : 1; int column = splitPosition.length > 2 ? Integer.parseInt(splitPosition[2].trim()) : 1; - String[] splitError = rawLine.substring(typeIndexStart).split(":", -1); + String[] splitError = rawLine.substring(typeIndexStart).split(":", 2); SeverityLevel severityLevel = SeverityLevel.valueOf(splitError[0].trim().toUpperCase()); String message = splitError[1].trim(); issues.add(new Issue(path, line, column, severityLevel, message)); diff --git a/src/test/java/com/leinardi/pycharm/mypy/mpapi/MypyRunnerTest.java b/src/test/java/com/leinardi/pycharm/mypy/mpapi/MypyRunnerTest.java new file mode 100644 index 0000000..88686fa --- /dev/null +++ b/src/test/java/com/leinardi/pycharm/mypy/mpapi/MypyRunnerTest.java @@ -0,0 +1,20 @@ +package com.leinardi.pycharm.mypy.mpapi; + +import org.junit.Assert; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.List; + +public class MypyRunnerTest { + @Test + public void testParseWithColon() throws IOException { + String message = "Dict entry 0 has incompatible type \"int\": \"str\"; expected \"int\": \"int\" [dict-item]"; + String input = "path/testfile.py:1:22: error: " + message + "\n"; + Issue parsed = new Issue("path/testfile.py", 1, 22, SeverityLevel.ERROR, message); + + List results = MypyRunner.parseMypyOutput(new ByteArrayInputStream(input.getBytes())); + Assert.assertArrayEquals(results.toArray(), new Issue[]{parsed}); + } +} From 7f4c35a785c7d2c547a416d433d8e8417181e3de Mon Sep 17 00:00:00 2001 From: Marti Raudsepp Date: Sat, 27 Nov 2021 14:17:09 +0200 Subject: [PATCH 02/11] Make Gradle check work 'gradlew check' was failing for me for half a dozen different reasons. To be completely frank, I have no idea what I'm doing. I'm not familiar with the tools used in the Java world. I just kept applying solutions from StackOverflow until it seemed to work. --- build.gradle | 6 +++--- config/checkstyle/checkstyle.xml | 6 +++--- gradle.properties | 6 +++--- gradle/wrapper/gradle-wrapper.properties | 2 +- .../pycharm/mypy/toolwindow/TogglableTreeNode.java | 7 ++++--- .../com/leinardi/pycharm/mypy/mpapi/MypyRunnerTest.java | 8 +++++++- 6 files changed, 21 insertions(+), 14 deletions(-) diff --git a/build.gradle b/build.gradle index 06179a3..a69f65c 100644 --- a/build.gradle +++ b/build.gradle @@ -33,7 +33,7 @@ buildscript { plugins { id 'org.jetbrains.intellij' version '0.4.17' - id 'net.ltgt.errorprone' version '0.0.16' + id 'net.ltgt.errorprone' version '2.0.2' id 'idea' id 'java' id 'checkstyle' @@ -45,7 +45,7 @@ checkstyle { ignoreFailures = false // Whether this task will ignore failures and continue running the build. configFile rootProject.file('config/checkstyle/checkstyle.xml') // The Checkstyle configuration file to use. - toolVersion = '8.29' // The version of Checkstyle you want to be used + toolVersion = '9.1' // The version of Checkstyle you want to be used } def hasPyCharm = project.hasProperty('pycharmPath') @@ -110,7 +110,7 @@ dependencies { if (hasPyCharm) { compileOnly name: 'pycharm' } - errorprone 'com.google.errorprone:error_prone_core:2.3.1' + errorprone 'com.google.errorprone:error_prone_core:2.10.0' } def getChangelogHtml() { diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index daba2b1..fdef6c0 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -15,13 +15,13 @@ --> + "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN" + "https://checkstyle.org/dtds/configuration_1_3.dtd"> - + diff --git a/gradle.properties b/gradle.properties index 41f45fa..5f99cbd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,7 +14,7 @@ # limitations under the License. # version=0.11.2 -ideaVersion=2018.1.8 +ideaVersion=2021.2.3 sinceBuild=181.5684 untilBuild= downloadIdeaSources=true @@ -24,6 +24,6 @@ publishChannels=Stable # Uncomment one of the following settings: either pycharmPath or pythonPlugin ####################################################################################################################### # Run Mypy plugin inside PyCharm installed into the following path -pycharmPath=/home/leinardi/pycharm-community +# pycharmPath=/home/leinardi/pycharm-community # Run Mypy plugin inside IntelliJ IDEA with the Python plugin. The IDE and the plugin will be downloaded automatically -pythonPlugin=PythonCore:2018.1.181.5087.50 +pythonPlugin=PythonCore:212.5457.59 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b57e667..312cb77 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -16,6 +16,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/src/main/java/com/leinardi/pycharm/mypy/toolwindow/TogglableTreeNode.java b/src/main/java/com/leinardi/pycharm/mypy/toolwindow/TogglableTreeNode.java index b68a240..03c9e3b 100644 --- a/src/main/java/com/leinardi/pycharm/mypy/toolwindow/TogglableTreeNode.java +++ b/src/main/java/com/leinardi/pycharm/mypy/toolwindow/TogglableTreeNode.java @@ -18,8 +18,8 @@ import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.TreeNode; -import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; /** * Tree node with togglable visibility. @@ -44,9 +44,10 @@ public void setVisible(final boolean visible) { this.visible = visible; } - @SuppressWarnings("unchecked") List getAllChildren() { - return Collections.unmodifiableList(children); + return children.stream() + .map(child -> (TogglableTreeNode) child) + .collect(Collectors.toList()); } @Override diff --git a/src/test/java/com/leinardi/pycharm/mypy/mpapi/MypyRunnerTest.java b/src/test/java/com/leinardi/pycharm/mypy/mpapi/MypyRunnerTest.java index 88686fa..5b1148e 100644 --- a/src/test/java/com/leinardi/pycharm/mypy/mpapi/MypyRunnerTest.java +++ b/src/test/java/com/leinardi/pycharm/mypy/mpapi/MypyRunnerTest.java @@ -5,16 +5,22 @@ import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.List; public class MypyRunnerTest { + private static InputStream stringToStream(String s) { + return new ByteArrayInputStream(s.getBytes(StandardCharsets.UTF_8)); + } + @Test public void testParseWithColon() throws IOException { String message = "Dict entry 0 has incompatible type \"int\": \"str\"; expected \"int\": \"int\" [dict-item]"; String input = "path/testfile.py:1:22: error: " + message + "\n"; Issue parsed = new Issue("path/testfile.py", 1, 22, SeverityLevel.ERROR, message); - List results = MypyRunner.parseMypyOutput(new ByteArrayInputStream(input.getBytes())); + List results = MypyRunner.parseMypyOutput(stringToStream(input)); Assert.assertArrayEquals(results.toArray(), new Issue[]{parsed}); } } From 88da5bb77f78fcde38f3e9c5f0d709b5d8243110 Mon Sep 17 00:00:00 2001 From: Marti Raudsepp Date: Sat, 27 Nov 2021 16:56:18 +0200 Subject: [PATCH 03/11] Auto-download PyCharm Community Edition instead of IntelliJ IDEA --- build.gradle | 8 ++++---- gradle.properties | 9 +++------ 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/build.gradle b/build.gradle index a69f65c..07c37b7 100644 --- a/build.gradle +++ b/build.gradle @@ -48,7 +48,7 @@ checkstyle { toolVersion = '9.1' // The version of Checkstyle you want to be used } -def hasPyCharm = project.hasProperty('pycharmPath') +def hasPycharmPath = project.hasProperty('pycharmPath') def hasPythonPlugin = project.hasProperty('pythonPlugin') def props = new Properties() rootProject.file('src/main/resources/com/leinardi/pycharm/mypy/MypyBundle.properties') @@ -73,7 +73,7 @@ intellij { pluginName props.getProperty('plugin.name').toLowerCase().replace(' ', '-') downloadSources Boolean.valueOf(downloadIdeaSources) updateSinceUntilBuild = true - if (hasPyCharm) { + if (hasPycharmPath) { alternativeIdePath pycharmPath } if (hasPythonPlugin) { @@ -99,7 +99,7 @@ repositories { jcenter() maven { url "https://plugins.gradle.org/m2/" } maven { url 'https://dl.bintray.com/jetbrains/intellij-plugin-service' } - if (hasPyCharm) { + if (hasPycharmPath) { flatDir { dirs "$pycharmPath/lib" } @@ -107,7 +107,7 @@ repositories { } dependencies { - if (hasPyCharm) { + if (hasPycharmPath) { compileOnly name: 'pycharm' } errorprone 'com.google.errorprone:error_prone_core:2.10.0' diff --git a/gradle.properties b/gradle.properties index 5f99cbd..f588dc3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,16 +14,13 @@ # limitations under the License. # version=0.11.2 -ideaVersion=2021.2.3 +ideaVersion=PC-2021.2.3 +pythonPlugin=PythonCore:212.5457.59 sinceBuild=181.5684 untilBuild= downloadIdeaSources=true publishUsername=leinardi publishChannels=Stable ####################################################################################################################### -# Uncomment one of the following settings: either pycharmPath or pythonPlugin -####################################################################################################################### -# Run Mypy plugin inside PyCharm installed into the following path +# To run PyCharm from a custom path, uncomment and change the following line # pycharmPath=/home/leinardi/pycharm-community -# Run Mypy plugin inside IntelliJ IDEA with the Python plugin. The IDE and the plugin will be downloaded automatically -pythonPlugin=PythonCore:212.5457.59 From b97f577799d784f09ae2205613a05ab4962b2e0f Mon Sep 17 00:00:00 2001 From: Marti Raudsepp Date: Mon, 29 Nov 2021 15:55:38 +0200 Subject: [PATCH 04/11] ideaVersion -> ideVersion; removed hasPythonPlugin --- build.gradle | 7 ++----- gradle.properties | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/build.gradle b/build.gradle index 07c37b7..4bfb94d 100644 --- a/build.gradle +++ b/build.gradle @@ -49,7 +49,6 @@ checkstyle { } def hasPycharmPath = project.hasProperty('pycharmPath') -def hasPythonPlugin = project.hasProperty('pythonPlugin') def props = new Properties() rootProject.file('src/main/resources/com/leinardi/pycharm/mypy/MypyBundle.properties') .withInputStream { @@ -69,16 +68,14 @@ sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 intellij { - version ideaVersion + version ideVersion pluginName props.getProperty('plugin.name').toLowerCase().replace(' ', '-') downloadSources Boolean.valueOf(downloadIdeaSources) updateSinceUntilBuild = true if (hasPycharmPath) { alternativeIdePath pycharmPath } - if (hasPythonPlugin) { - plugins += [pythonPlugin] - } + plugins += [pythonPlugin] } patchPluginXml { diff --git a/gradle.properties b/gradle.properties index f588dc3..d8ab203 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,7 +14,7 @@ # limitations under the License. # version=0.11.2 -ideaVersion=PC-2021.2.3 +ideVersion=PC-2021.2.3 pythonPlugin=PythonCore:212.5457.59 sinceBuild=181.5684 untilBuild= From 5a919f7a55e4796a938ed7db25aebc5a5be782d8 Mon Sep 17 00:00:00 2001 From: Roberto Leinardi Date: Mon, 29 Nov 2021 18:46:36 +0100 Subject: [PATCH 05/11] Cleaned up .idea folder --- .gitignore | 11 +- .idea/compiler.xml | 13 -- .idea/dictionaries/leinardi.xml | 8 -- .idea/inspectionProfiles/Project_Default.xml | 6 - .idea/uiDesigner.xml | 124 ------------------- .idea/vcs.xml | 7 -- 6 files changed, 4 insertions(+), 165 deletions(-) delete mode 100644 .idea/compiler.xml delete mode 100644 .idea/dictionaries/leinardi.xml delete mode 100644 .idea/inspectionProfiles/Project_Default.xml delete mode 100644 .idea/uiDesigner.xml delete mode 100644 .idea/vcs.xml diff --git a/.gitignore b/.gitignore index f1478b6..4310615 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,8 @@ **/.DS_Store -.idea/workspace.xml -.idea/modules.xml -.idea/misc.xml -.idea/gradle.xml -.idea/libraries/ -.idea/kotlinc.xml -.idea/shelf/ +.idea/* +!.idea/codeStyles/ +!.idea/copyright/ +!.idea/checkstyle-idea.xml **/*.iml .gradle/ **/build/ diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index 60bc341..0000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/dictionaries/leinardi.xml b/.idea/dictionaries/leinardi.xml deleted file mode 100644 index f394628..0000000 --- a/.idea/dictionaries/leinardi.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - leinardi - pylint - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index 2f62a92..0000000 --- a/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml deleted file mode 100644 index e96534f..0000000 --- a/.idea/uiDesigner.xml +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 8306744..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file From ce8e226dc465f04598184dbdec26adf572cad66c Mon Sep 17 00:00:00 2001 From: Marti Raudsepp Date: Thu, 25 Nov 2021 17:46:14 +0200 Subject: [PATCH 06/11] Improve performance, convert real-time scanning to ExternalAnnotator API Using the `ExternalAnnotator` API (instead of `LocalInspectionTool`) behaves a lot better with slow scanners like mypy and fixes reported performance problems with the plugin (fixes #43). Following multiple successive changes to a file, `LocalInspectionTool` can invoke the checker for each modification from multiple threads in parallel, which can bog down the system. `ExternalAnnotator` cancels the previous running check (if any) before running the next one. Modeled after `com.jetbrains.python.validation.Pep8ExternalAnnotator` --- ...MypyInspection.java => MypyAnnotator.java} | 103 +++++++++++++----- .../pycharm/mypy/MypyBatchInspection.java | 21 ++++ .../pycharm/mypy/checker/Problem.java | 38 +++++-- .../pycharm/mypy/mpapi/MypyRunner.java | 1 + src/main/resources/META-INF/plugin.xml | 7 +- 5 files changed, 129 insertions(+), 41 deletions(-) rename src/main/java/com/leinardi/pycharm/mypy/{MypyInspection.java => MypyAnnotator.java} (57%) create mode 100644 src/main/java/com/leinardi/pycharm/mypy/MypyBatchInspection.java diff --git a/src/main/java/com/leinardi/pycharm/mypy/MypyInspection.java b/src/main/java/com/leinardi/pycharm/mypy/MypyAnnotator.java similarity index 57% rename from src/main/java/com/leinardi/pycharm/mypy/MypyInspection.java rename to src/main/java/com/leinardi/pycharm/mypy/MypyAnnotator.java index c550117..f62e64d 100644 --- a/src/main/java/com/leinardi/pycharm/mypy/MypyInspection.java +++ b/src/main/java/com/leinardi/pycharm/mypy/MypyAnnotator.java @@ -16,9 +16,8 @@ package com.leinardi.pycharm.mypy; -import com.intellij.codeInspection.InspectionManager; -import com.intellij.codeInspection.LocalInspectionTool; -import com.intellij.codeInspection.ProblemDescriptor; +import com.intellij.lang.annotation.AnnotationHolder; +import com.intellij.lang.annotation.ExternalAnnotator; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.project.Project; @@ -38,16 +37,44 @@ import java.util.Map; import static com.leinardi.pycharm.mypy.MypyBundle.message; -import static com.leinardi.pycharm.mypy.util.Async.asyncResultOf; import static com.leinardi.pycharm.mypy.util.Notifications.showException; import static com.leinardi.pycharm.mypy.util.Notifications.showWarning; import static java.util.Collections.singletonList; -import static java.util.Optional.ofNullable; -public class MypyInspection extends LocalInspectionTool { +/** + * Using the `ExternalAnnotator` API instead of `LocalInspectionTool`, because the former has better behavior with + * long-running expensive checkers like mypy. Following multiple successive changes to a file, `LocalInspectionTool` + * can invoke the checker for each modification from multiple threads in parallel, which can bog down the system + * (see https://github.com/leinardi/mypy-pycharm/issues/43). + * `ExternalAnnotator` cancels the previous running check (if any) before running the next one. + *

+ * Modeled after `com.jetbrains.python.validation.Pep8ExternalAnnotator` + *

+ * IDE calls methods in three phases: + * 1. `State collectInformation(PsiFile)`: preparation + * 2. `Results doAnnotate(State)`: called in the background. + * 3. `void apply(PsiFile, State, AnnotationHolder)`: apply the annotations to the editor. + */ +public class MypyAnnotator extends ExternalAnnotator { + /* Inner classes storing intermediate results */ + static class State { + PsiFile file; + + public State(PsiFile file) { + this.file = file; + } + } + + static class Results { + List issues; + + public Results(List issues) { + this.issues = issues; + } + } - private static final Logger LOG = Logger.getInstance(MypyInspection.class); - private static final List NO_PROBLEMS_FOUND = Collections.emptyList(); + private static final Logger LOG = Logger.getInstance(MypyAnnotator.class); + private static final Results NO_PROBLEMS_FOUND = new Results(Collections.emptyList()); private static final String ERROR_MESSAGE_INVALID_SYNTAX = "invalid syntax"; private MypyPlugin plugin(final Project project) { @@ -58,20 +85,32 @@ private MypyPlugin plugin(final Project project) { return mypyPlugin; } + /** + * Integration with `MypyBatchInspection` + */ @Override - public ProblemDescriptor[] checkFile(@NotNull final PsiFile psiFile, - @NotNull final InspectionManager manager, - final boolean isOnTheFly) { - return asProblemDescriptors(asyncResultOf(() -> inspectFile(psiFile, manager), NO_PROBLEMS_FOUND), - manager); + public String getPairedBatchInspectionShortName() { + return MypyBatchInspection.INSPECTION_SHORT_NAME; } @Nullable - public List inspectFile(@NotNull final PsiFile psiFile, - @NotNull final InspectionManager manager) { - LOG.debug("Inspection has been invoked."); + @Override + public State collectInformation(@NotNull PsiFile file) { + LOG.debug("Mypy collectInformation " + file.getName() + + " modified=" + file.getModificationStamp() + + " thread=" + Thread.currentThread().getName() + ); - final MypyPlugin plugin = plugin(manager.getProject()); + return new State(file); + } + + @Nullable + @Override + public Results doAnnotate(State state) { + PsiFile psiFile = state.file; + Project project = psiFile.getProject(); + final MypyPlugin plugin = plugin(project); + long startTime = System.currentTimeMillis(); if (!MypyRunner.checkMypyAvailable(plugin.getProject())) { LOG.debug("Scan failed: Mypy not available."); @@ -91,7 +130,10 @@ public List inspectFile(@NotNull final PsiFile psiFile, if (map.isEmpty()) { return NO_PROBLEMS_FOUND; } - return map.get(psiFile); + + long duration = System.currentTimeMillis() - startTime; + LOG.debug("Mypy scan completed: " + psiFile.getName() + " in " + duration + " ms"); + return new Results(map.get(psiFile)); } catch (ProcessCanceledException | AssertionError e) { LOG.debug("Process cancelled when scanning: " + psiFile.getName()); @@ -102,7 +144,7 @@ public List inspectFile(@NotNull final PsiFile psiFile, return NO_PROBLEMS_FOUND; } catch (Throwable e) { - handlePluginException(e, psiFile, manager.getProject()); + handlePluginException(e, psiFile, project); return NO_PROBLEMS_FOUND; } finally { @@ -110,6 +152,20 @@ public List inspectFile(@NotNull final PsiFile psiFile, } } + @Override + public void apply(@NotNull PsiFile file, Results results, @NotNull AnnotationHolder holder) { + if (results == null || !file.isValid()) { + return; + } + + LOG.debug("Applying " + results.issues.size() + " annotations for " + file.getName()); + + for (Problem problem : results.issues) { + LOG.debug(" " + problem.getLine() + ": " + problem.getMessage()); + problem.createAnnotation(holder); + } + } + private void handlePluginException(final Throwable e, final @NotNull PsiFile psiFile, final @NotNull Project project) { @@ -125,13 +181,4 @@ private void handlePluginException(final Throwable e, showException(project, e); } } - - @NotNull - private ProblemDescriptor[] asProblemDescriptors(final List results, final InspectionManager manager) { - return ofNullable(results) - .map(problems -> problems.stream() - .map(problem -> problem.toProblemDescriptor(manager)) - .toArray(ProblemDescriptor[]::new)) - .orElse(ProblemDescriptor.EMPTY_ARRAY); - } } diff --git a/src/main/java/com/leinardi/pycharm/mypy/MypyBatchInspection.java b/src/main/java/com/leinardi/pycharm/mypy/MypyBatchInspection.java new file mode 100644 index 0000000..3dc242c --- /dev/null +++ b/src/main/java/com/leinardi/pycharm/mypy/MypyBatchInspection.java @@ -0,0 +1,21 @@ +package com.leinardi.pycharm.mypy; + +import com.intellij.codeInspection.LocalInspectionTool; +import com.intellij.codeInspection.ex.ExternalAnnotatorBatchInspection; +import org.jetbrains.annotations.NotNull; + +/** + * By itself, the `MypyAnnotator` class does not provide support for the explicit "Inspect code" feature. + * + * This class uses `ExternalAnnotatorBatchInspection` middleware to provides that functionality. + * + * Modeled after `com.jetbrains.python.inspections.PyPep8Inspection` + */ +public class MypyBatchInspection extends LocalInspectionTool implements ExternalAnnotatorBatchInspection { + public static final String INSPECTION_SHORT_NAME = "Mypy"; + + @Override + public @NotNull String getShortName() { + return INSPECTION_SHORT_NAME; + } +} 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 90cebb3..a0b7f92 100644 --- a/src/main/java/com/leinardi/pycharm/mypy/checker/Problem.java +++ b/src/main/java/com/leinardi/pycharm/mypy/checker/Problem.java @@ -16,9 +16,9 @@ package com.leinardi.pycharm.mypy.checker; -import com.intellij.codeInspection.InspectionManager; -import com.intellij.codeInspection.ProblemDescriptor; -import com.intellij.codeInspection.ProblemHighlightType; +import com.intellij.lang.annotation.AnnotationBuilder; +import com.intellij.lang.annotation.AnnotationHolder; +import com.intellij.lang.annotation.HighlightSeverity; import com.intellij.psi.PsiElement; import com.leinardi.pycharm.mypy.MypyBundle; import com.leinardi.pycharm.mypy.mpapi.SeverityLevel; @@ -52,17 +52,35 @@ public Problem(final PsiElement target, this.suppressErrors = suppressErrors; } - @NotNull - public ProblemDescriptor toProblemDescriptor(final InspectionManager inspectionManager) { - return inspectionManager.createProblemDescriptor(target, - MypyBundle.message("inspection.message", getMessage()), - null, problemHighlightType(), false, afterEndOfLine); + public void createAnnotation(@NotNull AnnotationHolder holder) { + String message = MypyBundle.message("inspection.message", getMessage()); + AnnotationBuilder annotation = holder + .newAnnotation(getHighlightSeverity(), message) + .range(target.getTextRange()); + if (isAfterEndOfLine()) { + annotation = annotation.afterEndOfLine(); + } + annotation.create(); } public SeverityLevel getSeverityLevel() { return severityLevel; } + @NotNull + public HighlightSeverity getHighlightSeverity() { + switch (severityLevel) { + case ERROR: + return HighlightSeverity.ERROR; + case WARNING: + case NOTE: // WEAK_WARNING can be a bit difficult to see, use WARNING instead + return HighlightSeverity.WARNING; + default: + assert false : "Unhandled SeverityLevel: " + severityLevel; + } + return null; + } + public int getLine() { return line; } @@ -83,10 +101,6 @@ public boolean isSuppressErrors() { return suppressErrors; } - private ProblemHighlightType problemHighlightType() { - return ProblemHighlightType.GENERIC_ERROR_OR_WARNING; - } - @Override public String toString() { return new ToStringBuilder(this) diff --git a/src/main/java/com/leinardi/pycharm/mypy/mpapi/MypyRunner.java b/src/main/java/com/leinardi/pycharm/mypy/mpapi/MypyRunner.java index cfdfaa2..8af14bb 100644 --- a/src/main/java/com/leinardi/pycharm/mypy/mpapi/MypyRunner.java +++ b/src/main/java/com/leinardi/pycharm/mypy/mpapi/MypyRunner.java @@ -303,6 +303,7 @@ private static List runMypy(Project project, Set filesToScan, Str cmd.setWorkDirectory(project.getBasePath()); final Process process; try { + LOG.info("Running command: " + cmd.getCommandLineString()); process = cmd.createProcess(); InputStream inputStream = process.getInputStream(); assert (inputStream != null); diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index c8d0d96..74f22e7 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -46,11 +46,16 @@ - + + Date: Sat, 4 Dec 2021 16:58:27 +0200 Subject: [PATCH 07/11] Use severity level from inspection profile --- .../leinardi/pycharm/mypy/MypyAnnotator.java | 12 +++++++++++- .../leinardi/pycharm/mypy/checker/Problem.java | 18 ++---------------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/leinardi/pycharm/mypy/MypyAnnotator.java b/src/main/java/com/leinardi/pycharm/mypy/MypyAnnotator.java index f62e64d..05db8bc 100644 --- a/src/main/java/com/leinardi/pycharm/mypy/MypyAnnotator.java +++ b/src/main/java/com/leinardi/pycharm/mypy/MypyAnnotator.java @@ -16,11 +16,15 @@ package com.leinardi.pycharm.mypy; +import com.intellij.codeInsight.daemon.HighlightDisplayKey; +import com.intellij.codeInspection.InspectionProfile; import com.intellij.lang.annotation.AnnotationHolder; import com.intellij.lang.annotation.ExternalAnnotator; +import com.intellij.lang.annotation.HighlightSeverity; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.project.Project; +import com.intellij.profile.codeInspection.InspectionProjectProfileManager; import com.intellij.psi.PsiFile; import com.leinardi.pycharm.mypy.checker.Problem; import com.leinardi.pycharm.mypy.checker.ScanFiles; @@ -160,9 +164,15 @@ public void apply(@NotNull PsiFile file, Results results, @NotNull AnnotationHol LOG.debug("Applying " + results.issues.size() + " annotations for " + file.getName()); + // Get severity from inspection profile + final InspectionProfile profile = + InspectionProjectProfileManager.getInstance(file.getProject()).getCurrentProfile(); + final HighlightDisplayKey key = HighlightDisplayKey.find(MypyBatchInspection.INSPECTION_SHORT_NAME); + HighlightSeverity severity = profile.getErrorLevel(key, file).getSeverity(); + for (Problem problem : results.issues) { LOG.debug(" " + problem.getLine() + ": " + problem.getMessage()); - problem.createAnnotation(holder); + problem.createAnnotation(holder, severity); } } 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 a0b7f92..ff4c275 100644 --- a/src/main/java/com/leinardi/pycharm/mypy/checker/Problem.java +++ b/src/main/java/com/leinardi/pycharm/mypy/checker/Problem.java @@ -52,10 +52,10 @@ public Problem(final PsiElement target, this.suppressErrors = suppressErrors; } - public void createAnnotation(@NotNull AnnotationHolder holder) { + public void createAnnotation(@NotNull AnnotationHolder holder, @NotNull HighlightSeverity severity) { String message = MypyBundle.message("inspection.message", getMessage()); AnnotationBuilder annotation = holder - .newAnnotation(getHighlightSeverity(), message) + .newAnnotation(severity, message) .range(target.getTextRange()); if (isAfterEndOfLine()) { annotation = annotation.afterEndOfLine(); @@ -67,20 +67,6 @@ public SeverityLevel getSeverityLevel() { return severityLevel; } - @NotNull - public HighlightSeverity getHighlightSeverity() { - switch (severityLevel) { - case ERROR: - return HighlightSeverity.ERROR; - case WARNING: - case NOTE: // WEAK_WARNING can be a bit difficult to see, use WARNING instead - return HighlightSeverity.WARNING; - default: - assert false : "Unhandled SeverityLevel: " + severityLevel; - } - return null; - } - public int getLine() { return line; } From 8cca4c97c29cf25d1945f8b230a5f6379c9c8152 Mon Sep 17 00:00:00 2001 From: Marti Raudsepp Date: Thu, 25 Nov 2021 17:33:05 +0200 Subject: [PATCH 08/11] IDE/build configuration --- .idea/inspectionProfiles/Project_Default.xml | 11 +++++++++++ build.gradle | 4 ++++ 2 files changed, 15 insertions(+) create mode 100644 .idea/inspectionProfiles/Project_Default.xml diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..23e18bd --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 4bfb94d..97cbb00 100644 --- a/build.gradle +++ b/build.gradle @@ -78,6 +78,10 @@ intellij { plugins += [pythonPlugin] } +runIde { + systemProperties.put("idea.log.debug.categories", "#com.leinardi.pycharm.mypy") +} + patchPluginXml { version project.property('version') sinceBuild project.property('sinceBuild') From 147a7376fe55dc0423fdd846ba8e657fd466d477 Mon Sep 17 00:00:00 2001 From: Marti Raudsepp Date: Thu, 25 Nov 2021 17:46:01 +0200 Subject: [PATCH 09/11] Fix some dead code --- src/main/java/com/leinardi/pycharm/mypy/mpapi/MypyRunner.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/leinardi/pycharm/mypy/mpapi/MypyRunner.java b/src/main/java/com/leinardi/pycharm/mypy/mpapi/MypyRunner.java index 8af14bb..4d4c0f0 100644 --- a/src/main/java/com/leinardi/pycharm/mypy/mpapi/MypyRunner.java +++ b/src/main/java/com/leinardi/pycharm/mypy/mpapi/MypyRunner.java @@ -280,7 +280,7 @@ private static List runMypy(Project project, Set filesToScan, Str if (daemon) { cmd.addParameter("run"); cmd.addParameter("--"); - cmd.addParameter("``--show-column-numbers"); + cmd.addParameter("--show-column-numbers"); } else { cmd.addParameter("--show-column-numbers"); } From cb8ab289c98bdf930db9ab3cecf99286c7550f9f Mon Sep 17 00:00:00 2001 From: Marti Raudsepp Date: Thu, 25 Nov 2021 18:57:57 +0200 Subject: [PATCH 10/11] Change name & bump version for experimental version --- gradle.properties | 2 +- src/main/resources/META-INF/plugin.xml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle.properties b/gradle.properties index d8ab203..5ebe052 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # -version=0.11.2 +version=0.12.0-alpha1 ideVersion=PC-2021.2.3 pythonPlugin=PythonCore:212.5457.59 sinceBuild=181.5684 diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 74f22e7..0969546 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -15,9 +15,9 @@ --> - com.leinardi.pycharm.mypy - Mypy - Roberto Leinardi + com.leinardi.pycharm.mypy.experimental + Mypy (experimental) + Marti Raudsepp From 8ffaf2ebe622878cfc15806b7a957cf26c8c2287 Mon Sep 17 00:00:00 2001 From: Marti Raudsepp Date: Fri, 26 Nov 2021 13:50:10 +0200 Subject: [PATCH 11/11] Bump minimum IntelliJ version & update fork description According to IntelliJ Plugin Verifier. --- build.gradle | 1 - gradle.properties | 4 ++-- .../com/leinardi/pycharm/mypy/MypyBundle.properties | 6 ++++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index 97cbb00..42fe626 100644 --- a/build.gradle +++ b/build.gradle @@ -87,7 +87,6 @@ patchPluginXml { sinceBuild project.property('sinceBuild') untilBuild project.property('untilBuild') pluginDescription props.getProperty('plugin.Mypy-PyCharm.description') - changeNotes getChangelogHtml() } publishPlugin { diff --git a/gradle.properties b/gradle.properties index 5ebe052..e0ad87e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,10 +13,10 @@ # See the License for the specific language governing permissions and # limitations under the License. # -version=0.12.0-alpha1 +version=0.12.0-alpha2 ideVersion=PC-2021.2.3 pythonPlugin=PythonCore:212.5457.59 -sinceBuild=181.5684 +sinceBuild=192.7142.36 untilBuild= downloadIdeaSources=true publishUsername=leinardi diff --git a/src/main/resources/com/leinardi/pycharm/mypy/MypyBundle.properties b/src/main/resources/com/leinardi/pycharm/mypy/MypyBundle.properties index 491bf5c..7a6a682 100644 --- a/src/main/resources/com/leinardi/pycharm/mypy/MypyBundle.properties +++ b/src/main/resources/com/leinardi/pycharm/mypy/MypyBundle.properties @@ -42,8 +42,10 @@ plugin.status.in-progress.no-file=No file is open for editing plugin.status.in-progress.no-module=The current file being edited does not belong to a module plugin.status.in-progress.project=Scanning current project... plugin.status.aborted=Check was aborted -plugin.Mypy-PyCharm.description=

This plugin provides both real-time \ - and on-demand scanning of Python files with Mypy from within the PyCharm IDE.

+plugin.Mypy-PyCharm.description=\ +

Experimental fork of the original Mypy plugin by Roberto Leinardi with various improvements.

\ +

This plugin provides both real-time and on-demand scanning of Python files with the Mypy type checker from \ + within the PyCharm and IntelliJ IDEs.

plugin.notification.alerts=Mypy Alerts plugin.notification.logging=Mypy Logging plugin.notification.unable-to-run-mypy.subtitle=Unable to run Mypy