diff --git a/packages/pyright-internal/src/analyzer/parseTreeUtils.ts b/packages/pyright-internal/src/analyzer/parseTreeUtils.ts index b55c8e17e556..84e3ae6f452f 100644 --- a/packages/pyright-internal/src/analyzer/parseTreeUtils.ts +++ b/packages/pyright-internal/src/analyzer/parseTreeUtils.ts @@ -11,6 +11,7 @@ import * as AnalyzerNodeInfo from '../analyzer/analyzerNodeInfo'; import { containsOnlyWhitespace } from '../common/core'; import { assert, assertNever, fail } from '../common/debug'; import { convertPositionToOffset, convertTextRangeToRange } from '../common/positionUtils'; +import { PythonVersion, pythonVersion3_12 } from '../common/pythonVersion'; import { Position, Range, TextRange } from '../common/textRange'; import { TextRangeCollection, getIndexContaining } from '../common/textRangeCollection'; import { @@ -704,6 +705,57 @@ export function getEnclosingFunction(node: ParseNode): FunctionNode | undefined return undefined; } +// Determines whether a zero-argument `super()` call at the specified node +// can succeed at runtime. The zero-argument form uses the first argument of +// the frame that is executing the call along with the compiler-provided +// `__class__` cell, so the caller must separately verify that an enclosing +// class is present. This routine verifies only that the executing frame is +// one that receives a first argument. +export function isZeroArgSuperCallAllowed(node: ParseNode, pythonVersion: PythonVersion): boolean { + // Use the evaluation scope machinery to determine which frame executes + // the call. This handles decorators, parameter default values, class + // headers, lambdas, and comprehensions. + let curNode: ParseNode | undefined = getEvaluationScopeNode(node).node; + + while (curNode) { + switch (curNode.nodeType) { + case ParseNodeType.Function: + case ParseNodeType.Lambda: { + // The zero-argument form requires that the executing frame + // accept a first positional argument. + const firstParam = curNode.d.params.length > 0 ? curNode.d.params[0] : undefined; + return firstParam?.d.category === ParamCategory.Simple && firstParam.d.name !== undefined; + } + + case ParseNodeType.Comprehension: { + // A generator expression always executes in its own frame whose + // first argument is the implicit iterator, not the method's + // "self" argument. List, set, and dict comprehensions likewise + // used a separate frame prior to Python 3.12 (PEP 709). + if (curNode.d.isGenerator || PythonVersion.isLessThan(pythonVersion, pythonVersion3_12)) { + return false; + } + break; + } + + case ParseNodeType.TypeParameterList: { + // A type parameter scope is a proxy scope, so continue + // searching for the enclosing execution frame. + break; + } + + default: { + // A class body or module frame receives no first argument. + return false; + } + } + + curNode = curNode.parent ? getEvaluationScopeNode(curNode.parent).node : undefined; + } + + return false; +} + // This is similar to getEnclosingFunction except that it uses evaluation // scopes rather than the parse tree to determine whether the specified node // is within the scope. That means if the node is within a class decorator diff --git a/packages/pyright-internal/src/analyzer/typeEvaluator.ts b/packages/pyright-internal/src/analyzer/typeEvaluator.ts index 68b3343e6fae..2fa60e453d7b 100644 --- a/packages/pyright-internal/src/analyzer/typeEvaluator.ts +++ b/packages/pyright-internal/src/analyzer/typeEvaluator.ts @@ -9636,22 +9636,31 @@ export function createTypeEvaluator( if (enclosingClassType) { targetClassType = enclosingClassType ?? UnknownType.create(); + const functionInfo = enclosingFunction + ? getFunctionInfoFromDecorators(evaluatorInterface, enclosingFunction, /* isInClass */ true) + : undefined; + // Zero-argument forms of super are not allowed within static methods. // This results in a runtime exception. - if (enclosingFunction) { - const functionInfo = getFunctionInfoFromDecorators( - evaluatorInterface, - enclosingFunction, - /* isInClass */ true + if (functionInfo !== undefined && (functionInfo.flags & FunctionTypeFlags.StaticMethod) !== 0) { + addDiagnostic( + DiagnosticRule.reportGeneralTypeIssues, + LocMessage.superCallZeroArgFormStaticMethod(), + node.d.leftExpr + ); + } else if ( + !ParseTreeUtils.isZeroArgSuperCallAllowed( + node, + AnalyzerNodeInfo.getFileInfo(node).executionEnvironment.pythonVersion + ) + ) { + // The frame that executes the call doesn't receive a first + // argument, so the zero-argument form raises at runtime. + addDiagnostic( + DiagnosticRule.reportGeneralTypeIssues, + LocMessage.superCallZeroArgForm(), + node.d.leftExpr ); - - if ((functionInfo?.flags & FunctionTypeFlags.StaticMethod) !== 0) { - addDiagnostic( - DiagnosticRule.reportGeneralTypeIssues, - LocMessage.superCallZeroArgFormStaticMethod(), - node.d.leftExpr - ); - } } } else { addDiagnostic( diff --git a/packages/pyright-internal/src/tests/samples/super1.py b/packages/pyright-internal/src/tests/samples/super1.py index d1513b09d905..707d588083c3 100644 --- a/packages/pyright-internal/src/tests/samples/super1.py +++ b/packages/pyright-internal/src/tests/samples/super1.py @@ -46,9 +46,47 @@ def __init__(self): super().non_method1() def method(self): - def inner(): + def inner1(): + # This should generate an error because the frame that executes + # the zero-arg form of super receives no first argument. super().method1() + def inner2(obj): + # This is allowed because the frame receives a first argument, + # so `inner2(self)` succeeds at runtime. + super().method1() + + def inner3(value=super().method1()): + # Parameter default values are evaluated in the enclosing + # method's frame, so this is allowed. + pass + + async def inner4(): + # This should generate an error because the frame that executes + # the zero-arg form of super receives no first argument. + super().method1() + + async def inner5(obj): + # This is allowed because the frame receives a first argument. + super().method1() + + # This should generate an error because a lambda with no parameters + # receives no first argument. + lambda: super().method1() + + # This is allowed because the lambda receives a first argument. + lambda obj: super().method1() + + # As of Python 3.12, list, set, and dict comprehensions are inlined + # into the enclosing frame, so these are allowed. + [super().method1() for _ in [1]] + {super().method1() for _ in [1]} + {0: super().method1() for _ in [1]} + + # This should generate an error because a generator expression always + # executes in its own frame whose first argument is the iterator. + list(super().method1() for _ in [1]) + super(ClassD) diff --git a/packages/pyright-internal/src/tests/samples/super14.py b/packages/pyright-internal/src/tests/samples/super14.py new file mode 100644 index 000000000000..273b1c6e02bc --- /dev/null +++ b/packages/pyright-internal/src/tests/samples/super14.py @@ -0,0 +1,28 @@ +# This sample tests the handling of the zero-argument form of super() +# within a comprehension when the target Python version is older than 3.12. +# Prior to PEP 709, list, set, and dict comprehensions each executed in +# their own frame whose first argument was the implicit iterator. + + +class ClassA: + def method1(self): + pass + + +class ClassB(ClassA): + def method2(self): + # This should generate an error because a list comprehension + # executes in its own frame prior to Python 3.12. + [super().method1() for _ in [1]] + + # This should generate an error because a set comprehension + # executes in its own frame prior to Python 3.12. + {super().method1() for _ in [1]} + + # This should generate an error because a dict comprehension + # executes in its own frame prior to Python 3.12. + {0: super().method1() for _ in [1]} + + # This should generate an error because a generator expression + # executes in its own frame. + list(super().method1() for _ in [1]) diff --git a/packages/pyright-internal/src/tests/typeEvaluator2.test.ts b/packages/pyright-internal/src/tests/typeEvaluator2.test.ts index b6db2402a2f3..29840e1146db 100644 --- a/packages/pyright-internal/src/tests/typeEvaluator2.test.ts +++ b/packages/pyright-internal/src/tests/typeEvaluator2.test.ts @@ -9,7 +9,13 @@ */ import { ConfigOptions } from '../common/configOptions'; -import { pythonVersion3_10, pythonVersion3_13, pythonVersion3_15, pythonVersion3_9 } from '../common/pythonVersion'; +import { + pythonVersion3_10, + pythonVersion3_11, + pythonVersion3_13, + pythonVersion3_15, + pythonVersion3_9, +} from '../common/pythonVersion'; import { Uri } from '../common/uri/uri'; import * as TestUtils from './testUtils'; @@ -177,7 +183,7 @@ test('AugmentedAssignment3', () => { test('Super1', () => { const analysisResults = TestUtils.typeAnalyzeSampleFiles(['super1.py']); - TestUtils.validateResults(analysisResults, 6); + TestUtils.validateResults(analysisResults, 10); }); test('Super2', () => { @@ -252,6 +258,14 @@ test('Super13', () => { TestUtils.validateResults(analysisResults, 0); }); +test('Super14', () => { + const configOptions = new ConfigOptions(Uri.empty()); + configOptions.defaultPythonVersion = pythonVersion3_11; + const analysisResults = TestUtils.typeAnalyzeSampleFiles(['super14.py'], configOptions); + + TestUtils.validateResults(analysisResults, 4); +}); + test('MissingSuper1', () => { const configOptions = new ConfigOptions(Uri.empty());