PY-20801 "Convert to variable annotation" for simple assignments
authorMikhail Golubev <mikhail.golubev@jetbrains.com>
Thu, 20 Oct 2016 14:57:09 +0000 (17:57 +0300)
committerMikhail Golubev <mikhail.golubev@jetbrains.com>
Mon, 24 Oct 2016 21:03:49 +0000 (00:03 +0300)
14 files changed:
python/resources/intentionDescriptions/PyConvertTypeCommentToVariableAnnotation/after.py.template [new file with mode: 0644]
python/resources/intentionDescriptions/PyConvertTypeCommentToVariableAnnotation/before.py.template [new file with mode: 0644]
python/resources/intentionDescriptions/PyConvertTypeCommentToVariableAnnotation/description.html [new file with mode: 0644]
python/src/META-INF/python-core-common.xml
python/src/com/jetbrains/python/PyBundle.properties
python/src/com/jetbrains/python/codeInsight/intentions/PyConvertTypeCommentToVariableAnnotation.java [new file with mode: 0644]
python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/assignmentWithUnpacking.py [new file with mode: 0644]
python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/badLanguageLevel.py [new file with mode: 0644]
python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/chainedAssignment.py [new file with mode: 0644]
python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/multilineAssignment.py [new file with mode: 0644]
python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/multilineAssignment_after.py [new file with mode: 0644]
python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/simpleAssignment.py [new file with mode: 0644]
python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/simpleAssignment_after.py [new file with mode: 0644]
python/testSrc/com/jetbrains/python/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest.java [new file with mode: 0644]

diff --git a/python/resources/intentionDescriptions/PyConvertTypeCommentToVariableAnnotation/after.py.template b/python/resources/intentionDescriptions/PyConvertTypeCommentToVariableAnnotation/after.py.template
new file mode 100644 (file)
index 0000000..89023c6
--- /dev/null
@@ -0,0 +1 @@
+x: Optional[int] = undefined()
\ No newline at end of file
diff --git a/python/resources/intentionDescriptions/PyConvertTypeCommentToVariableAnnotation/before.py.template b/python/resources/intentionDescriptions/PyConvertTypeCommentToVariableAnnotation/before.py.template
new file mode 100644 (file)
index 0000000..e28b15f
--- /dev/null
@@ -0,0 +1 @@
+x = undefined()  # type: Optional[int]
\ No newline at end of file
diff --git a/python/resources/intentionDescriptions/PyConvertTypeCommentToVariableAnnotation/description.html b/python/resources/intentionDescriptions/PyConvertTypeCommentToVariableAnnotation/description.html
new file mode 100644 (file)
index 0000000..2d034cc
--- /dev/null
@@ -0,0 +1,7 @@
+<html>
+<body>
+<span>
+  This intention convert PEP-484 type comments to Python 3.6 variable annotations
+</span>
+</body>
+</html>
\ No newline at end of file
index 57df91bfea5e950a4ccd25f5c2bc0d176665dae3..a2b5c41eb4f06cc397506de395f4df752d16984c 100644 (file)
       <category>Python</category>
     </intentionAction>
 
+    <intentionAction>
+      <className>com.jetbrains.python.codeInsight.intentions.PyConvertTypeCommentToVariableAnnotation</className>
+      <category>Python</category>
+    </intentionAction>
+
     <testFinder implementation="com.jetbrains.python.codeInsight.testIntegration.PyTestFinder"/>
     <testCreator language="Python" implementationClass="com.jetbrains.python.codeInsight.testIntegration.PyTestCreator"/>
 
index e092447a124b9fc6f6bd03dd44840e63884f4b0b..db09df1ac8994bc1c7c559401b2a966672535152 100644 (file)
@@ -245,9 +245,15 @@ INTN.convert.variadic.param=Convert from variadic to normal parameter(s)
 # PyConvertTripleQuotedStringIntention
 INTN.triple.quoted.string=Convert triple-quoted string to single-quoted string
 
+# PyBaseConvertCollectionLiteralIntention
 INTN.convert.collection.literal.family=Convert collection to {0}
 INTN.convert.collection.literal.text=Convert {0} to {1}
 
+# PyConvertTypeCommentToVariableAnnotation
+INTN.convert.type.comment.to.variable.annotation.family=Convert type comment to variable annotation
+INTN.convert.type.comment.to.variable.annotation.text=Convert to variable annotation
+
+
 INTN.demorgan.law=DeMorgan law
 
 # PyTransformConditionalExpressionIntention
diff --git a/python/src/com/jetbrains/python/codeInsight/intentions/PyConvertTypeCommentToVariableAnnotation.java b/python/src/com/jetbrains/python/codeInsight/intentions/PyConvertTypeCommentToVariableAnnotation.java
new file mode 100644 (file)
index 0000000..8fb2e15
--- /dev/null
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2000-2016 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jetbrains.python.codeInsight.intentions;
+
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiComment;
+import com.intellij.psi.PsiDocumentManager;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.util.IncorrectOperationException;
+import com.jetbrains.python.PyBundle;
+import com.jetbrains.python.codeInsight.PyTypingTypeProvider;
+import com.jetbrains.python.psi.*;
+import com.jetbrains.python.psi.impl.PyPsiUtils;
+import org.jetbrains.annotations.Nls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class PyConvertTypeCommentToVariableAnnotation extends PyBaseIntentionAction {
+  @Override
+  public void doInvoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException {
+    final PsiComment comment = findCommentUnderCaret(editor, file);
+    if (comment != null) {
+      final PyTargetExpression target = findTypeCommentTarget(comment);
+      if (target != null) {
+        final String annotation = PyTypingTypeProvider.getTypeCommentValue(comment.getText());
+        final PsiElement prev = PyPsiUtils.getPrevNonWhitespaceSibling(comment);
+        final int commentStart = prev != null ? prev.getTextRange().getEndOffset() : comment.getTextRange().getStartOffset();
+        final int commentEnd = comment.getTextRange().getEndOffset();
+        final Document document = editor.getDocument();
+        runWithDocumentReleasedAndCommitted(project, document, () -> {
+          document.deleteString(commentStart, commentEnd);
+          document.insertString(target.getTextRange().getEndOffset(), ": " + annotation);
+        });
+      }
+    }
+  }
+
+  @NotNull
+  @Override
+  public String getText() {
+    return PyBundle.message("INTN.convert.type.comment.to.variable.annotation.text");
+  }
+
+  @Nls
+  @NotNull
+  @Override
+  public String getFamilyName() {
+    return PyBundle.message("INTN.convert.type.comment.to.variable.annotation.family");
+  }
+
+  @Override
+  public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) {
+    if (file instanceof PyFile && LanguageLevel.forElement(file).isAtLeast(LanguageLevel.PYTHON36)) {
+      final PsiComment comment = findCommentUnderCaret(editor, file);
+      return comment != null && isSuitableTypeComment(comment);
+    }
+    return false;
+  }
+
+  @Nullable
+  private static PsiComment findCommentUnderCaret(@NotNull Editor editor, @NotNull PsiFile file) {
+    final PsiElement element = file.findElementAt(editor.getCaretModel().getOffset());
+    return PsiTreeUtil.getParentOfType(element, PsiComment.class, false);
+  }
+
+  private boolean isSuitableTypeComment(@NotNull PsiComment comment) {
+    final String annotation = PyTypingTypeProvider.getTypeCommentValue(comment.getText());
+    return annotation != null && findTypeCommentTarget(comment) != null;
+  }
+
+  @Nullable
+  private PyTargetExpression findTypeCommentTarget(@NotNull PsiComment comment) {
+    final PsiElement parent = comment.getParent();
+    if (parent instanceof PyAssignmentStatement) {
+      final PyAssignmentStatement assignment = (PyAssignmentStatement)parent;
+      final PyExpression[] rawTargets = assignment.getRawTargets();
+      if (rawTargets.length == 1 && rawTargets[0] instanceof PyTargetExpression) {
+        final PyTargetExpression target = (PyTargetExpression)rawTargets[0];
+        if (target.getTypeComment() == comment) {
+          return target;
+        }
+      }
+    }
+    return null;
+  }
+
+  public static void runWithDocumentReleasedAndCommitted(@NotNull Project project, @NotNull Document document, @NotNull Runnable runnable) {
+    final PsiDocumentManager manager = PsiDocumentManager.getInstance(project);
+    manager.doPostponedOperationsAndUnblockDocument(document);
+    try {
+      runnable.run();
+    }
+    finally {
+      manager.commitDocument(document);
+    }
+  }
+}
diff --git a/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/assignmentWithUnpacking.py b/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/assignmentWithUnpacking.py
new file mode 100644 (file)
index 0000000..45bdb4e
--- /dev/null
@@ -0,0 +1 @@
+x, y = (1, 2)  # ty<caret>pe: int, int
diff --git a/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/badLanguageLevel.py b/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/badLanguageLevel.py
new file mode 100644 (file)
index 0000000..bfcc3bf
--- /dev/null
@@ -0,0 +1 @@
+x = 42 # ty<caret>pe: int
\ No newline at end of file
diff --git a/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/chainedAssignment.py b/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/chainedAssignment.py
new file mode 100644 (file)
index 0000000..6f7b13b
--- /dev/null
@@ -0,0 +1 @@
+x = y = 42 # ty<caret>pe: int
\ No newline at end of file
diff --git a/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/multilineAssignment.py b/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/multilineAssignment.py
new file mode 100644 (file)
index 0000000..7495802
--- /dev/null
@@ -0,0 +1,5 @@
+from typing import List
+
+xs = [1,
+      2,
+      3]  # ty<caret>pe: List[int]
diff --git a/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/multilineAssignment_after.py b/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/multilineAssignment_after.py
new file mode 100644 (file)
index 0000000..166ef07
--- /dev/null
@@ -0,0 +1,5 @@
+from typing import List
+
+xs: List[int] = [1,
+      2,
+      3]
diff --git a/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/simpleAssignment.py b/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/simpleAssignment.py
new file mode 100644 (file)
index 0000000..bfcc3bf
--- /dev/null
@@ -0,0 +1 @@
+x = 42 # ty<caret>pe: int
\ No newline at end of file
diff --git a/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/simpleAssignment_after.py b/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/simpleAssignment_after.py
new file mode 100644 (file)
index 0000000..c19c1b9
--- /dev/null
@@ -0,0 +1 @@
+x: int = 42
\ No newline at end of file
diff --git a/python/testSrc/com/jetbrains/python/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest.java b/python/testSrc/com/jetbrains/python/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest.java
new file mode 100644 (file)
index 0000000..e912da6
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2000-2016 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jetbrains.python.intentions;
+
+import com.jetbrains.python.PyBundle;
+import com.jetbrains.python.psi.LanguageLevel;
+
+public class PyConvertTypeCommentToVariableAnnotationIntentionTest extends PyIntentionTestCase {
+  private void doPositiveTest() {
+    doTest(PyBundle.message("INTN.convert.type.comment.to.variable.annotation.text"), LanguageLevel.PYTHON36);
+  }
+
+  private void doNegativeTest() {
+    runWithLanguageLevel(LanguageLevel.PYTHON36, () -> {
+      doNegativeTest(PyBundle.message("INTN.convert.type.comment.to.variable.annotation.text"));
+    });
+  }
+
+  public void testSimpleAssignment() {
+    doPositiveTest();
+  }
+
+  public void testBadLanguageLevel() {
+    runWithLanguageLevel(LanguageLevel.PYTHON35, () -> {
+      doNegativeTest(PyBundle.message("INTN.convert.type.comment.to.variable.annotation.text"));
+    });
+  }
+
+  public void testChainedAssignment() {
+    doNegativeTest();
+  }
+
+  public void testAssignmentWithUnpacking() {
+    doNegativeTest();
+  }
+
+  public void testMultilineAssignment() {
+    doPositiveTest();
+  }
+}