Skip to content

Commit 27bf8c1

Browse files
committed
Changed behavior of symbol resolution involving a quoted (forward-declared) type annotation that references a symbol in the global (module) or builtins namespaces. The previous implementation didn't match the runtime behavior of typing.get_type_hints.
1 parent b4b7843 commit 27bf8c1

File tree

7 files changed

+76
-30
lines changed

7 files changed

+76
-30
lines changed

packages/pyright-internal/src/analyzer/typeEvaluator.ts

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3371,7 +3371,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
33713371

33723372
// Look for the scope that contains the value definition and
33733373
// see if it has a declared type.
3374-
const symbolWithScope = lookUpSymbolRecursive(node, name, !allowForwardReferences);
3374+
const symbolWithScope = lookUpSymbolRecursive(node, name, !allowForwardReferences, allowForwardReferences);
33753375

33763376
if (symbolWithScope) {
33773377
let useCodeFlowAnalysis = !allowForwardReferences;
@@ -15825,7 +15825,12 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
1582515825
return nameType;
1582615826
}
1582715827

15828-
function lookUpSymbolRecursive(node: ParseNode, name: string, honorCodeFlow: boolean): SymbolWithScope | undefined {
15828+
function lookUpSymbolRecursive(
15829+
node: ParseNode,
15830+
name: string,
15831+
honorCodeFlow: boolean,
15832+
preferGlobalScope = false
15833+
): SymbolWithScope | undefined {
1582915834
const scope = ScopeUtils.getScopeForNode(node);
1583015835
let symbolWithScope = scope?.lookUpSymbolRecursive(name);
1583115836

@@ -15870,6 +15875,34 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
1587015875
}
1587115876
}
1587215877

15878+
// PEP 563 indicates that if a forward reference can be resolved in the module
15879+
// scope (or, by implication, in the builtins scope), it should prefer that
15880+
// resolution over local resolutions.
15881+
if (symbolWithScope && preferGlobalScope) {
15882+
let curSymbolWithScope: SymbolWithScope | undefined = symbolWithScope;
15883+
while (
15884+
curSymbolWithScope.scope.type !== ScopeType.Module &&
15885+
curSymbolWithScope.scope.type !== ScopeType.Builtin &&
15886+
curSymbolWithScope.scope.parent
15887+
) {
15888+
curSymbolWithScope = curSymbolWithScope.scope.parent.lookUpSymbolRecursive(
15889+
name,
15890+
curSymbolWithScope.isOutsideCallerModule,
15891+
curSymbolWithScope.isBeyondExecutionScope || curSymbolWithScope.scope.isIndependentlyExecutable()
15892+
);
15893+
if (!curSymbolWithScope) {
15894+
break;
15895+
}
15896+
}
15897+
15898+
if (
15899+
curSymbolWithScope?.scope.type === ScopeType.Module ||
15900+
curSymbolWithScope?.scope.type === ScopeType.Builtin
15901+
) {
15902+
symbolWithScope = curSymbolWithScope;
15903+
}
15904+
}
15905+
1587315906
return symbolWithScope;
1587415907
}
1587515908

@@ -16093,7 +16126,13 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
1609316126
allowForwardReferences = true;
1609416127
}
1609516128

16096-
const symbolWithScope = lookUpSymbolRecursive(node, node.value, !allowForwardReferences);
16129+
const symbolWithScope = lookUpSymbolRecursive(
16130+
node,
16131+
node.value,
16132+
!allowForwardReferences,
16133+
allowForwardReferences
16134+
);
16135+
1609716136
if (symbolWithScope) {
1609816137
declarations.push(...symbolWithScope.symbol.getDeclarations());
1609916138
}

packages/pyright-internal/src/tests/samples/annotations1.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,24 @@ def func10():
5050
int,
5151
str
5252
]
53-
"""
53+
"""
54+
55+
56+
class ClassD:
57+
ClassA: "ClassA"
58+
59+
# This should generate an error because ClassF refers
60+
# to itself, and there is no ClassF declared at the module
61+
# level.
62+
ClassF: "ClassF"
63+
64+
str: "str"
65+
66+
def int(self):
67+
...
68+
69+
foo: "int"
70+
71+
# This should generate an error because it refers to the local
72+
# "int" symbol rather than the builtins "int".
73+
bar: int

packages/pyright-internal/src/tests/samples/annotations3.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,15 @@ def func3(self) -> "Optional[ClassC]":
2929
def func4(self) -> Optional["ClassC"]:
3030
return None
3131

32+
def func5(self, x: ClassA):
33+
x.func0()
34+
35+
class ClassA:
36+
...
37+
38+
def func6(self, x: ClassC):
39+
x.my_int
40+
3241

3342
class ClassC:
34-
pass
43+
my_int: int

packages/pyright-internal/src/tests/samples/circular1.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,12 @@
55
class Example1:
66
# This should not generate an error because "int"
77
# is not forward-declared.
8-
str: str = 4
8+
str: str = ""
99

1010
int = int
1111

1212
test: int
1313

1414

1515
class Example2:
16-
# This should generate an error because it's forward-declared.
1716
int: "int" = 4

packages/pyright-internal/src/tests/samples/function5.py

Lines changed: 0 additions & 15 deletions
This file was deleted.

packages/pyright-internal/src/tests/typeEvaluator1.test.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -605,12 +605,6 @@ test('Function4', () => {
605605
TestUtils.validateResults(analysisResults, 1);
606606
});
607607

608-
test('Function5', () => {
609-
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['function5.py']);
610-
611-
TestUtils.validateResults(analysisResults, 1);
612-
});
613-
614608
test('Function6', () => {
615609
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['function6.py']);
616610

@@ -656,7 +650,7 @@ test('FunctionMember2', () => {
656650
test('Annotations1', () => {
657651
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['annotations1.py']);
658652

659-
TestUtils.validateResults(analysisResults, 3);
653+
TestUtils.validateResults(analysisResults, 5);
660654
});
661655

662656
test('Annotations2', () => {

packages/pyright-internal/src/tests/typeEvaluator4.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -902,7 +902,7 @@ test('Annotated1', () => {
902902
test('Circular1', () => {
903903
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['circular1.py']);
904904

905-
TestUtils.validateResults(analysisResults, 2);
905+
TestUtils.validateResults(analysisResults, 0);
906906
});
907907

908908
test('TryExcept1', () => {

0 commit comments

Comments
 (0)