-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Report illegal zero-argument super() in nested functions and lambdas #11643
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
| } | ||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This duplicates evaluation-scope boundary handling already provided by |
||
| // 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 | ||
|
|
||
| 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]) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
📍 packages/pyright-internal/src/tests/typeEvaluator2.test.ts:183 [verified] |
||
| }); | ||
|
|
||
| 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()); | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.