PY-49283 Add quick fix for from future import annotations

(cherry picked from commit 7d26e211c67f2574078e1f0e8951f744889d82d6)

IJ-CR-10316

GitOrigin-RevId: 34d4bc72b11613c0603587c1c0a9e640b0add023
This commit is contained in:
andrey.matveev
2021-06-16 15:42:03 +07:00
committed by intellij-monorepo-bot
parent c42fb45d25
commit 1fe7e5443c
12 changed files with 128 additions and 53 deletions

View File

@@ -1087,6 +1087,7 @@ QFIX.remove.type.comment=Remove the type comment
QFIX.remove.annotation=Remove the annotation
QFIX.replace.with.type.name=Replace with the type name
QFIX.replace.with.old.union.style=Replace with an old-style Union
QFIX.add.from.future.import.annotations=Add 'from __future__ import annotations'
# PyInspectionsSuppressor
INSP.python.suppressor.suppress.for.function=Suppress for a function

View File

@@ -136,19 +136,14 @@ public class PyCompatibilityInspection extends PyInspection {
protected void registerProblem(@NotNull PsiElement element,
@NotNull TextRange range,
@NotNull @InspectionMessage String message,
@Nullable LocalQuickFix quickFix,
boolean asError) {
boolean asError,
LocalQuickFix @NotNull ... fixes) {
if (element.getTextLength() == 0) {
return;
}
range = range.shiftRight(-element.getTextRange().getStartOffset());
if (quickFix != null) {
myHolder.registerProblem(element, range, message, quickFix);
}
else {
myHolder.registerProblem(element, range, message);
}
myHolder.registerProblem(element, range, message, fixes);
}
@Override

View File

@@ -244,8 +244,8 @@ public abstract class CompatibilityVisitor extends PyAnnotator {
PyPsiBundle.message("INSP.compatibility.feature.support.string.literal.prefix", prefix),
node,
TextRange.create(elementStart, elementStart + element.getPrefixLength()),
new RemovePrefixQuickFix(prefix),
true);
true,
new RemovePrefixQuickFix(prefix));
}
if (seenBytes && seenNonBytes) {
@@ -452,7 +452,6 @@ public abstract class CompatibilityVisitor extends PyAnnotator {
PyPsiBundle.message("INSP.compatibility.feature.support.this.syntax"),
node,
asyncNode.getTextRange(),
null,
true);
}
}
@@ -489,17 +488,13 @@ public abstract class CompatibilityVisitor extends PyAnnotator {
protected abstract void registerProblem(@NotNull PsiElement node,
@NotNull TextRange range,
@NotNull @InspectionMessage String message,
@Nullable LocalQuickFix localQuickFix,
boolean asError);
boolean asError,
LocalQuickFix @NotNull ... fixes);
protected void registerProblem(@NotNull PsiElement node,
@NotNull @InspectionMessage String message,
@Nullable LocalQuickFix localQuickFix) {
registerProblem(node, node.getTextRange(), message, localQuickFix, true);
}
protected void registerProblem(@NotNull PsiElement node, @NotNull @InspectionMessage String message) {
registerProblem(node, message, null);
LocalQuickFix @NotNull ... fixes) {
registerProblem(node, node.getTextRange(), message, true, fixes);
}
protected void setVersionsToProcess(@NotNull List<LanguageLevel> versionsToProcess) {
@@ -509,8 +504,8 @@ public abstract class CompatibilityVisitor extends PyAnnotator {
protected void registerForAllMatchingVersions(@NotNull Predicate<LanguageLevel> levelPredicate,
@NotNull @Nls String suffix,
@NotNull Iterable<Pair<? extends PsiElement, TextRange>> nodesWithRanges,
@Nullable LocalQuickFix localQuickFix,
boolean asError) {
boolean asError,
LocalQuickFix... fixes) {
final List<String> levels = myVersionsToProcess
.stream()
.filter(levelPredicate)
@@ -522,7 +517,7 @@ public abstract class CompatibilityVisitor extends PyAnnotator {
@InspectionMessage String message = PyPsiBundle.message("INSP.compatibility.inspection.unsupported.feature.prefix",
levels.size(), versions, suffix);
for (Pair<? extends PsiElement, TextRange> nodeWithRange : nodesWithRanges) {
registerProblem(nodeWithRange.first, nodeWithRange.second, message, localQuickFix, asError);
registerProblem(nodeWithRange.first, nodeWithRange.second, message, asError, fixes);
}
}
}
@@ -530,41 +525,27 @@ public abstract class CompatibilityVisitor extends PyAnnotator {
protected void registerForAllMatchingVersions(@NotNull Predicate<LanguageLevel> levelPredicate,
@NotNull @Nls String suffix,
@NotNull Iterable<? extends PsiElement> nodes,
@Nullable LocalQuickFix localQuickFix) {
LocalQuickFix... fixes) {
final List<Pair<? extends PsiElement, TextRange>> nodesWithRanges =
ContainerUtil.map(nodes, node -> Pair.createNonNull(node, node.getTextRange()));
registerForAllMatchingVersions(levelPredicate, suffix, nodesWithRanges, localQuickFix, true);
registerForAllMatchingVersions(levelPredicate, suffix, nodesWithRanges, true, fixes);
}
protected void registerForAllMatchingVersions(@NotNull Predicate<LanguageLevel> levelPredicate,
@NotNull @Nls String suffix,
@NotNull PsiElement node,
@NotNull TextRange range,
@Nullable LocalQuickFix localQuickFix,
boolean asError) {
boolean asError,
LocalQuickFix... fixes) {
final List<Pair<? extends PsiElement, TextRange>> nodesWithRanges = Collections.singletonList(Pair.createNonNull(node, range));
registerForAllMatchingVersions(levelPredicate, suffix, nodesWithRanges, localQuickFix, asError);
registerForAllMatchingVersions(levelPredicate, suffix, nodesWithRanges, asError, fixes);
}
protected void registerForAllMatchingVersions(@NotNull Predicate<LanguageLevel> levelPredicate,
@NotNull @Nls String suffix,
@NotNull PsiElement node,
@Nullable LocalQuickFix localQuickFix,
boolean asError) {
registerForAllMatchingVersions(levelPredicate, suffix, node, node.getTextRange(), localQuickFix, asError);
}
protected void registerForAllMatchingVersions(@NotNull Predicate<LanguageLevel> levelPredicate,
@NotNull @Nls String suffix,
@NotNull PsiElement node,
@Nullable LocalQuickFix localQuickFix) {
registerForAllMatchingVersions(levelPredicate, suffix, node, localQuickFix, true);
}
protected void registerForAllMatchingVersions(@NotNull Predicate<LanguageLevel> levelPredicate,
@NotNull @Nls String suffix,
@NotNull PsiElement node) {
registerForAllMatchingVersions(levelPredicate, suffix, node, null, true);
LocalQuickFix... fixes) {
registerForAllMatchingVersions(levelPredicate, suffix, node, node.getTextRange(), true, fixes);
}
@Override
@@ -737,10 +718,11 @@ public abstract class CompatibilityVisitor extends PyAnnotator {
if (node.getOperator() != PyTokenTypes.OR) return;
final PsiFile file = node.getContainingFile();
final boolean isInAnnotation = PsiTreeUtil.getParentOfType(node, PyAnnotation.class, false, ScopeOwner.class) != null;
if (file == null ||
file instanceof PyFile &&
((PyFile)file).hasImportFromFuture(FutureFeature.ANNOTATIONS) &&
PsiTreeUtil.getParentOfType(node, PyAnnotation.class, false, ScopeOwner.class) != null) {
isInAnnotation) {
return;
}
@@ -761,10 +743,16 @@ public abstract class CompatibilityVisitor extends PyAnnotator {
final Ref<PyType> refType = PyTypingTypeProvider.getType(node, context);
if (refType != null && refType.get() instanceof PyUnionType) {
registerForAllMatchingVersions(level -> level.isOlderThan(LanguageLevel.PYTHON310),
PyPsiBundle.message("INSP.compatibility.new.union.syntax.not.available.in.earlier.version"),
node,
new ReplaceWithOldStyleUnionQuickFix());
if (isInAnnotation) {
registerForAllMatchingVersions(level -> level.isOlderThan(LanguageLevel.PYTHON310),
PyPsiBundle.message("INSP.compatibility.new.union.syntax.not.available.in.earlier.version"),
node, new ReplaceWithOldStyleUnionQuickFix(), new AddFromFutureImportAnnotationsQuickFix());
}
else {
registerForAllMatchingVersions(level -> level.isOlderThan(LanguageLevel.PYTHON310),
PyPsiBundle.message("INSP.compatibility.new.union.syntax.not.available.in.earlier.version"),
node, new ReplaceWithOldStyleUnionQuickFix());
}
}
}
@@ -829,4 +817,17 @@ public abstract class CompatibilityVisitor extends PyAnnotator {
}
}
}
private static class AddFromFutureImportAnnotationsQuickFix implements LocalQuickFix {
@Override
public @NotNull String getFamilyName() {
return PyPsiBundle.message("QFIX.add.from.future.import.annotations");
}
@Override
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
final PsiFile file = descriptor.getPsiElement().getContainingFile();
AddImportHelper.addOrUpdateFromImportStatement(file, "__future__", "annotations", null, AddImportHelper.ImportPriority.FUTURE, null);
}
}
}

View File

@@ -23,6 +23,7 @@ import com.intellij.codeInspection.ProblemHighlightType;
import com.intellij.codeInspection.ex.ProblemDescriptorImpl;
import com.intellij.codeInspection.ex.QuickFixWrapper;
import com.intellij.codeInspection.util.InspectionMessage;
import com.intellij.lang.annotation.AnnotationBuilder;
import com.intellij.lang.annotation.AnnotationHolder;
import com.intellij.lang.annotation.HighlightSeverity;
import com.intellij.openapi.util.TextRange;
@@ -52,16 +53,22 @@ public class UnsupportedFeatures extends CompatibilityVisitor {
@Override
protected void registerProblem(@NotNull PsiElement node,
@NotNull TextRange range,
@NotNull @InspectionMessage String message,
@Nullable LocalQuickFix localQuickFix,
boolean asError) {
@NotNull String message,
boolean asError,
LocalQuickFix @NotNull ... fixes) {
if (range.isEmpty()) {
return;
}
HighlightSeverity severity = asError ? HighlightSeverity.ERROR : HighlightSeverity.WARNING;
if (localQuickFix != null) {
getHolder().newAnnotation(severity, message).range(range).withFix(createIntention(node, message, localQuickFix)).create();
if (fixes.length > 0) {
AnnotationBuilder annotationBuilder = getHolder().newAnnotation(severity, message).range(range);
for (LocalQuickFix fix: fixes) {
if (fix != null) {
annotationBuilder = annotationBuilder.withFix(createIntention(node, message, fix));
}
}
annotationBuilder.create();
}
else {
getHolder().newAnnotation(severity, message).range(range).create();

View File

@@ -0,0 +1,4 @@
class A:
pass
assert isinstance(A(), <warning descr="Python versions 2.7, 3.5, 3.6, 3.7, 3.8, 3.9 do not allow writing union types as X | Y"><caret>int | str</warning>)

View File

@@ -0,0 +1,4 @@
class A:
pass
assert issubclass(A, <warning descr="Python versions 2.7, 3.5, 3.6, 3.7, 3.8, 3.9 do not allow writing union types as X | Y"><caret>int | str</warning>)

View File

@@ -0,0 +1,5 @@
from typing import Union
def foo() -> <warning descr="Python versions 2.7, 3.5, 3.6, 3.7, 3.8, 3.9 do not allow writing union types as X | Y"><caret>int | Union[str, bool]</warning>:
return 42

View File

@@ -0,0 +1,7 @@
from __future__ import annotations
from typing import Union
def foo() -> int | Union[str, bool]:
return 42

View File

@@ -0,0 +1,4 @@
from typing import Union
a<warning descr="Python versions 2.7, 3.5 do not support variable annotations">: <warning descr="Python versions 2.7, 3.5, 3.6, 3.7, 3.8, 3.9 do not allow writing union types as X | Y"><caret>int | Union[str, bool]</warning></warning> = 42

View File

@@ -0,0 +1,6 @@
from __future__ import annotations
from typing import Union
a: <caret>int | Union[str, bool] = 42

View File

@@ -66,4 +66,12 @@ public abstract class PyQuickFixTestCase extends PyTestCase {
myFixture.launchAction(intentionAction);
myFixture.checkResultByFile(testFileName + ".py", testFileName + "_after.py", true);
}
protected void doNegativeTest(@NotNull Class inspectionClass, @NotNull String hint) {
final var testFileName = getTestName(true);
myFixture.enableInspections(inspectionClass);
myFixture.configureByFile(testFileName + ".py");
myFixture.checkHighlighting(true, false, false);
assertEmpty(myFixture.filterAvailableIntentions(hint));
}
}

View File

@@ -0,0 +1,33 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.jetbrains.python.quickFixes
import com.jetbrains.python.PyPsiBundle
import com.jetbrains.python.PyQuickFixTestCase
import com.jetbrains.python.inspections.PyCompatibilityInspection
class PyAddFromFutureImportAnnotationsQuickFixTest: PyQuickFixTestCase() {
// PY-44974
fun testNotSuggestedOnBitwiseOrUnionInIsInstance() {
doNegativeTest(PyPsiBundle.message("QFIX.add.from.future.import.annotations"))
}
// PY-44974
fun testNotSuggestedOnBitwiseOrUnionInIsSubclass() {
doNegativeTest(PyPsiBundle.message("QFIX.add.from.future.import.annotations"))
}
// PY-44974
fun testSuggestedOnBitwiseOrUnionInReturnAnnotation() {
doQuickFixTest(PyCompatibilityInspection::class.java,
PyPsiBundle.message("QFIX.add.from.future.import.annotations"))
}
// PY-44974
fun testSuggestedOnBitwiseOrUnionInVariableAnnotation() {
doQuickFixTest(PyCompatibilityInspection::class.java, PyPsiBundle.message("QFIX.add.from.future.import.annotations"))
}
private fun doNegativeTest(hint: String) {
super.doNegativeTest(PyCompatibilityInspection::class.java, hint)
}
}