Skip to content

Commit

Permalink
Added missing check for a PEP 695 type parameter being the target of …
Browse files Browse the repository at this point in the history
…a `nonlocal` statement. This results in a runtime exception, so it should be reported as an error. This addresses #9817. (#9821)
  • Loading branch information
erictraut authored Feb 5, 2025
1 parent b105a38 commit b28da12
Show file tree
Hide file tree
Showing 5 changed files with 38 additions and 2 deletions.
18 changes: 18 additions & 0 deletions packages/pyright-internal/src/analyzer/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1440,6 +1440,8 @@ export class Checker extends ParseTreeWalker {
this._evaluator.getType(name);

this.walk(name);

this._validateNonlocalTypeParam(name);
});
});

Expand Down Expand Up @@ -1909,6 +1911,22 @@ export class Checker extends ParseTreeWalker {
}
}

// Verifies that the target of a nonlocal statement is not a PEP 695-style
// TypeParameter. This situation results in a runtime exception.
private _validateNonlocalTypeParam(node: NameNode) {
// Look up the symbol to see if it's a type parameter.
const symbolWithScope = this._evaluator.lookUpSymbolRecursive(node, node.d.value, /* honorCodeFlow */ false);
if (!symbolWithScope || symbolWithScope.scope.type !== ScopeType.TypeParameter) {
return;
}

this._evaluator.addDiagnostic(
DiagnosticRule.reportGeneralTypeIssues,
LocMessage.nonlocalTypeParam().format({ name: node.d.value }),
node
);
}

private _validateExhaustiveMatch(node: MatchNode) {
// This check can be expensive, so skip it if it's disabled.
if (this._fileInfo.diagnosticRuleSet.reportMatchNotExhaustive === 'none') {
Expand Down
4 changes: 3 additions & 1 deletion packages/pyright-internal/src/localization/localize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -703,15 +703,17 @@ export namespace Localizer {
new ParameterizedString<{ operator: string }>(getRawString('Diagnostic.noneOperator'));
export const noneUnknownMember = () =>
new ParameterizedString<{ name: string }>(getRawString('Diagnostic.noneUnknownMember'));
export const nonLocalInModule = () => getRawString('Diagnostic.nonLocalInModule');
export const nonLocalNoBinding = () =>
new ParameterizedString<{ name: string }>(getRawString('Diagnostic.nonLocalNoBinding'));
export const nonLocalReassignment = () =>
new ParameterizedString<{ name: string }>(getRawString('Diagnostic.nonLocalReassignment'));
export const nonLocalRedefinition = () =>
new ParameterizedString<{ name: string }>(getRawString('Diagnostic.nonLocalRedefinition'));
export const nonLocalInModule = () => getRawString('Diagnostic.nonLocalInModule');
export const noOverload = () =>
new ParameterizedString<{ name: string }>(getRawString('Diagnostic.noOverload'));
export const nonlocalTypeParam = () =>
new ParameterizedString<{ name: string }>(getRawString('Diagnostic.nonlocalTypeParam'));
export const noReturnContainsReturn = () => getRawString('Diagnostic.noReturnContainsReturn');
export const noReturnContainsYield = () => getRawString('Diagnostic.noReturnContainsYield');
export const noReturnReturnsNone = () => getRawString('Diagnostic.noReturnReturnsNone');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -844,6 +844,10 @@
"message": "\"{name}\" was already declared nonlocal",
"comment": "{Locked='nonlocal'}"
},
"nonlocalTypeParam": {
"message": "Nonlocal binding is not allowed for type parameter \"{name}\"",
"comment": ["{StrContains=i'nonlocal'}", "'nonlocal' is a keyword and should not be localized. It is only capitalized here because it is the first word in the sentence"]
},
"noneNotCallable": {
"message": "Object of type \"None\" cannot be called",
"comment": "{Locked='None'}"
Expand Down
12 changes: 12 additions & 0 deletions packages/pyright-internal/src/tests/samples/typeParams1.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,15 @@ def method2[T](self, v: ClassI3) -> None: ...


class ClassI3: ...


def func9[T, **P, S](x: T) -> T:
S = 1

def inner():
# This should generate two errors.
nonlocal T, P

nonlocal S

return x
2 changes: 1 addition & 1 deletion packages/pyright-internal/src/tests/typeEvaluator5.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ test('TypeParams1', () => {
configOptions.defaultPythonVersion = pythonVersion3_12;

const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeParams1.py'], configOptions);
TestUtils.validateResults(analysisResults, 6);
TestUtils.validateResults(analysisResults, 8);
});

test('TypeParams2', () => {
Expand Down

0 comments on commit b28da12

Please sign in to comment.