mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
Fixed type checking arguments against generic keyword container (PY-22513)
This commit is contained in:
@@ -28,7 +28,6 @@ import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider;
|
||||
import com.jetbrains.python.documentation.PythonDocumentationProvider;
|
||||
import com.jetbrains.python.inspections.quickfix.PyMakeFunctionReturnTypeQuickFix;
|
||||
import com.jetbrains.python.psi.*;
|
||||
import com.jetbrains.python.psi.impl.PyCallExpressionHelper;
|
||||
import com.jetbrains.python.psi.types.*;
|
||||
import one.util.streamex.StreamEx;
|
||||
import org.jetbrains.annotations.Nls;
|
||||
@@ -39,6 +38,7 @@ import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.jetbrains.python.psi.PyUtil.as;
|
||||
import static com.jetbrains.python.psi.impl.PyCallExpressionHelper.*;
|
||||
|
||||
/**
|
||||
* @author vlan
|
||||
@@ -206,7 +206,7 @@ public class PyTypeCheckerInspection extends PyInspection {
|
||||
}
|
||||
|
||||
private static boolean callDoesNotHaveUnmappedArgumentsAndUnfilledParameters(@NotNull PyTypeChecker.AnalyzeCallResults callResults) {
|
||||
final PyCallExpressionHelper.ArgumentMappingResults mapping = callResults.getMapping();
|
||||
final ArgumentMappingResults mapping = callResults.getMapping();
|
||||
return mapping.getUnmappedArguments().isEmpty() && mapping.getUnmappedParameters().isEmpty();
|
||||
}
|
||||
|
||||
@@ -214,15 +214,57 @@ public class PyTypeCheckerInspection extends PyInspection {
|
||||
private AnalyzeCalleeResults analyzeCallee(@NotNull PyTypeChecker.AnalyzeCallResults results) {
|
||||
final List<AnalyzeArgumentResult> result = new ArrayList<>();
|
||||
final Map<PyGenericType, PyType> substitutions = PyTypeChecker.unifyReceiver(results.getReceiver(), myTypeEvalContext);
|
||||
|
||||
for (Map.Entry<PyExpression, PyNamedParameter> entry : results.getMapping().getMappedParameters().entrySet()) {
|
||||
final AnalyzeArgumentResult argumentResult = analyzeArgument(entry.getValue(), entry.getKey(), substitutions);
|
||||
result.add(argumentResult);
|
||||
final Map<PyExpression, PyNamedParameter> mapping = results.getMapping().getMappedParameters();
|
||||
for (Map.Entry<PyExpression, PyNamedParameter> entry : getRegularMappedParameters(mapping).entrySet()) {
|
||||
final PyExpression argument = entry.getKey();
|
||||
final PyNamedParameter parameter = entry.getValue();
|
||||
final PyType expected = parameter.getArgumentType(myTypeEvalContext);
|
||||
final PyType actual = myTypeEvalContext.getType(argument);
|
||||
final boolean matched = PyTypeChecker.match(expected, actual, myTypeEvalContext, substitutions);
|
||||
result.add(new AnalyzeArgumentResult(argument, expected, substituteGenerics(expected, substitutions), actual, matched));
|
||||
}
|
||||
final PyNamedParameter positionalContainer = getMappedPositionalContainer(mapping);
|
||||
if (positionalContainer != null) {
|
||||
result.addAll(analyzeContainerMapping(positionalContainer, getArgumentsMappedToPositionalContainer(mapping), substitutions));
|
||||
}
|
||||
final PyNamedParameter keywordContainer = getMappedKeywordContainer(mapping);
|
||||
if (keywordContainer != null) {
|
||||
result.addAll(analyzeContainerMapping(keywordContainer, getArgumentsMappedToKeywordContainer(mapping), substitutions));
|
||||
}
|
||||
|
||||
return new AnalyzeCalleeResults(results.getCallable(), result);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private List<AnalyzeArgumentResult> analyzeContainerMapping(@NotNull PyNamedParameter container, @NotNull List<PyExpression> arguments,
|
||||
@NotNull Map<PyGenericType, PyType> substitutions) {
|
||||
final PyType expected = container.getArgumentType(myTypeEvalContext);
|
||||
final PyType expectedWithSubstitutions = substituteGenerics(expected, substitutions);
|
||||
// For an expected type with generics we have to match all the actual types against it in order to do proper generic unification
|
||||
if (PyTypeChecker.hasGenerics(expected, myTypeEvalContext)) {
|
||||
final PyType actual = PyUnionType.union(arguments.stream().map(e -> myTypeEvalContext.getType(e)).collect(Collectors.toList()));
|
||||
final boolean matched = PyTypeChecker.match(expected, actual, myTypeEvalContext, substitutions);
|
||||
return arguments.stream()
|
||||
.map(argument -> new AnalyzeArgumentResult(argument, expected, expectedWithSubstitutions, actual, matched))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
else {
|
||||
return arguments.stream()
|
||||
.map(argument -> {
|
||||
final PyType actual = myTypeEvalContext.getType(argument);
|
||||
final boolean matched = PyTypeChecker.match(expected, actual, myTypeEvalContext, substitutions);
|
||||
return new AnalyzeArgumentResult(argument, expected, expectedWithSubstitutions, actual, matched);
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private PyType substituteGenerics(@Nullable PyType expectedArgumentType, @NotNull Map<PyGenericType, PyType> substitutions) {
|
||||
return PyTypeChecker.hasGenerics(expectedArgumentType, myTypeEvalContext)
|
||||
? PyTypeChecker.substitute(expectedArgumentType, substitutions, myTypeEvalContext)
|
||||
: null;
|
||||
}
|
||||
|
||||
private static boolean matchedCalleeResultsExist(@NotNull List<AnalyzeCalleeResults> calleesResults) {
|
||||
return calleesResults
|
||||
.stream()
|
||||
@@ -240,19 +282,6 @@ public class PyTypeCheckerInspection extends PyInspection {
|
||||
.map(AnalyzeArgumentResult::getActualType)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private AnalyzeArgumentResult analyzeArgument(@NotNull PyNamedParameter parameter,
|
||||
@NotNull PyExpression argument,
|
||||
@NotNull Map<PyGenericType, PyType> substitutions) {
|
||||
final PyType expectedArgumentType = parameter.getArgumentType(myTypeEvalContext);
|
||||
final PyType actualArgumentType = myTypeEvalContext.getType(argument);
|
||||
final PyType expectedTypeAfterSubstitution = PyTypeChecker.hasGenerics(expectedArgumentType, myTypeEvalContext)
|
||||
? PyTypeChecker.substitute(expectedArgumentType, substitutions, myTypeEvalContext)
|
||||
: null;
|
||||
final boolean isMatched = PyTypeChecker.match(expectedArgumentType, actualArgumentType, myTypeEvalContext, substitutions);
|
||||
return new AnalyzeArgumentResult(argument, expectedArgumentType, expectedTypeAfterSubstitution, actualArgumentType, isMatched);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
from typing import Any, TypeVar
|
||||
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
def generic_kwargs(**kwargs: T) -> None:
|
||||
pass
|
||||
|
||||
|
||||
generic_kwargs(a=1, b='foo')
|
||||
@@ -519,6 +519,19 @@ public class Py3TypeTest extends PyTestCase {
|
||||
" print(expr)");
|
||||
}
|
||||
|
||||
// PY-22513
|
||||
public void testGenericKwargs() {
|
||||
doTest("Dict[str, Union[int, str]]",
|
||||
"from typing import Any, Dict, TypeVar\n" +
|
||||
"\n" +
|
||||
"T = TypeVar('T')\n" +
|
||||
"\n" +
|
||||
"def generic_kwargs(**kwargs: T) -> Dict[str, T]:\n" +
|
||||
" pass\n" +
|
||||
"\n" +
|
||||
"expr = generic_kwargs(a=1, b='foo')\n");
|
||||
}
|
||||
|
||||
private void doTest(final String expectedType, final String text) {
|
||||
myFixture.configureByText(PythonFileType.INSTANCE, text);
|
||||
final PyExpression expr = myFixture.findElementByText("expr", PyExpression.class);
|
||||
|
||||
@@ -228,4 +228,9 @@ public class Py3TypeCheckerInspectionTest extends PyTestCase {
|
||||
public void testUnboundTypeVarsMatchClassObjectTypes() {
|
||||
doTest();
|
||||
}
|
||||
|
||||
// PY-22513
|
||||
public void testGenericKwargs() {
|
||||
doTest();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user