-
+
diff --git a/gradle.properties b/gradle.properties
index 41f45fa..e0ad87e 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -13,17 +13,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
-version=0.11.2
-ideaVersion=2018.1.8
-sinceBuild=181.5684
+version=0.12.0-alpha2
+ideVersion=PC-2021.2.3
+pythonPlugin=PythonCore:212.5457.59
+sinceBuild=192.7142.36
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
-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
+# To run PyCharm from a custom path, uncomment and change the following line
+# pycharmPath=/home/leinardi/pycharm-community
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/MypyInspection.java b/src/main/java/com/leinardi/pycharm/mypy/MypyAnnotator.java
similarity index 52%
rename from src/main/java/com/leinardi/pycharm/mypy/MypyInspection.java
rename to src/main/java/com/leinardi/pycharm/mypy/MypyAnnotator.java
index c550117..05db8bc 100644
--- a/src/main/java/com/leinardi/pycharm/mypy/MypyInspection.java
+++ b/src/main/java/com/leinardi/pycharm/mypy/MypyAnnotator.java
@@ -16,12 +16,15 @@
package com.leinardi.pycharm.mypy;
-import com.intellij.codeInspection.InspectionManager;
-import com.intellij.codeInspection.LocalInspectionTool;
-import com.intellij.codeInspection.ProblemDescriptor;
+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;
@@ -38,16 +41,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 +89,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 +134,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 +148,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 +156,26 @@ 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());
+
+ // 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, severity);
+ }
+ }
+
private void handlePluginException(final Throwable e,
final @NotNull PsiFile psiFile,
final @NotNull Project project) {
@@ -125,13 +191,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..ff4c275 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,11 +52,15 @@ 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, @NotNull HighlightSeverity severity) {
+ String message = MypyBundle.message("inspection.message", getMessage());
+ AnnotationBuilder annotation = holder
+ .newAnnotation(severity, message)
+ .range(target.getTextRange());
+ if (isAfterEndOfLine()) {
+ annotation = annotation.afterEndOfLine();
+ }
+ annotation.create();
}
public SeverityLevel getSeverityLevel() {
@@ -83,10 +87,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 348be80..4d4c0f0 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;
@@ -279,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");
}
@@ -302,8 +303,10 @@ 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);
//TODO check stderr for errors
// process.waitFor();
return parseMypyOutput(inputStream);
@@ -319,7 +322,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 +337,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/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/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml
index c8d0d96..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
@@ -46,11 +46,16 @@
-
+
+
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
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..5b1148e
--- /dev/null
+++ b/src/test/java/com/leinardi/pycharm/mypy/mpapi/MypyRunnerTest.java
@@ -0,0 +1,26 @@
+package com.leinardi.pycharm.mypy.mpapi;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+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(stringToStream(input));
+ Assert.assertArrayEquals(results.toArray(), new Issue[]{parsed});
+ }
+}