From af24ba5f2081e62943aa1cef5aaed2f5219af6ac Mon Sep 17 00:00:00 2001 From: Mikhail Golubev Date: Mon, 22 Jan 2018 17:47:52 +0300 Subject: [PATCH] PY-16412 Detect presence of type annotations for variables purely syntactically Namely, if there is any PEP 526 variable type annotation or a comment that starts with "# type:", don't suggest the intention regardless of whether the type hint itself is well-formed, properly resolved, etc. --- .../PyAnnotateVariableTypeIntention.java | 75 ++++++++++++++++++- ...rgetWithUnresolvedAheadOfTimeAnnotation.py | 4 + ...ssignmentTargetWithUnresolvedAnnotation.py | 3 + ...signmentTargetWithUnresolvedTypeComment.py | 3 + .../PyAnnotateVariableTypeIntentionTest.java | 12 +++ 5 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 python/testData/intentions/PyAnnotateVariableTypeIntentionTest/notSuggestedForLocalAssignmentTargetWithUnresolvedAheadOfTimeAnnotation.py create mode 100644 python/testData/intentions/PyAnnotateVariableTypeIntentionTest/notSuggestedForLocalAssignmentTargetWithUnresolvedAnnotation.py create mode 100644 python/testData/intentions/PyAnnotateVariableTypeIntentionTest/notSuggestedForLocalAssignmentTargetWithUnresolvedTypeComment.py diff --git a/python/src/com/jetbrains/python/codeInsight/intentions/PyAnnotateVariableTypeIntention.java b/python/src/com/jetbrains/python/codeInsight/intentions/PyAnnotateVariableTypeIntention.java index 5674a6cf5732..9c687ee970c1 100644 --- a/python/src/com/jetbrains/python/codeInsight/intentions/PyAnnotateVariableTypeIntention.java +++ b/python/src/com/jetbrains/python/codeInsight/intentions/PyAnnotateVariableTypeIntention.java @@ -11,11 +11,16 @@ 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.typing.PyTypingTypeProvider; +import com.jetbrains.python.codeInsight.controlflow.ControlFlowCache; +import com.jetbrains.python.codeInsight.controlflow.ScopeOwner; +import com.jetbrains.python.codeInsight.dataflow.scope.Scope; +import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil; import com.jetbrains.python.documentation.PythonDocumentationProvider; import com.jetbrains.python.documentation.doctest.PyDocstringFile; import com.jetbrains.python.psi.*; import com.jetbrains.python.psi.resolve.PyResolveContext; +import com.jetbrains.python.psi.resolve.RatedResolveResult; +import com.jetbrains.python.psi.types.PyClassTypeImpl; import com.jetbrains.python.psi.types.PyType; import com.jetbrains.python.psi.types.TypeEvalContext; import one.util.streamex.StreamEx; @@ -82,9 +87,73 @@ public class PyAnnotateVariableTypeIntention extends PyBaseIntentionAction { return PsiTreeUtil.getParentOfType(target, PyWithItem.class, PyAssignmentStatement.class, PyForPart.class) != null; } + // TODO unify this logic with PyTypingTypeProvider somehow private static boolean isAnnotated(@NotNull PyTargetExpression target, @NotNull TypeEvalContext context) { - // TODO filter out fields explicitly annotated as Any - return new PyTypingTypeProvider().getReferenceType(target, context, null) != null; + final ScopeOwner scopeOwner = ScopeUtil.getScopeOwner(target); + final String name = target.getName(); + if (scopeOwner == null || name == null) { + return false; + } + + if (!target.isQualified()) { + if (hasInlineAnnotation(target)) { + return true; + } + + StreamEx candidates = null; + if (context.maySwitchToAST(target)) { + final Scope scope = ControlFlowCache.getScope(scopeOwner); + candidates = StreamEx.of(scope.getNamedElements(name, false)).select(PyTargetExpression.class); + } + // Unqualified target expression in either class or module + else if (scopeOwner instanceof PyFile) { + candidates = StreamEx.of(((PyFile)scopeOwner).getTopLevelAttributes()).filter(t -> name.equals(t.getName())); + } + else if (scopeOwner instanceof PyClass) { + candidates = StreamEx.of(((PyClass)scopeOwner).getClassAttributes()).filter(t -> name.equals(t.getName())); + } + if (candidates != null) { + return candidates.anyMatch(PyAnnotateVariableTypeIntention::hasInlineAnnotation); + } + } + else { + final PyClass pyClass = target.getContainingClass(); + if (pyClass != null && scopeOwner instanceof PyFunction) { + final PyResolveContext resolveContext = PyResolveContext.noImplicits().withTypeEvalContext(context); + + final boolean isInstanceAttribute; + if (context.maySwitchToAST(target)) { + //noinspection ConstantConditions + isInstanceAttribute = StreamEx.of(PyUtil.multiResolveTopPriority(target.getQualifier(), resolveContext)) + .select(PyParameter.class) + .filter(PyParameter::isSelf) + .anyMatch(p -> PsiTreeUtil.getParentOfType(p, PyFunction.class) == scopeOwner); + } + else { + isInstanceAttribute = PyUtil.isInstanceAttribute(target); + } + if (!isInstanceAttribute) { + return false; + } + // Set isDefinition=true to start searching right from the class level. + final PyClassTypeImpl classType = new PyClassTypeImpl(pyClass, true); + final List classAttrs = + classType.resolveMember(name, target, AccessDirection.READ, resolveContext, true); + if (classAttrs == null) { + return false; + } + return StreamEx.of(classAttrs) + .map(RatedResolveResult::getElement) + .select(PyTargetExpression.class) + .filter(x -> ScopeUtil.getScopeOwner(x) instanceof PyClass) + .anyMatch(PyAnnotateVariableTypeIntention::hasInlineAnnotation); + } + } + return false; + } + + private static boolean hasInlineAnnotation(@NotNull PyTargetExpression target) { + return target.getAnnotationValue() != null || target.getTypeCommentAnnotation() != null; } @Override diff --git a/python/testData/intentions/PyAnnotateVariableTypeIntentionTest/notSuggestedForLocalAssignmentTargetWithUnresolvedAheadOfTimeAnnotation.py b/python/testData/intentions/PyAnnotateVariableTypeIntentionTest/notSuggestedForLocalAssignmentTargetWithUnresolvedAheadOfTimeAnnotation.py new file mode 100644 index 000000000000..823f973896b5 --- /dev/null +++ b/python/testData/intentions/PyAnnotateVariableTypeIntentionTest/notSuggestedForLocalAssignmentTargetWithUnresolvedAheadOfTimeAnnotation.py @@ -0,0 +1,4 @@ +def func(x): + var: Any + var = x + var \ No newline at end of file diff --git a/python/testData/intentions/PyAnnotateVariableTypeIntentionTest/notSuggestedForLocalAssignmentTargetWithUnresolvedAnnotation.py b/python/testData/intentions/PyAnnotateVariableTypeIntentionTest/notSuggestedForLocalAssignmentTargetWithUnresolvedAnnotation.py new file mode 100644 index 000000000000..4642d7be8ee0 --- /dev/null +++ b/python/testData/intentions/PyAnnotateVariableTypeIntentionTest/notSuggestedForLocalAssignmentTargetWithUnresolvedAnnotation.py @@ -0,0 +1,3 @@ +def func(x): + var: Any = x + var \ No newline at end of file diff --git a/python/testData/intentions/PyAnnotateVariableTypeIntentionTest/notSuggestedForLocalAssignmentTargetWithUnresolvedTypeComment.py b/python/testData/intentions/PyAnnotateVariableTypeIntentionTest/notSuggestedForLocalAssignmentTargetWithUnresolvedTypeComment.py new file mode 100644 index 000000000000..a76b078f3513 --- /dev/null +++ b/python/testData/intentions/PyAnnotateVariableTypeIntentionTest/notSuggestedForLocalAssignmentTargetWithUnresolvedTypeComment.py @@ -0,0 +1,3 @@ +def func(x): + var = x # type: Any + var \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/intentions/PyAnnotateVariableTypeIntentionTest.java b/python/testSrc/com/jetbrains/python/intentions/PyAnnotateVariableTypeIntentionTest.java index 551d795b41ba..b0b2fbf2b328 100644 --- a/python/testSrc/com/jetbrains/python/intentions/PyAnnotateVariableTypeIntentionTest.java +++ b/python/testSrc/com/jetbrains/python/intentions/PyAnnotateVariableTypeIntentionTest.java @@ -38,6 +38,18 @@ public class PyAnnotateVariableTypeIntentionTest extends PyIntentionTestCase { doNegativeTest(); } + public void testNotSuggestedForLocalAssignmentTargetWithUnresolvedAnnotation() { + doNegativeTest(); + } + + public void testNotSuggestedForLocalAssignmentTargetWithUnresolvedTypeComment() { + doNegativeTest(); + } + + public void testNotSuggestedForLocalAssignmentTargetWithUnresolvedAheadOfTimeAnnotation() { + doNegativeTest(); + } + public void testAnnotationLocalSimpleAssignmentTarget() { doTestAnnotation(); }