Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions packages/pyright-internal/src/analyzer/parseTreeUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue · Please address or respond

This accepts all comprehensions, but generator expressions retain a separate frame whose implicit iterator argument is not the method instance. List, set, and dict comprehensions likewise had separate frames before Python 3.12. Distinguish comprehension kinds and the configured Python version so runtime-failing forms receive the diagnostic.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning · Non-blocking recommendation

This duplicates evaluation-scope boundary handling already provided by getEvaluationScopeNode, including decorators, defaults, class headers, lambdas, and comprehensions. Derive this predicate from that machinery with only the needed frame-specific logic so the two scope definitions do not drift.

// 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
Expand Down
35 changes: 22 additions & 13 deletions packages/pyright-internal/src/analyzer/typeEvaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
40 changes: 39 additions & 1 deletion packages/pyright-internal/src/tests/samples/super1.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
28 changes: 28 additions & 0 deletions packages/pyright-internal/src/tests/samples/super14.py
Original file line number Diff line number Diff line change
@@ -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])
18 changes: 16 additions & 2 deletions packages/pyright-internal/src/tests/typeEvaluator2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -177,7 +183,7 @@ test('AugmentedAssignment3', () => {
test('Super1', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['super1.py']);

TestUtils.validateResults(analysisResults, 6);
TestUtils.validateResults(analysisResults, 10);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning · Non-blocking recommendation

📍 packages/pyright-internal/src/tests/typeEvaluator2.test.ts:183
The prior test-precision feedback remains unresolved: an aggregate count can pass if a missing diagnostic is offset by one on a valid expression. Add range-specific assertions, or split valid and invalid scenarios into separately analyzed samples, so each new case is independently protected.

[verified]

});

test('Super2', () => {
Expand Down Expand Up @@ -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());

Expand Down