diff --git a/python/resources/intentionDescriptions/PyConvertTypeCommentToVariableAnnotation/after.py.template b/python/resources/intentionDescriptions/PyConvertTypeCommentToVariableAnnotation/after.py.template new file mode 100644 index 000000000000..89023c6848ad --- /dev/null +++ b/python/resources/intentionDescriptions/PyConvertTypeCommentToVariableAnnotation/after.py.template @@ -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 index 000000000000..e28b15f6074b --- /dev/null +++ b/python/resources/intentionDescriptions/PyConvertTypeCommentToVariableAnnotation/before.py.template @@ -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 index 000000000000..2d034cc57781 --- /dev/null +++ b/python/resources/intentionDescriptions/PyConvertTypeCommentToVariableAnnotation/description.html @@ -0,0 +1,7 @@ + + + + This intention convert PEP-484 type comments to Python 3.6 variable annotations + + + \ No newline at end of file diff --git a/python/src/META-INF/python-core-common.xml b/python/src/META-INF/python-core-common.xml index 57df91bfea5e..a2b5c41eb4f0 100644 --- a/python/src/META-INF/python-core-common.xml +++ b/python/src/META-INF/python-core-common.xml @@ -311,6 +311,11 @@ Python + + com.jetbrains.python.codeInsight.intentions.PyConvertTypeCommentToVariableAnnotation + Python + + diff --git a/python/src/com/jetbrains/python/PyBundle.properties b/python/src/com/jetbrains/python/PyBundle.properties index e092447a124b..db09df1ac899 100644 --- a/python/src/com/jetbrains/python/PyBundle.properties +++ b/python/src/com/jetbrains/python/PyBundle.properties @@ -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 index 000000000000..8fb2e15d8127 --- /dev/null +++ b/python/src/com/jetbrains/python/codeInsight/intentions/PyConvertTypeCommentToVariableAnnotation.java @@ -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 index 000000000000..45bdb4e44262 --- /dev/null +++ b/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/assignmentWithUnpacking.py @@ -0,0 +1 @@ +x, y = (1, 2) # type: int, int diff --git a/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/badLanguageLevel.py b/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/badLanguageLevel.py new file mode 100644 index 000000000000..bfcc3bf3ce4a --- /dev/null +++ b/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/badLanguageLevel.py @@ -0,0 +1 @@ +x = 42 # type: 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 index 000000000000..6f7b13bee865 --- /dev/null +++ b/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/chainedAssignment.py @@ -0,0 +1 @@ +x = y = 42 # type: 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 index 000000000000..7495802f2d54 --- /dev/null +++ b/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/multilineAssignment.py @@ -0,0 +1,5 @@ +from typing import List + +xs = [1, + 2, + 3] # type: List[int] diff --git a/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/multilineAssignment_after.py b/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/multilineAssignment_after.py new file mode 100644 index 000000000000..166ef07c5417 --- /dev/null +++ b/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/multilineAssignment_after.py @@ -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 index 000000000000..bfcc3bf3ce4a --- /dev/null +++ b/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/simpleAssignment.py @@ -0,0 +1 @@ +x = 42 # type: 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 index 000000000000..c19c1b98f2a6 --- /dev/null +++ b/python/testData/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest/simpleAssignment_after.py @@ -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 index 000000000000..e912da672ef0 --- /dev/null +++ b/python/testSrc/com/jetbrains/python/intentions/PyConvertTypeCommentToVariableAnnotationIntentionTest.java @@ -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(); + } +}