mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
PY-90614 python: report Unpack[TypedDict] kwargs mismatch once per call
A call's `**kwargs` argument was checked separately against each overload
candidate, so the same warning was reported multiple times.
Also see through parentheses around a `**`-unpacked dict literal
(`**({...})`) when checking it against a `TypedDict`.
GitOrigin-RevId: 1df029e928a8a20c854e2e4c11ce0301a62996b4
This commit is contained in:
committed by
intellij-monorepo-bot
parent
464c62d5a6
commit
0632f905f6
@@ -78,6 +78,10 @@ inline fun <reified T : PsiElement> PsiElement.parentOfType(withSelf: Boolean =
|
||||
return PsiTreeUtil.getParentOfType(this, T::class.java, !withSelf)
|
||||
}
|
||||
|
||||
inline fun <reified T : PsiElement> PsiElement.childOfType(withSelf: Boolean = false): T? {
|
||||
return PsiTreeUtil.findChildOfType(this, T::class.java, !withSelf)
|
||||
}
|
||||
|
||||
@Deprecated("Use parentOfTypes()", ReplaceWith("parentOfTypes(*classes)"))
|
||||
fun <T : PsiElement> PsiElement.parentOfType(vararg classes: KClass<out T>): T? {
|
||||
return parentOfTypes(*classes)
|
||||
|
||||
+40
-27
@@ -11,6 +11,7 @@ import com.intellij.openapi.util.Ref
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.PsiElementVisitor
|
||||
import com.intellij.psi.util.PsiTreeUtil
|
||||
import com.intellij.psi.util.childOfType
|
||||
import com.jetbrains.python.PyNames
|
||||
import com.jetbrains.python.PyPsiBundle
|
||||
import com.jetbrains.python.ast.PyAstFunction
|
||||
@@ -125,8 +126,6 @@ import com.jetbrains.python.psi.types.isNoneType
|
||||
import com.jetbrains.python.psi.types.isObject
|
||||
import com.jetbrains.python.pyi.PyiUtil.isOverload
|
||||
import org.jetbrains.annotations.PropertyKey
|
||||
import java.util.Objects
|
||||
import java.util.function.Supplier
|
||||
import kotlin.math.min
|
||||
|
||||
open class PyTypeCheckerInspection : PyInspection() {
|
||||
@@ -146,6 +145,8 @@ open class PyTypeCheckerInspection : PyInspection() {
|
||||
open class Visitor(holder: ProblemsHolder, context: TypeEvalContext) : PyInspectionVisitor(holder, context) {
|
||||
override val holder = super.holder!!
|
||||
|
||||
private val typedDictProblemReporter = TypedDictProblemReporter()
|
||||
|
||||
// TODO: Visit decorators with arguments
|
||||
override fun visitPyCallExpression(node: PyCallExpression) {
|
||||
checkCallSite(node)
|
||||
@@ -707,9 +708,9 @@ open class PyTypeCheckerInspection : PyInspection() {
|
||||
private fun reportTypedDictProblems(expectedType: PyTypedDictType, expression: PyExpression) {
|
||||
val result = TypeCheckingResult()
|
||||
checkExpression(expectedType, expression, myTypeEvalContext, result)
|
||||
result.valueTypeErrors.forEach { error: ValueTypeError? ->
|
||||
val actualExpression = error!!.actualExpression ?: return@forEach
|
||||
PyTypeCheckerProblemReporter.report(
|
||||
result.valueTypeErrors.forEach { error ->
|
||||
val actualExpression = error.actualExpression ?: return@forEach
|
||||
typedDictProblemReporter.report(
|
||||
holder,
|
||||
PyTypeCheckerSuppressionCode.BAD_TYPED_DICT,
|
||||
actualExpression,
|
||||
@@ -717,19 +718,19 @@ open class PyTypeCheckerInspection : PyInspection() {
|
||||
effectiveHighlightType(ProblemHighlightType.GENERIC_ERROR_OR_WARNING)
|
||||
)
|
||||
}
|
||||
result.extraKeys.forEach { error: ExtraKeyError? ->
|
||||
PyTypeCheckerProblemReporter.report(
|
||||
result.extraKeys.forEach { error ->
|
||||
typedDictProblemReporter.report(
|
||||
holder,
|
||||
PyTypeCheckerSuppressionCode.BAD_TYPED_DICT_KEY,
|
||||
Objects.requireNonNullElse<PyExpression?>(error!!.actualExpression, expression),
|
||||
error.actualExpression ?: expression,
|
||||
PyPsiBundle.problemMessage("INSP.type.checker.typed.dict.extra.key", error.key, error.expectedTypedDictName)
|
||||
)
|
||||
}
|
||||
result.missingKeys.forEach { error: MissingKeysError? ->
|
||||
PyTypeCheckerProblemReporter.report(
|
||||
result.missingKeys.forEach { error ->
|
||||
typedDictProblemReporter.report(
|
||||
holder,
|
||||
PyTypeCheckerSuppressionCode.BAD_TYPED_DICT,
|
||||
if (error!!.actualExpression != null) error.actualExpression else expression,
|
||||
error.actualExpression ?: expression,
|
||||
PyPsiBundle.problemMessage(
|
||||
"INSP.type.checker.typed.dict.missing.keys", error.expectedTypedDictName,
|
||||
error.missingKeys.size,
|
||||
@@ -743,11 +744,6 @@ open class PyTypeCheckerInspection : PyInspection() {
|
||||
expectedType: PyUnpackedTypedDictType,
|
||||
expression: PyExpression,
|
||||
) {
|
||||
var expression: PyExpression? = expression
|
||||
if (expression is PyStarArgument) {
|
||||
expression = PsiTreeUtil.findChildOfType(expression, PyExpression::class.java)
|
||||
}
|
||||
if (expression == null) return
|
||||
val argumentType = myTypeEvalContext.getType(expression)
|
||||
val typedDictType = expectedType.typedDictType
|
||||
if (isDictExpression(expression, myTypeEvalContext)) {
|
||||
@@ -755,7 +751,7 @@ open class PyTypeCheckerInspection : PyInspection() {
|
||||
return
|
||||
}
|
||||
if (!match(typedDictType, argumentType, myTypeEvalContext)) {
|
||||
PyTypeCheckerProblemReporter.report(
|
||||
typedDictProblemReporter.report(
|
||||
holder,
|
||||
PyTypeCheckerSuppressionCode.BAD_ARGUMENT_TYPE,
|
||||
expression,
|
||||
@@ -1425,23 +1421,24 @@ open class PyTypeCheckerInspection : PyInspection() {
|
||||
argument: PyExpression?,
|
||||
substitutions: GenericSubstitutions,
|
||||
): Boolean {
|
||||
var argument = argument
|
||||
argument = peelArgument(argument)
|
||||
val peeledArgument = peelArgument(argument)
|
||||
val expression = when (argument) {
|
||||
is PyStarArgument -> peelArgument(argument.childOfType<PyExpression>())
|
||||
else -> peeledArgument
|
||||
}
|
||||
|
||||
if (argument != null) {
|
||||
if (isDictExpression(argument, myTypeEvalContext) &&
|
||||
parameterType is PyTypedDictType
|
||||
) {
|
||||
reportTypedDictProblems(parameterType, argument)
|
||||
if (expression != null) {
|
||||
if (isDictExpression(expression, myTypeEvalContext) && parameterType is PyTypedDictType) {
|
||||
reportTypedDictProblems(parameterType, expression)
|
||||
return true
|
||||
}
|
||||
else if (parameterType is PyUnpackedTypedDictType) {
|
||||
reportUnpackedTypedDictProblems(parameterType, argument)
|
||||
if (parameterType is PyUnpackedTypedDictType) {
|
||||
reportUnpackedTypedDictProblems(parameterType, expression)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return matchesExpectedType(parameterType, argumentType, argument, substitutions)
|
||||
return matchesExpectedType(parameterType, argumentType, peeledArgument, substitutions)
|
||||
&& !matchingProtocolDefinitions(parameterType, argumentType, myTypeEvalContext)
|
||||
}
|
||||
|
||||
@@ -1516,6 +1513,22 @@ open class PyTypeCheckerInspection : PyInspection() {
|
||||
}
|
||||
}
|
||||
|
||||
internal class TypedDictProblemReporter {
|
||||
private val reportedKeys = mutableSetOf<Pair<PsiElement, String>>()
|
||||
|
||||
fun report(
|
||||
holder: ProblemsHolder,
|
||||
code: PyTypeCheckerSuppressionCode,
|
||||
element: PsiElement,
|
||||
message: PyInspectionMessages.ProblemMessage,
|
||||
type: ProblemHighlightType = ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
|
||||
) {
|
||||
if (reportedKeys.add(element to message.description)) {
|
||||
PyTypeCheckerProblemReporter.report(holder, code, element, message, type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class AnalyzeCalleeResults(
|
||||
val callableType: PyCallableType,
|
||||
val callable: PyCallable?,
|
||||
|
||||
@@ -914,6 +914,60 @@ class PyTypedDictTypeTest : PyCodeInsightTestCase() {
|
||||
foo(1, "hello", name=42)
|
||||
# ^^^^^^^ WARNING Expected type 'str', got 'Literal[42]' instead
|
||||
""")
|
||||
|
||||
@Test
|
||||
@TestFor(issues = ["PY-90614"])
|
||||
fun `dict variable mismatch with Unpack TypedDict kwargs warned once across overloads`() = test("""
|
||||
from typing import TypedDict, Unpack, overload
|
||||
|
||||
class EmptyKwargs(TypedDict):
|
||||
pass
|
||||
|
||||
@overload
|
||||
def foo(*, a: int, **kwargs: Unpack[EmptyKwargs]) -> None: ...
|
||||
@overload
|
||||
def foo(*, b: str, **kwargs: Unpack[EmptyKwargs]) -> None: ...
|
||||
def foo(**kwargs) -> None: ...
|
||||
|
||||
dict_var = {}
|
||||
foo(**dict_var)
|
||||
# ^^^^^^^^ WARNING Expected type 'EmptyKwargs', got 'dict[Unknown, Unknown]' instead
|
||||
""")
|
||||
|
||||
@Test
|
||||
@TestFor(issues = ["PY-90614"])
|
||||
fun `unpacked parenthesized dict literal keys are checked`() = test("""
|
||||
from typing import TypedDict, Unpack
|
||||
|
||||
class FieldKwargs(TypedDict):
|
||||
description: str
|
||||
|
||||
def foo(**kwargs: Unpack[FieldKwargs]) -> None: ...
|
||||
|
||||
foo(**((({"description": "foo"}))))
|
||||
foo(**((({"description": "foo", "unknown": "foo"}))))
|
||||
# ^^^^^^^^^^^^^^^^ WARNING Extra key 'unknown' for TypedDict 'FieldKwargs'
|
||||
""")
|
||||
|
||||
@Test
|
||||
@TestFor(issues = ["PY-91511"])
|
||||
fun `unpacked dict literal keys checked against the overload with matching parameter`() = test("""
|
||||
from typing import TypedDict, Unpack, overload
|
||||
|
||||
class EmptyKwargs(TypedDict):
|
||||
pass
|
||||
|
||||
@overload
|
||||
def foo(*, description: str) -> None: ...
|
||||
@overload
|
||||
def foo(**kwargs: Unpack[EmptyKwargs]) -> None: ...
|
||||
def foo(**kwargs) -> None: ...
|
||||
|
||||
foo(**{"unknown": "foo"})
|
||||
# ^^^^^^^^^^^^^^^^ WARNING Extra key 'unknown' for TypedDict 'EmptyKwargs'
|
||||
foo(**{"description": "foo"})
|
||||
# ^^^^^^^^^^^^^^^^^^^^ WARNING Extra key 'description' for TypedDict 'EmptyKwargs' FIXME
|
||||
""")
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user