From cba187233b7fb7ed2a6534f2aafacb2a4ed01f97 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Wed, 8 Jan 2025 10:19:18 -0700 Subject: [PATCH] Added missing syntax error check for positional argument that follows an unpacked keyword argument in a call expression. Also added a check for an unpack operator used within the first argument of a `cast` call. This addresses #9674. (#9675) --- .../pyright-internal/src/analyzer/typeEvaluator.ts | 8 ++++++++ .../pyright-internal/src/localization/localize.ts | 1 + .../src/localization/package.nls.en-us.json | 1 + packages/pyright-internal/src/parser/parser.ts | 14 ++++++++++++-- .../src/tests/samples/paramSpec49.py | 2 ++ .../src/tests/typeEvaluator4.test.ts | 2 +- 6 files changed, 25 insertions(+), 3 deletions(-) diff --git a/packages/pyright-internal/src/analyzer/typeEvaluator.ts b/packages/pyright-internal/src/analyzer/typeEvaluator.ts index c7fb9cbd1fd9..066e6cf805ce 100644 --- a/packages/pyright-internal/src/analyzer/typeEvaluator.ts +++ b/packages/pyright-internal/src/analyzer/typeEvaluator.ts @@ -10452,6 +10452,14 @@ export function createTypeEvaluator( // Evaluates the type of the "cast" call. function evaluateCastCall(argList: Arg[], errorNode: ExpressionNode) { + if (argList[0].argCategory !== ArgCategory.Simple && argList[0].valueExpression) { + addDiagnostic( + DiagnosticRule.reportInvalidTypeForm, + LocMessage.unpackInAnnotation(), + argList[0].valueExpression + ); + } + // Verify that the cast is necessary. let castToType = getTypeOfArgExpectingType(argList[0], { typeExpression: true }).type; diff --git a/packages/pyright-internal/src/localization/localize.ts b/packages/pyright-internal/src/localization/localize.ts index f68f88e411a2..f3239d985cd4 100644 --- a/packages/pyright-internal/src/localization/localize.ts +++ b/packages/pyright-internal/src/localization/localize.ts @@ -810,6 +810,7 @@ export namespace Localizer { export const patternNeverMatches = () => new ParameterizedString<{ type: string }>(getRawString('Diagnostic.patternNeverMatches')); export const positionArgAfterNamedArg = () => getRawString('Diagnostic.positionArgAfterNamedArg'); + export const positionArgAfterUnpackedDictArg = () => getRawString('Diagnostic.positionArgAfterUnpackedDictArg'); export const privateImportFromPyTypedModule = () => new ParameterizedString<{ name: string; module: string }>( getRawString('Diagnostic.privateImportFromPyTypedModule') 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 8f18337db6e4..d7a18edb167b 100644 --- a/packages/pyright-internal/src/localization/package.nls.en-us.json +++ b/packages/pyright-internal/src/localization/package.nls.en-us.json @@ -996,6 +996,7 @@ }, "patternNeverMatches": "Pattern will never be matched for subject type \"{type}\"", "positionArgAfterNamedArg": "Positional argument cannot appear after keyword arguments", + "positionArgAfterUnpackedDictArg": "Positional argument cannot appear after keyword argument unpacking", "positionOnlyAfterArgs": "Position-only parameter separator not allowed after \"*\" parameter", "positionOnlyAfterKeywordOnly": "\"/\" parameter must appear before \"*\" parameter", "positionOnlyAfterNon": "Position-only parameter not allowed after parameter that is not position-only", diff --git a/packages/pyright-internal/src/parser/parser.ts b/packages/pyright-internal/src/parser/parser.ts index 779daf6f56a1..218fb66cd594 100644 --- a/packages/pyright-internal/src/parser/parser.ts +++ b/packages/pyright-internal/src/parser/parser.ts @@ -3862,6 +3862,7 @@ export class Parser { private _parseArgList(): ArgListResult { const argList: ArgumentNode[] = []; let sawKeywordArg = false; + let sawUnpackedKeywordArg = false; let trailingComma = false; while (true) { @@ -3878,8 +3879,17 @@ export class Parser { const arg = this._parseArgument(); if (arg.d.name) { sawKeywordArg = true; - } else if (sawKeywordArg && arg.d.argCategory === ArgCategory.Simple) { - this._addSyntaxError(LocMessage.positionArgAfterNamedArg(), arg); + } else { + if (sawKeywordArg && arg.d.argCategory === ArgCategory.Simple) { + this._addSyntaxError(LocMessage.positionArgAfterNamedArg(), arg); + } + + if (sawUnpackedKeywordArg && arg.d.argCategory !== ArgCategory.UnpackedDictionary) { + this._addSyntaxError(LocMessage.positionArgAfterUnpackedDictArg(), arg); + } + } + if (arg.d.argCategory === ArgCategory.UnpackedDictionary) { + sawUnpackedKeywordArg = true; } argList.push(arg); diff --git a/packages/pyright-internal/src/tests/samples/paramSpec49.py b/packages/pyright-internal/src/tests/samples/paramSpec49.py index a12574f6c220..5869d2bb6f80 100644 --- a/packages/pyright-internal/src/tests/samples/paramSpec49.py +++ b/packages/pyright-internal/src/tests/samples/paramSpec49.py @@ -30,6 +30,8 @@ def inner0(*args: P.args, **kwargs: P.kwargs) -> None: self.dispatcher.dispatch(stub, 1, *args, **kwargs) def inner1(*args: P.args, **kwargs: P.kwargs) -> None: + # This should generate an error because a positional argument + # cannot appear after an unpacked keyword argument. self.dispatcher.dispatch(stub, 1, **kwargs, *args) def inner2(*args: P.args, **kwargs: P.kwargs) -> None: diff --git a/packages/pyright-internal/src/tests/typeEvaluator4.test.ts b/packages/pyright-internal/src/tests/typeEvaluator4.test.ts index 41a483d5797a..c225fd78cf53 100644 --- a/packages/pyright-internal/src/tests/typeEvaluator4.test.ts +++ b/packages/pyright-internal/src/tests/typeEvaluator4.test.ts @@ -829,7 +829,7 @@ test('ParamSpec48', () => { test('ParamSpec49', () => { const results = TestUtils.typeAnalyzeSampleFiles(['paramSpec49.py']); - TestUtils.validateResults(results, 7); + TestUtils.validateResults(results, 8); }); test('ParamSpec50', () => {