From 98016a1ae6326f5ff3d3b5b083ee3cb7f0f564f4 Mon Sep 17 00:00:00 2001 From: Mikhail Golubev Date: Tue, 3 Oct 2023 22:37:17 +0300 Subject: [PATCH] PY-63367 PY-63366 Include instructions for annotations of new-styles generic functions in their own CFG This way they have access to type parameters of these functions, and PyReferenceImpl.getResultsFromProcessor can properly perform flow-sensitive resolve for them now that READ instructions for expressions inside annotations and WRITE instructions for type parameters are in the same CFG. It allowed to remove all the special logic for type parameters from PyReferenceImpl as well as from PyUnboundLocalVariableInspection. I had to special-case these annotations in PyResolveUtil.scopeCrawlUp, though, to "lift" their original scope back from what ScopeUtil.getScopeOwner returns, making names defined in a class scope, e.g., nested classes, visible to them. GitOrigin-RevId: b2e78211565300dbcd7c2e2b246a7a8e14bb0e8f --- .../jetbrains/python/PyCommonResolveTest.java | 15 +++++ .../controlflow/PyControlFlowBuilder.java | 33 +++++---- .../PyUnboundLocalVariableInspection.java | 3 - .../psi/impl/PyTypeAliasStatementImpl.java | 7 ++ .../psi/impl/references/PyReferenceImpl.java | 38 +---------- .../python/psi/resolve/PyResolveUtil.java | 67 +++++++++++++------ ...unctionAnnotationsAreIncludedInItsGraph.py | 2 + ...nctionAnnotationsAreIncludedInItsGraph.txt | 12 ++++ ...ionsAreNotIncludedInEnclosingScopeGraph.py | 2 + ...onsAreNotIncludedInEnclosingScopeGraph.txt | 5 ++ .../controlflow/TypeAliasStatement.txt | 5 +- ...TypeParameterListInFunctionDeclaration.txt | 8 ++- ...ListOfAnotherNewStyleGenericNestedClass.py | 7 ++ ...olvedInsideMethodOfNewStyleGenericClass.py | 7 ++ ...InsideAnnotationOfNewStyleGenericMethod.py | 7 ++ .../python/PyControlFlowBuilderTest.java | 10 +++ 16 files changed, 149 insertions(+), 79 deletions(-) create mode 100644 python/testData/codeInsight/controlflow/NewStyleGenericFunctionAnnotationsAreIncludedInItsGraph.py create mode 100644 python/testData/codeInsight/controlflow/NewStyleGenericFunctionAnnotationsAreIncludedInItsGraph.txt create mode 100644 python/testData/codeInsight/controlflow/NewStyleGenericFunctionAnnotationsAreNotIncludedInEnclosingScopeGraph.py create mode 100644 python/testData/codeInsight/controlflow/NewStyleGenericFunctionAnnotationsAreNotIncludedInEnclosingScopeGraph.txt create mode 100644 python/testData/resolve/NestedClassIsResolvedInSuperclassListOfAnotherNewStyleGenericNestedClass.py create mode 100644 python/testData/resolve/NestedClassNotResolvedInsideMethodOfNewStyleGenericClass.py create mode 100644 python/testData/resolve/NestedClassResolvedInsideAnnotationOfNewStyleGenericMethod.py diff --git a/python/python-common-tests/com/jetbrains/python/PyCommonResolveTest.java b/python/python-common-tests/com/jetbrains/python/PyCommonResolveTest.java index 8fe2d392bdd0..907acc487dba 100644 --- a/python/python-common-tests/com/jetbrains/python/PyCommonResolveTest.java +++ b/python/python-common-tests/com/jetbrains/python/PyCommonResolveTest.java @@ -1845,6 +1845,21 @@ public abstract class PyCommonResolveTest extends PyCommonResolveTestCase { }); } + // PY-61877 PY-63366 + public void testNestedClassNotResolvedInsideMethodOfNewStyleGenericClass() { + assertNotResolved(); + } + + // PY-61877 PY-63367 + public void testNestedClassResolvedInsideAnnotationOfNewStyleGenericMethod() { + assertResolvesTo(PyClass.class, "Nested"); + } + + // PY-61877 + public void testNestedClassIsResolvedInSuperclassListOfAnotherNewStyleGenericNestedClass() { + assertResolvesTo(PyClass.class, "Nested"); + } + // [TODO] daniil.kalinin enable when resolve for collisions in type parameter names and class attribute names is implemented // PY-61877 //public void testClassAttributeDeclarationWithSameAsTypeParameterNameNotResolvedToTypeParameter() { diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/controlflow/PyControlFlowBuilder.java b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/controlflow/PyControlFlowBuilder.java index 9758437ea7c7..3cb4ba37ae5b 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/controlflow/PyControlFlowBuilder.java +++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/controlflow/PyControlFlowBuilder.java @@ -91,9 +91,11 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor { visitParameterListExpressions(node.getParameterList()); visitDecorators(node.getDecoratorList()); - final PyAnnotation annotation = node.getAnnotation(); - if (annotation != null) { - annotation.acceptChildren(this); + if (node.getTypeParameterList() == null) { + final PyAnnotation annotation = node.getAnnotation(); + if (annotation != null) { + annotation.acceptChildren(this); + } } final ReadWriteInstruction instruction = ReadWriteInstruction.write(myBuilder, node, node.getName()); @@ -121,9 +123,11 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor { if (defaultValue != null) { defaultValue.accept(PyControlFlowBuilder.this); } - final PyAnnotation annotation = param.getAnnotation(); - if (annotation != null) { - annotation.acceptChildren(PyControlFlowBuilder.this); + if (parameterList.getParent() instanceof PyFunction function && function.getTypeParameterList() == null) { + final PyAnnotation annotation = param.getAnnotation(); + if (annotation != null) { + annotation.acceptChildren(PyControlFlowBuilder.this); + } } } }); @@ -302,6 +306,10 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor { @Override public void visitPyNamedParameter(final @NotNull PyNamedParameter node) { + PyAnnotation annotation = node.getAnnotation(); + if (annotation != null) { + annotation.accept(this); + } final ReadWriteInstruction instruction = ReadWriteInstruction.write(myBuilder, node, node.getName()); myBuilder.addNode(instruction); myBuilder.checkPending(instruction); @@ -309,8 +317,12 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor { @Override public void visitPyAnnotation(@NotNull PyAnnotation node) { - // Return type and parameter annotations for functions are evaluated in their enclosing scope and processed in visitPyFunction - if (!(node.getParent() instanceof PyFunction || node.getParent() instanceof PyParameter)) { + // Unless there is a type parameter list, return type and parameter annotations for functions are evaluated in their enclosing scope + // and processed in visitPyFunction. + // If there are type parameters, though, we need to put the corresponding instructions *inside* the function's scope to be able to + // access them from annotations. + PyFunction function = PsiTreeUtil.getParentOfType(node, PyFunction.class, true, PyStatement.class); + if (function == null || function.getTypeParameterList() != null) { super.visitPyAnnotation(node); } } @@ -1036,11 +1048,6 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor { public void visitPyTypeAliasStatement(@NotNull PyTypeAliasStatement node) { myBuilder.startNode(node); - PyExpression typeExpression = node.getTypeExpression(); - if (typeExpression != null) { - typeExpression.accept(this); - } - final ReadWriteInstruction instruction = ReadWriteInstruction.write(myBuilder, node, node.getName()); myBuilder.addNode(instruction); myBuilder.checkPending(instruction); diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyUnboundLocalVariableInspection.java b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyUnboundLocalVariableInspection.java index 737349e8b968..4f7ffe3a5933 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyUnboundLocalVariableInspection.java +++ b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyUnboundLocalVariableInspection.java @@ -126,9 +126,6 @@ public class PyUnboundLocalVariableInspection extends PyInspection { if (PyInspectionsUtil.hasAnyInterruptedControlFlowPaths(node)) { return; } - else if (resolved instanceof PyTypeParameter && currentScopeOwner instanceof PyFunction) { - return; - } if (owner instanceof PyFile) { if (isBuiltin) { return; diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyTypeAliasStatementImpl.java b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyTypeAliasStatementImpl.java index 6db2c412dbf5..738b39389867 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyTypeAliasStatementImpl.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyTypeAliasStatementImpl.java @@ -14,6 +14,7 @@ import com.jetbrains.python.psi.types.PyType; import com.jetbrains.python.psi.types.TypeEvalContext; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import com.jetbrains.python.codeInsight.controlflow.ControlFlowCache; public class PyTypeAliasStatementImpl extends PyBaseElementImpl implements PyTypeAliasStatement { public PyTypeAliasStatementImpl(ASTNode astNode) { @@ -109,4 +110,10 @@ public class PyTypeAliasStatementImpl extends PyBaseElementImpl resolvedElements = processor.getElements(); - - if (referenceOwner instanceof PyTypeParameterListOwner typeParameterListOwner) { - tryMatchTypeParametersInDeclaration(referencedName, typeParameterListOwner, realContext, resultList, typeEvalContext); - } - if (resolvedOwner != null && !resolvedElements.isEmpty() && !ControlFlowCache.getScope(resolvedOwner).isGlobal(referencedName)) { if (resolvedOwner == referenceOwner) { final List instructions = getLatestDefinitions(referencedName, resolvedOwner, realContext); @@ -274,9 +269,7 @@ public class PyReferenceImpl implements PsiReferenceEx, PsiPolyVariantReference resolveInParentScope = () -> ScopeUtil.getScopeOwner(resolvedOwner); } else { - if (resultList.isEmpty()) { - unreachableLocalDeclaration = true; - } + unreachableLocalDeclaration = true; } } else if (referenceOwner != null) { @@ -317,18 +310,7 @@ public class PyReferenceImpl implements PsiReferenceEx, PsiPolyVariantReference continue; } if (definer == null) { - if (resolved instanceof PyTypeParameter) { - if (resolvedOwner != referenceOwner || - PsiTreeUtil.getParentOfType(realContext, PyAnnotation.class, PyTypeCommentOwner.class) != null) { - resultList.poke(resolved, RatedResolveResult.RATE_HIGH); - } - else { - resultList.poke(resolved, RatedResolveResult.RATE_LOW); - } - } - else { - resultList.poke(resolved, getRate(resolved, typeEvalContext)); - } + resultList.poke(resolved, getRate(resolved, typeEvalContext)); } else { resultList.poke(definer, getRate(definer, typeEvalContext)); @@ -391,22 +373,6 @@ public class PyReferenceImpl implements PsiReferenceEx, PsiPolyVariantReference return results; } - private static void tryMatchTypeParametersInDeclaration(@NotNull String referencedName, - PyTypeParameterListOwner owner, - PsiElement realContext, - ResolveResultList resultList, - TypeEvalContext typeEvalContext) { - PyTypeParameterList typeParameterList = owner.getTypeParameterList(); - if (typeParameterList != null && PsiTreeUtil.getParentOfType(realContext, PyTypeParameterListOwner.class) != null) { - typeParameterList.getTypeParameters() - .stream().filter(typeParameter -> typeParameter.getName() != null && typeParameter.getName().equals(referencedName)) - .forEach(typeParameter -> { - resultList.add(new RatedResolveResult(getRate(typeParameter, typeEvalContext), typeParameter)); - } - ); - } - } - @NotNull private ResolveResultList resolveByReferenceResolveProviders() { final ResolveResultList results = new ResolveResultList(); diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/resolve/PyResolveUtil.java b/python/python-psi-impl/src/com/jetbrains/python/psi/resolve/PyResolveUtil.java index ec52cfac071f..65b2008cc4ec 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/resolve/PyResolveUtil.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/resolve/PyResolveUtil.java @@ -59,8 +59,8 @@ public final class PyResolveUtil { */ public static void scopeCrawlUp(@NotNull PsiScopeProcessor processor, @NotNull PsiElement element, @Nullable String name, @Nullable PsiElement roof) { - final ScopeOwner originalOwner = ScopeUtil.getScopeOwner(element); - final PsiElement parent = element.getParent(); + ScopeOwner originalOwner = ScopeUtil.getScopeOwner(element); + PsiElement parent = element.getParent(); ScopeOwner owner = originalOwner; if (parent instanceof PyNonlocalStatement) { /* wee need to search in one step out scope for nonlocal statements */ @@ -76,6 +76,24 @@ public final class PyResolveUtil { owner = (PyFile)globalScope; } } + if (owner instanceof PyFunction function && function.getTypeParameterList() != null) { + // Type parameters of generic functions and methods need to be visible in their parameter and return type annotations, + // so we resolve names in these annotations starting from the containing function's scope, "lowering" their scope in + // ScopeUtil.getScopeOwner(). + // At the same time, these annotations still need to see names from the class scope, such as nested classes defined there, + // as if these names were evaluated on a class-level scope. + PyAnnotation annotation = PsiTreeUtil.getParentOfType(element, PyAnnotation.class); + if (PsiTreeUtil.getParentOfType(annotation, PyFunction.class, true, PyStatement.class) == function) { + originalOwner = ScopeUtil.getScopeOwner(function); + } + } + if (owner instanceof PyClass pyClass && pyClass.getTypeParameterList() != null) { + // The same logic applies to the list of base classes of a generic class + PyArgumentList superclassList = PsiTreeUtil.getParentOfType(element, PyArgumentList.class); + if (superclassList != null && superclassList.getParent() == pyClass) { + originalOwner = ScopeUtil.getScopeOwner(pyClass); + } + } scopeCrawlUp(processor, owner, originalOwner, name, roof); } @@ -87,29 +105,30 @@ public final class PyResolveUtil { public static void scopeCrawlUp(@NotNull PsiScopeProcessor processor, @Nullable ScopeOwner scopeOwner, @Nullable ScopeOwner originalScopeOwner, @Nullable String name, @Nullable PsiElement roof) { while (scopeOwner != null) { - if (!(scopeOwner instanceof PyClass) || scopeOwner == originalScopeOwner || ((PyClass)scopeOwner).getTypeParameterList() != null) { - final Scope scope = ControlFlowCache.getScope(scopeOwner); - if (name != null) { - final boolean includeNestedGlobals = scopeOwner instanceof PyFile; - for (PsiNamedElement resolved : scope.getNamedElements(name, includeNestedGlobals)) { - if (!processor.execute(resolved, ResolveState.initial())) { - return; - } - } - } - else { - for (PsiNamedElement element : scope.getNamedElements()) { - if (!processor.execute(element, ResolveState.initial())) { - return; - } - } - } - for (PyImportedNameDefiner definer : scope.getImportedNameDefiners()) { - if (!processor.execute(definer, ResolveState.initial())) { + final Scope scope = ControlFlowCache.getScope(scopeOwner); + if (name != null) { + final boolean includeNestedGlobals = scopeOwner instanceof PyFile; + for (PsiNamedElement resolved : scope.getNamedElements(name, includeNestedGlobals)) { + if (isClassLevelDefinitionInvisibleToReference(resolved, scopeOwner, originalScopeOwner)) continue; + if (!processor.execute(resolved, ResolveState.initial())) { return; } } } + else { + for (PsiNamedElement element : scope.getNamedElements()) { + if (isClassLevelDefinitionInvisibleToReference(element, scopeOwner, originalScopeOwner)) continue; + if (!processor.execute(element, ResolveState.initial())) { + return; + } + } + } + for (PyImportedNameDefiner definer : scope.getImportedNameDefiners()) { + if (isClassLevelDefinitionInvisibleToReference(definer, scopeOwner, originalScopeOwner)) continue; + if (!processor.execute(definer, ResolveState.initial())) { + return; + } + } if (scopeOwner == roof) { return; } @@ -122,6 +141,12 @@ public final class PyResolveUtil { } } + private static boolean isClassLevelDefinitionInvisibleToReference(@NotNull PsiElement definition, + @NotNull ScopeOwner definitionScope, + @Nullable ScopeOwner referenceScope) { + return definitionScope instanceof PyClass pyClass && pyClass != referenceScope && !(definition instanceof PyTypeParameter); + } + /** * Resolves the passed expression in its containing file. * Does not go outside this file. diff --git a/python/testData/codeInsight/controlflow/NewStyleGenericFunctionAnnotationsAreIncludedInItsGraph.py b/python/testData/codeInsight/controlflow/NewStyleGenericFunctionAnnotationsAreIncludedInItsGraph.py new file mode 100644 index 000000000000..e547fd22021d --- /dev/null +++ b/python/testData/codeInsight/controlflow/NewStyleGenericFunctionAnnotationsAreIncludedInItsGraph.py @@ -0,0 +1,2 @@ +def f[T](x: list[T] = list()) -> T: + return x[0] \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/NewStyleGenericFunctionAnnotationsAreIncludedInItsGraph.txt b/python/testData/codeInsight/controlflow/NewStyleGenericFunctionAnnotationsAreIncludedInItsGraph.txt new file mode 100644 index 000000000000..3fc5784c56ac --- /dev/null +++ b/python/testData/codeInsight/controlflow/NewStyleGenericFunctionAnnotationsAreIncludedInItsGraph.txt @@ -0,0 +1,12 @@ +0(1) element: null +1(2) element: PyTypeParameter +2(3) WRITE ACCESS: T +3(4) element: PySubscriptionExpression +4(5) READ ACCESS: list +5(6) READ ACCESS: T +6(7) WRITE ACCESS: x +7(8) READ ACCESS: T +8(9) element: PyReturnStatement +9(10) element: PySubscriptionExpression +10(11) READ ACCESS: x +11() element: null \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/NewStyleGenericFunctionAnnotationsAreNotIncludedInEnclosingScopeGraph.py b/python/testData/codeInsight/controlflow/NewStyleGenericFunctionAnnotationsAreNotIncludedInEnclosingScopeGraph.py new file mode 100644 index 000000000000..e547fd22021d --- /dev/null +++ b/python/testData/codeInsight/controlflow/NewStyleGenericFunctionAnnotationsAreNotIncludedInEnclosingScopeGraph.py @@ -0,0 +1,2 @@ +def f[T](x: list[T] = list()) -> T: + return x[0] \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/NewStyleGenericFunctionAnnotationsAreNotIncludedInEnclosingScopeGraph.txt b/python/testData/codeInsight/controlflow/NewStyleGenericFunctionAnnotationsAreNotIncludedInEnclosingScopeGraph.txt new file mode 100644 index 000000000000..519765ff4346 --- /dev/null +++ b/python/testData/codeInsight/controlflow/NewStyleGenericFunctionAnnotationsAreNotIncludedInEnclosingScopeGraph.txt @@ -0,0 +1,5 @@ +0(1) element: null +1(2) element: PyFunction('f') +2(3) READ ACCESS: list +3(4) WRITE ACCESS: f +4() element: null \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/TypeAliasStatement.txt b/python/testData/codeInsight/controlflow/TypeAliasStatement.txt index 4a8dcdf18167..eb0f4340316a 100644 --- a/python/testData/codeInsight/controlflow/TypeAliasStatement.txt +++ b/python/testData/codeInsight/controlflow/TypeAliasStatement.txt @@ -1,5 +1,4 @@ 0(1) element: null 1(2) element: PyTypeAliasStatement -2(3) READ ACCESS: str -3(4) WRITE ACCESS: myType -4() element: null \ No newline at end of file +2(3) WRITE ACCESS: myType +3() element: null \ No newline at end of file diff --git a/python/testData/codeInsight/controlflow/TypeParameterListInFunctionDeclaration.txt b/python/testData/codeInsight/controlflow/TypeParameterListInFunctionDeclaration.txt index ad6ce0d09355..4437ca6dbbb8 100644 --- a/python/testData/codeInsight/controlflow/TypeParameterListInFunctionDeclaration.txt +++ b/python/testData/codeInsight/controlflow/TypeParameterListInFunctionDeclaration.txt @@ -3,6 +3,8 @@ 2(3) WRITE ACCESS: T 3(4) element: PyTypeParameter 4(5) WRITE ACCESS: U -5(6) WRITE ACCESS: a -6(7) WRITE ACCESS: b -7() element: null \ No newline at end of file +5(6) READ ACCESS: T +6(7) WRITE ACCESS: a +7(8) READ ACCESS: U +8(9) WRITE ACCESS: b +9() element: null \ No newline at end of file diff --git a/python/testData/resolve/NestedClassIsResolvedInSuperclassListOfAnotherNewStyleGenericNestedClass.py b/python/testData/resolve/NestedClassIsResolvedInSuperclassListOfAnotherNewStyleGenericNestedClass.py new file mode 100644 index 000000000000..68acbe5dffb0 --- /dev/null +++ b/python/testData/resolve/NestedClassIsResolvedInSuperclassListOfAnotherNewStyleGenericNestedClass.py @@ -0,0 +1,7 @@ +class C: + class Nested: + pass + + class Another[T](Nested): + # + pass \ No newline at end of file diff --git a/python/testData/resolve/NestedClassNotResolvedInsideMethodOfNewStyleGenericClass.py b/python/testData/resolve/NestedClassNotResolvedInsideMethodOfNewStyleGenericClass.py new file mode 100644 index 000000000000..100cbdeeb095 --- /dev/null +++ b/python/testData/resolve/NestedClassNotResolvedInsideMethodOfNewStyleGenericClass.py @@ -0,0 +1,7 @@ +class C[T]: + class Nested: + pass + + def method(): + Nested +# \ No newline at end of file diff --git a/python/testData/resolve/NestedClassResolvedInsideAnnotationOfNewStyleGenericMethod.py b/python/testData/resolve/NestedClassResolvedInsideAnnotationOfNewStyleGenericMethod.py new file mode 100644 index 000000000000..e815ad565bd6 --- /dev/null +++ b/python/testData/resolve/NestedClassResolvedInsideAnnotationOfNewStyleGenericMethod.py @@ -0,0 +1,7 @@ +class C: + class Nested: + pass + + def method[T](self, x: Nested): + # + pass \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/PyControlFlowBuilderTest.java b/python/testSrc/com/jetbrains/python/PyControlFlowBuilderTest.java index a2d93603e1f3..25b858243d2a 100644 --- a/python/testSrc/com/jetbrains/python/PyControlFlowBuilderTest.java +++ b/python/testSrc/com/jetbrains/python/PyControlFlowBuilderTest.java @@ -549,6 +549,16 @@ public class PyControlFlowBuilderTest extends LightMarkedTestCase { doTest(); } + // PY-61877 + public void testNewStyleGenericFunctionAnnotationsAreIncludedInItsGraph() { + doTestFirstStatement(); + } + + // PY-61877 + public void testNewStyleGenericFunctionAnnotationsAreNotIncludedInEnclosingScopeGraph() { + doTest(); + } + private void doTestFirstStatement() { final String testName = getTestName(false); configureByFile(testName + ".py");