diff --git a/packages/pyright-internal/src/analyzer/checker.ts b/packages/pyright-internal/src/analyzer/checker.ts index aeead272ebad..753a4195fe01 100644 --- a/packages/pyright-internal/src/analyzer/checker.ts +++ b/packages/pyright-internal/src/analyzer/checker.ts @@ -1440,6 +1440,8 @@ export class Checker extends ParseTreeWalker { this._evaluator.getType(name); this.walk(name); + + this._validateNonlocalTypeParam(name); }); }); @@ -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') { diff --git a/packages/pyright-internal/src/localization/localize.ts b/packages/pyright-internal/src/localization/localize.ts index 05221f7f811a..7598a1b75d77 100644 --- a/packages/pyright-internal/src/localization/localize.ts +++ b/packages/pyright-internal/src/localization/localize.ts @@ -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'); diff --git a/packages/pyright-internal/src/localization/package.nls.en-us.json b/packages/pyright-internal/src/localization/package.nls.en-us.json index bf01a07582ed..7f99557df7be 100644 --- a/packages/pyright-internal/src/localization/package.nls.en-us.json +++ b/packages/pyright-internal/src/localization/package.nls.en-us.json @@ -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'}" diff --git a/packages/pyright-internal/src/tests/samples/typeParams1.py b/packages/pyright-internal/src/tests/samples/typeParams1.py index 2edcb758a3ff..adbff305ca9a 100644 --- a/packages/pyright-internal/src/tests/samples/typeParams1.py +++ b/packages/pyright-internal/src/tests/samples/typeParams1.py @@ -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 diff --git a/packages/pyright-internal/src/tests/typeEvaluator5.test.ts b/packages/pyright-internal/src/tests/typeEvaluator5.test.ts index ca8dce83fc8e..2fcec03324d8 100644 --- a/packages/pyright-internal/src/tests/typeEvaluator5.test.ts +++ b/packages/pyright-internal/src/tests/typeEvaluator5.test.ts @@ -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', () => {