Fix completions for typeddicts with errors (#11177)
Some checks failed
Run mypy_primer on push / Run mypy_primer on push (push) Has been cancelled
Validation / Typecheck (push) Has been cancelled
Validation / Style (push) Has been cancelled
Validation / Test macos-latest (push) Has been cancelled
Validation / Test ubuntu-latest (push) Has been cancelled
Validation / Test windows-latest (push) Has been cancelled
Validation / Build (push) Has been cancelled
Validation / Required (push) Has been cancelled

This commit is contained in:
Rich Chiodo 2025-12-19 09:09:17 -08:00 committed by GitHub
parent 45159a96da
commit 1576956c32
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 136 additions and 5 deletions

View file

@ -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.
argList.forEach((arg) => {
if (arg.valueExpression && !isSpeculativeModeInUse(arg.valueExpression)) {
getTypeOfExpression(arg.valueExpression);
// 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)) {
// 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;

View file

@ -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'" },
},
],
},
});
});