diff --git a/python/src/com/jetbrains/python/inspections/PyTypeCheckerInspection.java b/python/src/com/jetbrains/python/inspections/PyTypeCheckerInspection.java index 61e473da5c23..50230b05c795 100644 --- a/python/src/com/jetbrains/python/inspections/PyTypeCheckerInspection.java +++ b/python/src/com/jetbrains/python/inspections/PyTypeCheckerInspection.java @@ -15,13 +15,16 @@ */ package com.jetbrains.python.inspections; +import com.google.common.collect.Sets; import com.intellij.codeInspection.LocalInspectionToolSession; import com.intellij.codeInspection.ProblemHighlightType; import com.intellij.codeInspection.ProblemsHolder; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.Key; +import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiElementVisitor; +import com.intellij.util.Function; import com.intellij.util.containers.hash.LinkedHashMap; import com.jetbrains.python.PyNames; import com.jetbrains.python.documentation.PythonDocumentationProvider; @@ -32,6 +35,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Map; +import java.util.Set; /** * @author vlan @@ -126,7 +130,27 @@ public class PyTypeCheckerInspection extends PyInspection { } } final String actualName = PythonDocumentationProvider.getTypeName(actual, context); - final String msg = String.format("Expected type %s, got '%s' instead", quotedExpectedName, actualName); + String msg= String.format("Expected type %s, got '%s' instead", quotedExpectedName, actualName); + if (expected instanceof PyStructuralType) { + final Set expectedAttributes = ((PyStructuralType)expected).getAttributeNames(); + final Set actualAttributes = getAttributes(actual); + if (actualAttributes != null) { + final Sets.SetView missingAttributes = Sets.difference(expectedAttributes, actualAttributes); + if (missingAttributes.size() == 1) { + msg = String.format("Type '%s' doesn't have expected attribute '%s'", actualName, missingAttributes.iterator().next()); + } + else { + msg = String.format("Type '%s' doesn't have expected attributes %s", + actualName, + StringUtil.join(missingAttributes, new Function() { + @Override + public String fun(String s) { + return String.format("'%s'", s); + } + }, ", ")); + } + } + } registerProblem(node, msg, highlightType); return msg; } @@ -135,6 +159,17 @@ public class PyTypeCheckerInspection extends PyInspection { } } + @Nullable + private static Set getAttributes(@NotNull PyType type) { + if (type instanceof PyStructuralType) { + return ((PyStructuralType)type).getAttributeNames(); + } + else if (type instanceof PyClassType) { + return PyTypeChecker.getClassAttributes(((PyClassType)type).getPyClass(), true); + } + return null; + } + @Override public void inspectionFinished(@NotNull LocalInspectionToolSession session, @NotNull ProblemsHolder problemsHolder) { if (LOG.isDebugEnabled()) { diff --git a/python/src/com/jetbrains/python/psi/types/PyTypeChecker.java b/python/src/com/jetbrains/python/psi/types/PyTypeChecker.java index cec92a9514b3..d5c75466ddd0 100644 --- a/python/src/com/jetbrains/python/psi/types/PyTypeChecker.java +++ b/python/src/com/jetbrains/python/psi/types/PyTypeChecker.java @@ -205,7 +205,7 @@ public class PyTypeChecker { } @NotNull - private static Set getClassAttributes(@NotNull PyClass cls, boolean inherited) { + public static Set getClassAttributes(@NotNull PyClass cls, boolean inherited) { final Set attributes = new HashSet(); for (PyFunction function : cls.getMethods(false)) { attributes.add(function.getName()); diff --git a/python/testData/inspections/PyTypeCheckerInspection/ExpectedStructuralType.py b/python/testData/inspections/PyTypeCheckerInspection/ExpectedStructuralType.py index d550559e1f9a..1080906218ab 100644 --- a/python/testData/inspections/PyTypeCheckerInspection/ExpectedStructuralType.py +++ b/python/testData/inspections/PyTypeCheckerInspection/ExpectedStructuralType.py @@ -12,5 +12,5 @@ def test(x): g(x) z = 'foo' - f(z) + f(z) g(z) diff --git a/python/testData/inspections/PyTypeCheckerInspection/GetAttributeAgainstStructuralType.py b/python/testData/inspections/PyTypeCheckerInspection/GetAttributeAgainstStructuralType.py index 52249d9de37f..617df26bcbcf 100644 --- a/python/testData/inspections/PyTypeCheckerInspection/GetAttributeAgainstStructuralType.py +++ b/python/testData/inspections/PyTypeCheckerInspection/GetAttributeAgainstStructuralType.py @@ -18,4 +18,4 @@ class E(object): f(C()) f(D()) -f(E()) +f(E()) diff --git a/python/testData/inspections/PyTypeCheckerInspection/StructuralTypesForNestedCalls.py b/python/testData/inspections/PyTypeCheckerInspection/StructuralTypesForNestedCalls.py index 5e22d50690c3..1e4ae234bbdf 100644 --- a/python/testData/inspections/PyTypeCheckerInspection/StructuralTypesForNestedCalls.py +++ b/python/testData/inspections/PyTypeCheckerInspection/StructuralTypesForNestedCalls.py @@ -7,4 +7,4 @@ def g(x): def test(): - f('string') + f('string')