PY-18096 Fixed: False positive "Type doesn't have expected attributes" for namedtuple

Introduce PyClassLikeType.getMemberNames(boolean, TypeEvalContext). This method returns all members including dynamically ones (e.g. fields of namedtuple)
This commit is contained in:
Semyon Proshev
2016-02-25 12:36:33 +03:00
parent 646e78521d
commit 8d78d16800
9 changed files with 156 additions and 82 deletions

View File

@@ -31,9 +31,7 @@ import com.jetbrains.python.psi.types.TypeEvalContext;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.*;
/**
* @author yole
@@ -49,7 +47,7 @@ public class PyJavaClassType implements PyClassLikeType {
@Nullable
public List<? extends RatedResolveResult> resolveMember(@NotNull final String name,
PyExpression location,
@Nullable PyExpression location,
@NotNull AccessDirection direction,
@NotNull PyResolveContext resolveContext) {
return resolveMember(name, location, direction, resolveContext, true);
@@ -156,6 +154,30 @@ public class PyJavaClassType implements PyClassLikeType {
// TODO: Implement
}
@NotNull
@Override
public Set<String> getMemberNames(boolean inherited, @NotNull TypeEvalContext context) {
final Set<String> result = new LinkedHashSet<>();
for (PsiMethod method : myClass.getAllMethods()) {
result.add(method.getName());
}
for (PsiField field : myClass.getAllFields()) {
result.add(field.getName());
}
if (inherited) {
for (PyClassLikeType type : getAncestorTypes(context)) {
if (type != null) {
result.addAll(type.getMemberNames(false, context));
}
}
}
return result;
}
@NotNull
@Override
public List<PyClassLikeType> getAncestorTypes(@NotNull final TypeEvalContext context) {

View File

@@ -26,6 +26,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Set;
/**
* @author vlan
@@ -42,8 +43,10 @@ public interface PyClassLikeType extends PyCallableType, PyWithAncestors {
List<PyClassLikeType> getSuperClassTypes(@NotNull TypeEvalContext context);
@Nullable
List<? extends RatedResolveResult> resolveMember(@NotNull final String name, @Nullable PyExpression location,
@NotNull AccessDirection direction, @NotNull PyResolveContext resolveContext,
List<? extends RatedResolveResult> resolveMember(@NotNull final String name,
@Nullable PyExpression location,
@NotNull AccessDirection direction,
@NotNull PyResolveContext resolveContext,
boolean inherited);
// TODO: Pull to PyType at next iteration
@@ -58,6 +61,9 @@ public interface PyClassLikeType extends PyCallableType, PyWithAncestors {
*/
void visitMembers(@NotNull Processor<PsiElement> processor, boolean inherited, @NotNull TypeEvalContext context);
@NotNull
Set<String> getMemberNames(boolean inherited, @NotNull TypeEvalContext context);
boolean isValid();
@Nullable

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2000-2014 JetBrains s.r.o.
* Copyright 2000-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -112,7 +112,10 @@ public class PyCustomType implements PyClassLikeType {
// Delegate calls to classes, we mimic but filter if filter is set.
for (final PyClassLikeType typeToMimic : myTypesToMimic) {
final List<? extends RatedResolveResult> results = typeToMimic.toInstance().resolveMember(name, location, direction, resolveContext, inherited);
final List<? extends RatedResolveResult> results = typeToMimic.toInstance().resolveMember(
name, location, direction, resolveContext, inherited
);
if (results != null) {
globalResult.addAll(Collections2.filter(results, new ResolveFilter()));
}
@@ -253,7 +256,9 @@ public class PyCustomType implements PyClassLikeType {
}
@Override
public final void visitMembers(@NotNull final Processor<PsiElement> processor, final boolean inherited, @NotNull final TypeEvalContext context) {
public final void visitMembers(@NotNull final Processor<PsiElement> processor,
final boolean inherited,
@NotNull final TypeEvalContext context) {
for (final PyClassLikeType type : myTypesToMimic) {
// Only visit methods that are allowed by filter (if any)
type.visitMembers(new Processor<PsiElement>() {
@@ -271,6 +276,18 @@ public class PyCustomType implements PyClassLikeType {
}
}
@NotNull
@Override
public Set<String> getMemberNames(boolean inherited, @NotNull TypeEvalContext context) {
final Set<String> result = new LinkedHashSet<>();
for (PyClassLikeType type : myTypesToMimic) {
result.addAll(type.getMemberNames(inherited, context));
}
return result;
}
/**
* Predicate that filters completion using {@link #myFilter}
*/

View File

@@ -35,6 +35,7 @@ import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
/**
* @author yole
@@ -69,7 +70,7 @@ public class PyNamedTupleType extends PyClassTypeImpl implements PyCallableType
return classMembers;
}
if (myFields.contains(name)) {
return Collections.singletonList(new RatedResolveResult(1000, new PyElementImpl(myDeclaration.getNode())));
return Collections.singletonList(new RatedResolveResult(RatedResolveResult.RATE_HIGH, new PyElementImpl(myDeclaration.getNode())));
}
return null;
}
@@ -98,7 +99,7 @@ public class PyNamedTupleType extends PyClassTypeImpl implements PyCallableType
@Override
public PyType getCallType(@NotNull TypeEvalContext context, @NotNull PyCallSiteExpression callSite) {
if (myDefinitionLevel > 0) {
return new PyNamedTupleType(myClass, myDeclaration, myName, myFields, myDefinitionLevel-1);
return new PyNamedTupleType(myClass, myDeclaration, myName, myFields, myDefinitionLevel - 1);
}
return null;
}
@@ -113,6 +114,15 @@ public class PyNamedTupleType extends PyClassTypeImpl implements PyCallableType
return "PyNamedTupleType: " + myName;
}
@NotNull
@Override
public Set<String> getMemberNames(boolean inherited, @NotNull TypeEvalContext context) {
final Set<String> result = super.getMemberNames(inherited, context);
result.addAll(myFields);
return result;
}
@Nullable
public static PyType fromCall(@NotNull PyCallExpression call, @NotNull TypeEvalContext context, int level) {
final String name = PyPsiUtils.strValue(call.getArgument(0, PyExpression.class));

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2000-2014 JetBrains s.r.o.
* Copyright 2000-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -46,8 +46,9 @@ public class PyTypeCheckerInspection extends PyInspection {
@NotNull
@Override
public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly, @NotNull LocalInspectionToolSession session) {
if (LOG.isDebugEnabled())
if (LOG.isDebugEnabled()) {
session.putUserData(TIME_KEY, System.nanoTime());
}
return new Visitor(holder, session);
}
@@ -88,18 +89,22 @@ public class PyTypeCheckerInspection extends PyInspection {
private void checkCallSite(@Nullable PyCallSiteExpression callSite) {
final List<PyTypeChecker.AnalyzeCallResults> resultsSet = PyTypeChecker.analyzeCallSite(callSite, myTypeEvalContext);
final List<Map<PyExpression, Pair<String, ProblemHighlightType>>> problemsSet = new ArrayList<Map<PyExpression, Pair<String, ProblemHighlightType>>>();
final List<Map<PyExpression, Pair<String, ProblemHighlightType>>> problemsSet =
new ArrayList<Map<PyExpression, Pair<String, ProblemHighlightType>>>();
for (PyTypeChecker.AnalyzeCallResults results : resultsSet) {
problemsSet.add(checkMapping(results.getReceiver(), results.getArguments()));
}
if (!problemsSet.isEmpty()) {
Map<PyExpression, Pair<String, ProblemHighlightType>> minProblems = Collections.min(problemsSet, new Comparator<Map<PyExpression, Pair<String, ProblemHighlightType>>>() {
@Override
public int compare(Map<PyExpression, Pair<String, ProblemHighlightType>> o1,
Map<PyExpression, Pair<String, ProblemHighlightType>> o2) {
return o1.size() - o2.size();
Map<PyExpression, Pair<String, ProblemHighlightType>> minProblems = Collections.min(
problemsSet,
new Comparator<Map<PyExpression, Pair<String, ProblemHighlightType>>>() {
@Override
public int compare(Map<PyExpression, Pair<String, ProblemHighlightType>> o1,
Map<PyExpression, Pair<String, ProblemHighlightType>> o2) {
return o1.size() - o2.size();
}
}
});
);
for (Map.Entry<PyExpression, Pair<String, ProblemHighlightType>> entry : minProblems.entrySet()) {
registerProblem(entry.getKey(), entry.getValue().getFirst(), entry.getValue().getSecond());
}
@@ -109,7 +114,8 @@ public class PyTypeCheckerInspection extends PyInspection {
@NotNull
private Map<PyExpression, Pair<String, ProblemHighlightType>> checkMapping(@Nullable PyExpression receiver,
@NotNull Map<PyExpression, PyNamedParameter> mapping) {
final Map<PyExpression, Pair<String, ProblemHighlightType>> problems = new HashMap<PyExpression, Pair<String, ProblemHighlightType>>();
final Map<PyExpression, Pair<String, ProblemHighlightType>> problems =
new HashMap<PyExpression, Pair<String, ProblemHighlightType>>();
final Map<PyGenericType, PyType> substitutions = new LinkedHashMap<PyGenericType, PyType>();
boolean genericsCollected = false;
for (Map.Entry<PyExpression, PyNamedParameter> entry : mapping.entrySet()) {
@@ -130,7 +136,6 @@ public class PyTypeCheckerInspection extends PyInspection {
final Pair<String, ProblemHighlightType> problem = checkTypes(paramType, argType, myTypeEvalContext, substitutions);
if (problem != null) {
problems.put(arg, problem);
}
}
return problems;
@@ -151,13 +156,13 @@ public class PyTypeCheckerInspection extends PyInspection {
final PyType substitute = PyTypeChecker.substitute(expected, substitutions, context);
if (substitute != null) {
quotedExpectedName = String.format("'%s' (matched generic type '%s')",
PythonDocumentationProvider.getTypeName(substitute, context),
expectedName);
PythonDocumentationProvider.getTypeName(substitute, context),
expectedName);
highlightType = ProblemHighlightType.WEAK_WARNING;
}
}
final String actualName = PythonDocumentationProvider.getTypeName(actual, context);
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<String> expectedAttributes = ((PyStructuralType)expected).getAttributeNames();
final Set<String> actualAttributes = getAttributes(actual, context);
@@ -190,8 +195,8 @@ public class PyTypeCheckerInspection extends PyInspection {
if (type instanceof PyStructuralType) {
return ((PyStructuralType)type).getAttributeNames();
}
else if (type instanceof PyClassType) {
return PyTypeChecker.getClassTypeAttributes((PyClassType)type, true, context);
else if (type instanceof PyClassLikeType) {
return ((PyClassLikeType)type).getMemberNames(true, context);
}
return null;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2000-2014 JetBrains s.r.o.
* Copyright 2000-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -368,17 +368,17 @@ public class PyClassTypeImpl extends UserDataHolderBase implements PyClassType {
@Nullable
@Override
public PyType getReturnType(@NotNull TypeEvalContext context) {
return getReturnType(context, null);
return getPossibleCallType(context, null);
}
@Nullable
@Override
public PyType getCallType(@NotNull TypeEvalContext context, @NotNull PyCallSiteExpression callSite) {
return getReturnType(context, callSite);
return getPossibleCallType(context, callSite);
}
@Nullable
private PyType getReturnType(@NotNull TypeEvalContext context, @Nullable PyCallSiteExpression callSite) {
private PyType getPossibleCallType(@NotNull TypeEvalContext context, @Nullable PyCallSiteExpression callSite) {
if (!isDefinition()) {
return PyUtil.getReturnTypeOfMember(this, PyNames.CALL, callSite, context);
}
@@ -523,7 +523,6 @@ public class PyClassTypeImpl extends UserDataHolderBase implements PyClassType {
public void visitMembers(@NotNull final Processor<PsiElement> processor,
final boolean inherited,
@NotNull final TypeEvalContext context) {
myClass.visitMethods(new MyProcessorWrapper<PyFunction>(processor), false, context);
myClass.visitClassAttributes(new MyProcessorWrapper<PyTargetExpression>(processor), false, context);
@@ -541,6 +540,42 @@ public class PyClassTypeImpl extends UserDataHolderBase implements PyClassType {
}
}
@NotNull
@Override
public Set<String> getMemberNames(boolean inherited, @NotNull TypeEvalContext context) {
final Set<String> result = new LinkedHashSet<>();
for (PyFunction function : myClass.getMethods()) {
result.add(function.getName());
}
for (PyTargetExpression expression : myClass.getClassAttributes()) {
result.add(expression.getName());
}
for (PyTargetExpression expression : myClass.getInstanceAttributes()) {
result.add(expression.getName());
}
for (PyClassMembersProvider provider : Extensions.getExtensions(PyClassMembersProvider.EP_NAME)) {
for (PyCustomMember member : provider.getMembers(this, null, context)) {
result.add(member.getName());
}
}
if (inherited) {
for (PyClassLikeType type : getAncestorTypes(context)) {
if (type != null) {
final PyClassLikeType ancestorType = isDefinition() ? type : type.toInstance();
result.addAll(ancestorType.getMemberNames(false, context));
}
}
}
return result;
}
private void addOwnClassMembers(PsiElement expressionHook,
Set<String> namesAlready,
boolean suppressParentheses,

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2000-2014 JetBrains s.r.o.
* Copyright 2000-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,11 +15,10 @@
*/
package com.jetbrains.python.psi.types;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.psi.*;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiNamedElement;
import com.intellij.util.ArrayUtil;
import com.jetbrains.python.PyNames;
import com.jetbrains.python.codeInsight.PyCustomMember;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.impl.PyBuiltinCache;
import com.jetbrains.python.psi.impl.PyCallExpressionHelper;
@@ -46,8 +45,8 @@ public class PyTypeChecker {
* For example int matches object, while str doesn't match int.
* Work for builtin types, classes, tuples etc.
*
* @param expected expected type
* @param actual type to be matched against expected
* @param expected expected type
* @param actual type to be matched against expected
* @param context
* @param substitutions
* @return
@@ -179,11 +178,11 @@ public class PyTypeChecker {
if (overridesGetAttr(actualClassType.getPyClass(), context)) {
return true;
}
final Set<String> actualAttributes = getClassTypeAttributes(actualClassType, true, context);
final Set<String> actualAttributes = actualClassType.getMemberNames(true, context);
return actualAttributes.containsAll(((PyStructuralType)expected).getAttributeNames());
}
if (actual instanceof PyStructuralType && expected instanceof PyClassType) {
final Set<String> expectedAttributes = getClassTypeAttributes((PyClassType)expected, true, context);
final Set<String> expectedAttributes = ((PyClassType)expected).getMemberNames(true, context);
return expectedAttributes.containsAll(((PyStructuralType)actual).getAttributeNames());
}
if (actual instanceof PyCallableType && expected instanceof PyCallableType) {
@@ -212,47 +211,6 @@ public class PyTypeChecker {
return matchNumericTypes(expected, actual);
}
@NotNull
public static Set<String> getClassTypeAttributes(@NotNull PyClassType type, boolean inherited, @NotNull TypeEvalContext context) {
final Set<String> attributes = getClassAttributes(type.getPyClass(), inherited, type.isDefinition(), context);
for (PyClassMembersProvider provider : Extensions.getExtensions(PyClassMembersProvider.EP_NAME)) {
final Collection<PyCustomMember> members = provider.getMembers(type, null, context);
for (PyCustomMember member : members) {
attributes.add(member.getName());
}
}
return attributes;
}
@NotNull
private static Set<String> getClassAttributes(@NotNull PyClass cls,
boolean inherited,
boolean isDefinition,
@NotNull TypeEvalContext context) {
final Set<String> attributes = new HashSet<String>();
for (PyFunction function : cls.getMethods()) {
attributes.add(function.getName());
}
for (PyTargetExpression instanceAttribute : cls.getInstanceAttributes()) {
attributes.add(instanceAttribute.getName());
}
for (PyTargetExpression classAttribute : cls.getClassAttributes()) {
attributes.add(classAttribute.getName());
}
if (inherited) {
for (PyClass ancestor : cls.getAncestorClasses(null)) {
final PyType ancestorType = context.getType(ancestor);
if (ancestorType instanceof PyClassLikeType) {
final PyClassLikeType classType = isDefinition ? (PyClassLikeType)ancestorType : ((PyClassLikeType)ancestorType).toInstance();
if (classType instanceof PyClassType) {
attributes.addAll(getClassTypeAttributes((PyClassType)classType, false, context));
}
}
}
}
return attributes;
}
private static boolean matchNumericTypes(PyType expected, PyType actual) {
final String superName = expected.getName();
final String subName = actual.getName();
@@ -527,7 +485,7 @@ public class PyTypeChecker {
return isUnionCallable((PyUnionType)type);
}
if (type instanceof PyCallableType) {
return ((PyCallableType) type).isCallable();
return ((PyCallableType)type).isCallable();
}
if (type instanceof PyStructuralType && ((PyStructuralType)type).isInferredFromUsages()) {
return true;

View File

@@ -0,0 +1,16 @@
from collections import namedtuple
class C(namedtuple('C', ['foo', 'bar'])):
pass
def f(x):
return x.foo, x.bar
def g():
x = C(foo=0, bar=1)
return f(x)
print(g())

View File

@@ -151,6 +151,11 @@ public class PyTypeCheckerInspectionTest extends PyTestCase {
doTest();
}
// PY-18096
public void testNamedTupleBaseClass() {
doTest();
}
// PY-6803
public void testPropertyAndFactoryFunction() {
doTest();