From 8b38b5dbf902794aef63449890311f040d2cfe57 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Wed, 12 Jul 2023 00:24:14 +0200 Subject: [PATCH] Added support for deferred annotation evaluation for `Annotated` type arguments beyond the first one. This addresses https://github.com/microsoft/pylance-release/issues/4565. (#5472) Co-authored-by: Eric Traut --- .../pyright-internal/src/analyzer/binder.ts | 27 ++++++++++++- .../src/analyzer/typeEvaluator.ts | 39 +++++++++++++------ .../src/tests/samples/annotated2.py | 18 +++++++++ .../src/tests/typeEvaluator4.test.ts | 6 +++ 4 files changed, 77 insertions(+), 13 deletions(-) create mode 100644 packages/pyright-internal/src/tests/samples/annotated2.py diff --git a/packages/pyright-internal/src/analyzer/binder.ts b/packages/pyright-internal/src/analyzer/binder.ts index af1b35fa8..336d53ba6 100644 --- a/packages/pyright-internal/src/analyzer/binder.ts +++ b/packages/pyright-internal/src/analyzer/binder.ts @@ -229,6 +229,9 @@ export class Binder extends ParseTreeWalker { // Are we currently binding code located within an except block? private _isInExceptSuite = false; + // Are we currently walking the type arguments to an Annotated type annotation? + private _isInAnnotatedAnnotation = false; + // A list of names assigned to __slots__ within a class. private _dunderSlotsEntries: StringListNode[] | undefined; @@ -651,7 +654,12 @@ export class Binder extends ParseTreeWalker { // and this can lead to a performance issue when walking the control // flow graph if we need to evaluate every decorator. if (!ParseTreeUtils.isNodeContainedWithinNodeType(node, ParseNodeType.Decorator)) { - this._createCallFlowNode(node); + // Skip if we're in an 'Annotated' annotation because this creates + // problems for "No Return" return type analysis when annotation + // evaluation is deferred. + if (!this._isInAnnotatedAnnotation) { + this._createCallFlowNode(node); + } } // Is this an manipulation of dunder all? @@ -1269,7 +1277,22 @@ export class Binder extends ParseTreeWalker { override visitIndex(node: IndexNode): boolean { AnalyzerNodeInfo.setFlowNode(node, this._currentFlowNode!); - return true; + + this.walk(node.baseExpression); + + // If we're within an 'Annotated' type annotation, set the flag. + const wasInAnnotatedAnnotation = this._isInAnnotatedAnnotation; + if (this._isTypingAnnotation(node.baseExpression, 'Annotated')) { + this._isInAnnotatedAnnotation = true; + } + + node.items.forEach((argNode) => { + this.walk(argNode); + }); + + this._isInAnnotatedAnnotation = wasInAnnotatedAnnotation; + + return false; } override visitIf(node: IfNode): boolean { diff --git a/packages/pyright-internal/src/analyzer/typeEvaluator.ts b/packages/pyright-internal/src/analyzer/typeEvaluator.ts index c22ffb028..9bd261f90 100644 --- a/packages/pyright-internal/src/analyzer/typeEvaluator.ts +++ b/packages/pyright-internal/src/analyzer/typeEvaluator.ts @@ -7171,17 +7171,31 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions let typeResult: TypeResultWithNode; // If it's a custom __class_getitem__, none of the arguments should be - // treated as types. If it's an Annotated[a, b, c], only the first index - // should be treated as a type. The others can be regular (non-type) objects. - if (options?.hasCustomClassGetItem || (options?.isAnnotatedClass && argIndex > 0)) { + // treated as types. + if (options?.hasCustomClassGetItem) { + adjFlags = + EvaluatorFlags.DisallowParamSpec | + EvaluatorFlags.DisallowTypeVarTuple | + EvaluatorFlags.DoNotSpecialize | + EvaluatorFlags.DisallowClassVar; typeResult = { - ...getTypeOfExpression( - expr, - EvaluatorFlags.DisallowParamSpec | - EvaluatorFlags.DisallowTypeVarTuple | - EvaluatorFlags.DoNotSpecialize | - EvaluatorFlags.DisallowClassVar - ), + ...getTypeOfExpression(expr, adjFlags), + node: expr, + }; + } else if (options?.isAnnotatedClass && argIndex > 0) { + // If it's an Annotated[a, b, c], only the first index should be + // treated as a type.The others can be regular(non - type) objects. + adjFlags = + EvaluatorFlags.DisallowParamSpec | + EvaluatorFlags.DisallowTypeVarTuple | + EvaluatorFlags.DoNotSpecialize | + EvaluatorFlags.DisallowClassVar; + if (isAnnotationEvaluationPostponed(AnalyzerNodeInfo.getFileInfo(node))) { + adjFlags |= EvaluatorFlags.AllowForwardReferences; + } + + typeResult = { + ...getTypeOfExpression(expr, adjFlags), node: expr, }; } else { @@ -7564,7 +7578,10 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions if (node.leftExpression.nodeType === ParseNodeType.Lambda) { baseTypeResult = getTypeOfLambdaForCall(node, inferenceContext); } else { - baseTypeResult = getTypeOfExpression(node.leftExpression, EvaluatorFlags.DoNotSpecialize); + baseTypeResult = getTypeOfExpression( + node.leftExpression, + EvaluatorFlags.DoNotSpecialize | (flags & EvaluatorFlags.AllowForwardReferences) + ); } const argList = node.arguments.map((arg) => { diff --git a/packages/pyright-internal/src/tests/samples/annotated2.py b/packages/pyright-internal/src/tests/samples/annotated2.py new file mode 100644 index 000000000..b854c12f3 --- /dev/null +++ b/packages/pyright-internal/src/tests/samples/annotated2.py @@ -0,0 +1,18 @@ +# This sample tests the case where Annotated is used with deferred +# annotation evaluation. + +from __future__ import annotations +from typing import Annotated + + +v1: Annotated[str, ClassA, func1(), v2[0]] = "" + +v2 = [1, 2, 3] + + +class ClassA: + ... + + +def func1(): + ... diff --git a/packages/pyright-internal/src/tests/typeEvaluator4.test.ts b/packages/pyright-internal/src/tests/typeEvaluator4.test.ts index 241a88fa9..0c3da0d7c 100644 --- a/packages/pyright-internal/src/tests/typeEvaluator4.test.ts +++ b/packages/pyright-internal/src/tests/typeEvaluator4.test.ts @@ -1146,6 +1146,12 @@ test('Annotated1', () => { TestUtils.validateResults(analysisResults39, 3); }); +test('Annotated2', () => { + const analysisResults = TestUtils.typeAnalyzeSampleFiles(['annotated2.py']); + + TestUtils.validateResults(analysisResults, 0); +}); + test('Circular1', () => { const analysisResults = TestUtils.typeAnalyzeSampleFiles(['circular1.py']);