Added bounded generic types inference and checking

This commit is contained in:
Andrey Vlasovskikh
2013-06-27 12:47:39 +04:00
parent 8c892b2684
commit 5f51e22ca7
7 changed files with 63 additions and 24 deletions
@@ -93,20 +93,19 @@ public class PyTypeCheckerInspection extends PyInspection {
final String superName = PythonDocumentationProvider.getTypeName(superType, context);
String expected = String.format("'%s'", superName);
final boolean hasGenerics = PyTypeChecker.hasGenerics(superType, context);
ProblemHighlightType highlightType = ProblemHighlightType.GENERIC_ERROR_OR_WARNING;
if (hasGenerics) {
final PyType subst = PyTypeChecker.substitute(superType, substitutions, context);
if (subst != null) {
expected = String.format("'%s' (matched generic type '%s')",
PythonDocumentationProvider.getTypeName(subst, context),
superName);
highlightType = ProblemHighlightType.WEAK_WARNING;
}
}
final String msg = String.format("Expected type %s, got '%s' instead",
expected,
PythonDocumentationProvider.getTypeName(subType, context));
final ProblemHighlightType highlightType = hasGenerics ? ProblemHighlightType.WEAK_WARNING :
ProblemHighlightType.GENERIC_ERROR_OR_WARNING;
registerProblem(node, msg, highlightType);
return msg;
}
@@ -39,7 +39,7 @@ public class PyGenericType implements PyType {
@NotNull
@Override
public String getName() {
return myName;
return myBound != null ? myName + " (" + myBound.getName() + ")" : myName;
}
@Override
@@ -71,7 +71,7 @@ public class PyGenericType implements PyType {
@NotNull
@Override
public String toString() {
return "PyGenericType: " + myName;
return "PyGenericType: " + getName();
}
@Nullable
@@ -44,6 +44,32 @@ public class PyTypeChecker {
private static boolean match(@Nullable PyType expected, @Nullable PyType actual, @NotNull TypeEvalContext context,
@Nullable Map<PyGenericType, PyType> substitutions, boolean recursive) {
// TODO: subscriptable types?, module types?, etc.
if (expected instanceof PyGenericType && substitutions != null) {
final PyGenericType generic = (PyGenericType)expected;
final PyType subst = substitutions.get(generic);
final PyType bound = generic.getBound();
if (!match(bound, actual, context, substitutions, recursive)) {
return false;
}
else if (subst != null) {
if (expected.equals(actual)) {
return true;
}
else if (recursive) {
return match(subst, actual, context, substitutions, false);
}
else {
return false;
}
}
else if (actual != null && !(actual instanceof PyReturnTypeReference)) {
substitutions.put(generic, actual);
}
else if (bound != null) {
substitutions.put(generic, bound);
}
return true;
}
if (expected == null || actual == null) {
return true;
}
@@ -62,25 +88,6 @@ public class PyTypeChecker {
if (actual instanceof PyTypeReference) {
return match(expected, ((PyTypeReference)actual).resolve(null, context), context, substitutions, false);
}
if (expected instanceof PyGenericType && substitutions != null) {
final PyGenericType generic = (PyGenericType)expected;
final PyType subst = substitutions.get(generic);
if (subst != null) {
if (expected.equals(actual)) {
return true;
}
else if (recursive) {
return match(subst, actual, context, substitutions, false);
}
else {
return false;
}
}
else {
substitutions.put(generic, actual);
return true;
}
}
if (isUnknown(actual)) {
return true;
}
@@ -215,4 +215,9 @@ public class PyUnionType implements PyType {
public int hashCode() {
return myMembers.hashCode();
}
@Override
public String toString() {
return "PyUnionType: " + getName();
}
}
@@ -0,0 +1,12 @@
def test():
def f(x):
"""
:type x: T (int or str)
:rtype: T
"""
pass
x = f(10)
y = f('foo')
z = f(<warning descr="Expected type 'T (one of (int, str))', got 'list' instead">[]</warning>)
return x + <warning descr="Expected type 'one of (int, long, float, complex)', got 'str' instead">y</warning>
@@ -572,6 +572,17 @@ public class PyTypeTest extends PyTestCase {
" foo(3)\n");
}
public void testUpperBoundGeneric() {
doTest("int or str",
"def foo(x):\n" +
" '''\n" +
" :type x: T (int or str)\n" +
" :rtype: T\n" +
" '''\n" +
"def bar(x):\n" +
" expr = foo(x)\n");
}
private static TypeEvalContext getTypeEvalContext(@NotNull PyExpression element) {
return TypeEvalContext.userInitiated(element.getContainingFile()).withTracing();
}
@@ -594,6 +605,7 @@ public class PyTypeTest extends PyTestCase {
PyType expected = PyTypeParser.getTypeByName(expr, expectedType);
if (expected != null) {
assertNotNull(context.printTrace(), actual);
assertFalse(context.printTrace(), actual instanceof PyReturnTypeReference);
assertTrue(msg(expected, actual, context), PyTypeChecker.match(expected, actual, context));
}
}
@@ -180,4 +180,8 @@ public class PyTypeCheckerInspectionTest extends PyTestCase {
public void testFieldWithNoneInStub() {
doMultiFileTest();
}
public void testBoundedGeneric() {
doTest();
}
}