PY-54151 TypedDict completion at callee does not work for methods

GitOrigin-RevId: 5a037490d8c24ccdd354e8396540a21294148a7e
This commit is contained in:
Petr
2024-05-14 22:52:52 +00:00
committed by intellij-monorepo-bot
parent 8452c15aff
commit aefaeaa52a
6 changed files with 77 additions and 20 deletions
@@ -11,8 +11,12 @@ import com.intellij.util.ProcessingContext
import com.jetbrains.python.PyNames
import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil
import com.jetbrains.python.psi.*
import com.jetbrains.python.psi.impl.PyPsiUtils
import com.jetbrains.python.psi.types.*
import com.jetbrains.python.psi.impl.PyCallExpressionHelper
import com.jetbrains.python.psi.resolve.PyResolveContext
import com.jetbrains.python.psi.types.PyClassType
import com.jetbrains.python.psi.types.PyType
import com.jetbrains.python.psi.types.PyTypedDictType
import com.jetbrains.python.psi.types.TypeEvalContext
/**
* Provides completion variants for keys of dict literals marked as TypedDict
@@ -45,20 +49,11 @@ private class DictLiteralCompletionProvider : CompletionProvider<CompletionParam
private fun addCompletionToCallExpression(originalElement: PsiElement,
possibleSequenceExpr: PySequenceExpression,
result: CompletionResultSet) {
val callExpression = PsiTreeUtil.getParentOfType(originalElement, PyCallExpression::class.java)
if (callExpression != null) {
val typeEvalContext = TypeEvalContext.codeCompletion(originalElement.project, originalElement.containingFile)
val callType = typeEvalContext.getType(callExpression.callee ?: return)
if (callType !is PyCallableType) return
val argumentIndex = PyPsiUtils.findArgumentIndex(callExpression, possibleSequenceExpr)
if (argumentIndex < 0) return
val params = callType.getParameters(typeEvalContext) ?: return
if (params.size <= argumentIndex) return
val expectedType = params[argumentIndex].getType(typeEvalContext)
val actualType = typeEvalContext.getType(possibleSequenceExpr)
addCompletionForTypedDictKeys(expectedType, actualType, result, getForcedQuote(possibleSequenceExpr, originalElement))
val typeEvalContext = TypeEvalContext.codeCompletion(originalElement.project, originalElement.containingFile)
val actualType = typeEvalContext.getType(possibleSequenceExpr)
val quote = getForcedQuote(possibleSequenceExpr, originalElement)
PyCallExpressionHelper.getMappedParameters(possibleSequenceExpr, PyResolveContext.defaultContext(typeEvalContext)).forEach {
addCompletionForTypedDictKeys(it.getType(typeEvalContext), actualType, result, quote)
}
}
@@ -39,11 +39,9 @@ private class PyLiteralTypeCompletionProvider : CompletionProvider<CompletionPar
val callSiteExpr = PsiTreeUtil.getParentOfType(position, PyCallSiteExpression::class.java)
if (callSiteExpr != null) {
val parent = position.parent
val argumentExpr = if (parent is PyKeywordArgument && parent.valueExpression == position) parent else position
val types = PyCallExpressionHelper
.mapArguments(callSiteExpr, PyResolveContext.defaultContext(typeEvalContext))
.mapNotNull { it.mappedParameters[argumentExpr]?.getArgumentType(typeEvalContext) }
.getMappedParameters(position, PyResolveContext.defaultContext(typeEvalContext))
.mapNotNull { it.getArgumentType(typeEvalContext) }
addToResult(position, types, result)
return
}
@@ -26,6 +26,7 @@ import com.jetbrains.python.psi.types.*;
import com.jetbrains.python.pyi.PyiUtil;
import com.jetbrains.python.toolbox.Maybe;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -750,6 +751,33 @@ public final class PyCallExpressionHelper {
return null;
}
/**
* {@code argument} can be (parenthesized) expression or a value of a {@link PyKeywordArgument}
*/
@ApiStatus.Internal
@NotNull
public static List<PyCallableParameter> getMappedParameters(@NotNull PyExpression argument,
@NotNull PyResolveContext resolveContext) {
while (argument.getParent() instanceof PyParenthesizedExpression parenthesizedExpr) {
argument = parenthesizedExpr;
}
if (argument.getParent() instanceof PyKeywordArgument keywordArgument && keywordArgument.getValueExpression() == argument) {
argument = keywordArgument;
}
PsiElement parent = argument.getParent();
if (parent instanceof PyArgumentList) {
parent = parent.getParent();
}
if (!(parent instanceof PyCallSiteExpression callSite)) {
return Collections.emptyList();
}
PyExpression finalArgument = argument;
return ContainerUtil.mapNotNull(mapArguments(callSite, resolveContext), mapping -> mapping.getMappedParameters().get(finalArgument));
}
/**
* Gets implicit offset from the {@code callableType},
* should be used with the methods below since they specify correct offset value.
@@ -0,0 +1,14 @@
from typing import TypedDict
class Movie (TypedDict):
name: str
age: int
class Foo:
def foo(self, movie: Movie) -> None:
pass
Foo().foo(((({<caret>}))))
@@ -0,0 +1,14 @@
from typing import TypedDict
class Movie (TypedDict):
name: str
age: int
class Foo:
def foo(self, movie: Movie) -> None:
pass
Foo().foo(movie=((({<caret>}))))
@@ -9,6 +9,14 @@ class PyDictLiteralCompletionTest : PyTestCase() {
assertCompletionContains("\"x\"", "\"y\"")
}
fun testEmptyLiteralsInMethodCall() {
assertCompletionContains("\"name\"", "\"age\"")
}
fun testEmptyLiteralsInMethodCallWithKeywordArgument() {
assertCompletionContains("\"name\"", "\"age\"")
}
fun testEmptyLiteralsInCallExpressionsWithQuotes() {
assertCompletionContains("x", "y")
}