From f2b72a3f28573bb48f43bc2fa827624c0ad63892 Mon Sep 17 00:00:00 2001 From: Rich Chiodo false Date: Wed, 17 Dec 2025 10:24:31 -0800 Subject: [PATCH] Fix completions for typeddicts with errors --- .../src/analyzer/typeEvaluator.ts | 26 +++- .../src/tests/completions.test.ts | 113 ++++++++++++++++++ 2 files changed, 135 insertions(+), 4 deletions(-) diff --git a/packages/pyright-internal/src/analyzer/typeEvaluator.ts b/packages/pyright-internal/src/analyzer/typeEvaluator.ts index 5b64e469345e..43a7520dc201 100644 --- a/packages/pyright-internal/src/analyzer/typeEvaluator.ts +++ b/packages/pyright-internal/src/analyzer/typeEvaluator.ts @@ -12503,13 +12503,28 @@ export function createTypeEvaluator( if (matchResults.argumentErrors) { // Evaluate types of all args. This will ensure that referenced symbols are - // not reported as unaccessed. + // not reported as unaccessed. Also pass the expected parameter type as + // inference context to enable proper completions even when there are errors. + matchResults.argParams.forEach((argParam) => { + if (argParam.argument.valueExpression && !isSpeculativeModeInUse(argParam.argument.valueExpression)) { + getTypeOfExpression( + argParam.argument.valueExpression, + /* flags */ undefined, + makeInferenceContext(argParam.paramType) + ); + } + }); + + // Also evaluate any arguments that weren't matched to parameters argList.forEach((arg) => { if (arg.valueExpression && !isSpeculativeModeInUse(arg.valueExpression)) { - getTypeOfExpression(arg.valueExpression); + // Check if this argument was already evaluated above + const wasEvaluated = matchResults.argParams.some((argParam) => argParam.argument === arg); + if (!wasEvaluated) { + getTypeOfExpression(arg.valueExpression); + } } }); - // Use a return type of Unknown but attach a "possible type" to it // so the completion provider can suggest better completions. const possibleType = FunctionType.getEffectiveReturnType(typeResult.type); @@ -12786,7 +12801,10 @@ export function createTypeEvaluator( if (argParam.argType) { argType = argParam.argType; } else { - const argTypeResult = getTypeOfArg(argParam.argument, /* inferenceContext */ undefined); + const argTypeResult = getTypeOfArg( + argParam.argument, + makeInferenceContext(argParam.paramType, isTypeIncomplete) + ); argType = argTypeResult.type; if (argTypeResult.isIncomplete) { isTypeIncomplete = true; diff --git a/packages/pyright-internal/src/tests/completions.test.ts b/packages/pyright-internal/src/tests/completions.test.ts index 2505f25ad52a..646bcae96cf9 100644 --- a/packages/pyright-internal/src/tests/completions.test.ts +++ b/packages/pyright-internal/src/tests/completions.test.ts @@ -1480,3 +1480,116 @@ test('overloaded Literal[...] suggestions in call arguments', async () => { }, }); }); + +test('nested TypedDict completion with Unpack - without other fields', async () => { + const code = ` +// @filename: test.py +//// from typing import Unpack, TypedDict +//// +//// class InnerDict(TypedDict): +//// a: int +//// b: str +//// +//// class OuterDict(TypedDict): +//// inner: InnerDict +//// field_1: str +//// +//// def test_inner_dict(**kwargs: Unpack[OuterDict]): +//// pass +//// +//// test_inner_dict(inner={[|/*marker*/|]}) + `; + + const state = parseAndGetTestState(code).state; + + await state.verifyCompletion('included', 'markdown', { + marker: { + completions: [ + { + kind: CompletionItemKind.Constant, + label: "'a'", + textEdit: { range: state.getPositionRange('marker'), newText: "'a'" }, + }, + { + kind: CompletionItemKind.Constant, + label: "'b'", + textEdit: { range: state.getPositionRange('marker'), newText: "'b'" }, + }, + ], + }, + }); +}); + +test('nested TypedDict completion with Unpack - with other fields', async () => { + const code = ` +// @filename: test.py +//// from typing import Unpack, TypedDict +//// +//// class InnerDict(TypedDict): +//// a: int +//// b: str +//// +//// class OuterDict(TypedDict): +//// inner: InnerDict +//// field_1: str +//// +//// def test_inner_dict(**kwargs: Unpack[OuterDict]): +//// pass +//// +//// test_inner_dict(field_1="test", inner={[|/*marker*/|]}) + `; + + const state = parseAndGetTestState(code).state; + + await state.verifyCompletion('included', 'markdown', { + marker: { + completions: [ + { + kind: CompletionItemKind.Constant, + label: '"a"', + textEdit: { range: state.getPositionRange('marker'), newText: '"a"' }, + }, + { + kind: CompletionItemKind.Constant, + label: '"b"', + textEdit: { range: state.getPositionRange('marker'), newText: '"b"' }, + }, + ], + }, + }); +}); + +test('simple nested TypedDict completion - no Unpack', async () => { + const code = ` +// @filename: test.py +//// from typing import TypedDict +//// +//// class InnerDict(TypedDict): +//// a: int +//// b: str +//// +//// def test_func(inner: InnerDict): +//// pass +//// +//// test_func(inner={[|/*marker*/|]}) + `; + + const state = parseAndGetTestState(code).state; + + await state.verifyCompletion('included', 'markdown', { + marker: { + completions: [ + { + kind: CompletionItemKind.Constant, + label: "'a'", + textEdit: { range: state.getPositionRange('marker'), newText: "'a'" }, + }, + { + kind: CompletionItemKind.Constant, + label: "'b'", + textEdit: { range: state.getPositionRange('marker'), newText: "'b'" }, + }, + ], + }, + }); +});