Introduce PyClassType.attributeIsWritable to check if member could be created or updated (PY-27866)

This commit is contained in:
Semyon Proshev
2018-03-30 17:38:49 +03:00
parent 54898c22c6
commit bdca4ac85f
6 changed files with 89 additions and 98 deletions

View File

@@ -25,4 +25,14 @@ import org.jetbrains.annotations.NotNull;
public interface PyClassType extends PyClassLikeType, UserDataHolder {
@NotNull
PyClass getPyClass();
/**
* @param name name to check
* @param context type evaluation context
* @return true if attribute with the specified name could be created or updated.
* @see PyClass#getSlots(TypeEvalContext)
*/
default boolean isAttributeWritable(@NotNull String name, @NotNull TypeEvalContext context) {
return true;
}
}

View File

@@ -1,7 +1,6 @@
// Copyright 2000-2017 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.jetbrains.python.inspections
import com.google.common.collect.Iterables
import com.intellij.codeInspection.LocalInspectionToolSession
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.psi.PsiElementVisitor
@@ -70,65 +69,10 @@ class PyDunderSlotsInspection : PyInspection() {
}
val qualifierType = myTypeEvalContext.getType(qualifier)
if (qualifierType is PyClassType && !qualifierType.isDefinition) {
val qualifierClass = qualifierType.pyClass
if (!attributeIsWritable(qualifierClass, targetName)) {
registerProblem(target, "'${qualifierClass.name}' object attribute '$targetName' is read-only")
}
if (qualifierType is PyClassType && !qualifierType.isAttributeWritable(targetName, myTypeEvalContext)) {
registerProblem(target, "'${qualifierType.name}' object attribute '$targetName' is read-only")
}
}
private fun attributeIsWritable(cls: PyClass, name: String): Boolean {
/*
The only difference between Py2 and Py3+ is that the following case is not highlighted in Py3+:
class A:
attr = "attr"
__slots__ = ("attr")
A().attr
Py3+ raises ValueError about conflict between __slots__ and class variable.
This case is handled above by com.jetbrains.python.inspections.PyDunderSlotsInspection.Visitor.processSlot method.
*/
if (LanguageLevel.forElement(cls).isPython2) {
return attributeIsWritableInPy2(cls, name)
}
else {
return attributeIsWritableInPy3(cls, name)
}
}
private fun attributeIsWritableInPy2(cls: PyClass, name: String): Boolean {
val slots = cls.getSlots(myTypeEvalContext)
return slots == null ||
slots.contains(name) && cls.findClassAttribute(name, true, myTypeEvalContext) == null ||
cls.findProperty(name, true, myTypeEvalContext) != null
}
private fun attributeIsWritableInPy3(cls: PyClass, name: String): Boolean {
var classAttrIsFound = false
var slotIsFound = false
for (c in Iterables.concat(listOf(cls), cls.getAncestorClasses(myTypeEvalContext))) {
if (PyUtil.isObjectClass(c)) continue
val ownSlots = PyUtil.deactivateSlots(c, c.ownSlots, myTypeEvalContext)
if (ownSlots == null || c.findProperty(name, false, myTypeEvalContext) != null) return true
if (!classAttrIsFound) {
classAttrIsFound = c.findClassAttribute(name, false, myTypeEvalContext) != null
if (ownSlots.contains(name)) {
if (classAttrIsFound) return true
slotIsFound = true
}
}
}
return slotIsFound && !classAttrIsFound
}
}
}

View File

@@ -128,9 +128,9 @@ public class PyUnresolvedReferencesInspection extends PyInspection {
public static class Visitor extends PyInspectionVisitor {
private final Set<PyImportedNameDefiner> myImportsInsideGuard = Collections.synchronizedSet(new HashSet<PyImportedNameDefiner>());
private final Set<PyImportedNameDefiner> myUsedImports = Collections.synchronizedSet(new HashSet<PyImportedNameDefiner>());
private final Set<PyImportedNameDefiner> myAllImports = Collections.synchronizedSet(new HashSet<PyImportedNameDefiner>());
private final Set<PyImportedNameDefiner> myImportsInsideGuard = Collections.synchronizedSet(new HashSet<>());
private final Set<PyImportedNameDefiner> myUsedImports = Collections.synchronizedSet(new HashSet<>());
private final Set<PyImportedNameDefiner> myAllImports = Collections.synchronizedSet(new HashSet<>());
private final ImmutableSet<String> myIgnoredIdentifiers;
private volatile Boolean myIsEnabled = null;
@@ -162,40 +162,17 @@ public class PyUnresolvedReferencesInspection extends PyInspection {
private void checkSlotsAndProperties(PyQualifiedExpression node) {
final PyExpression qualifier = node.getQualifier();
if (qualifier != null) {
final String attrName = node.getReferencedName();
if (qualifier != null && attrName != null) {
final PyType type = myTypeEvalContext.getType(qualifier);
if (type instanceof PyClassType) {
final PyClass pyClass = ((PyClassType)type).getPyClass();
final String attrName = node.getReferencedName();
if (attrName != null && !canHaveAttribute(pyClass, attrName)) {
for (PyClass ancestor : pyClass.getAncestorClasses(myTypeEvalContext)) {
if (ancestor == null) {
return;
}
if (PyUtil.isObjectClass(ancestor)) {
break;
}
if (canHaveAttribute(ancestor, attrName)) {
return;
}
}
final ASTNode nameNode = node.getNameElement();
final PsiElement e = nameNode != null ? nameNode.getPsi() : node;
registerProblem(e, "'" + pyClass.getName() + "' object has no attribute '" + attrName + "'");
}
if (type instanceof PyClassType && !((PyClassType)type).isAttributeWritable(attrName, myTypeEvalContext)) {
final ASTNode nameNode = node.getNameElement();
final PsiElement e = nameNode != null ? nameNode.getPsi() : node;
registerProblem(e, "'" + type.getName() + "' object has no attribute '" + attrName + "'");
}
}
}
private boolean canHaveAttribute(@NotNull PyClass cls, @NotNull String attrName) {
final List<String> slots = PyUtil.deactivateSlots(cls, cls.getOwnSlots(), myTypeEvalContext);
return slots == null ||
slots.contains(attrName) ||
cls.findClassAttribute(attrName, false, myTypeEvalContext) != null ||
cls.findProperty(attrName, false, myTypeEvalContext) != null;
}
@Override
public void visitPyImportElement(PyImportElement node) {
super.visitPyImportElement(node);

View File

@@ -1905,13 +1905,6 @@ public class PyUtil {
}
}
@Nullable
public static List<String> deactivateSlots(@NotNull PyClass cls, @Nullable List<String> slots, @NotNull TypeEvalContext context) {
if (!cls.isNewStyleClass(context)) return null;
if (slots == null || slots.contains(PyNames.DICT)) return null;
return slots;
}
/**
* This helper class allows to collect various information about AST nodes composing {@link PyStringLiteralExpression}.
*/

View File

@@ -1,6 +1,7 @@
// Copyright 2000-2017 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.jetbrains.python.psi.types;
import com.google.common.collect.Iterables;
import com.intellij.codeInsight.completion.CompletionUtil;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.codeInsight.lookup.LookupElementBuilder;
@@ -862,6 +863,72 @@ public class PyClassTypeImpl extends UserDataHolderBase implements PyClassType {
return myClass.isValid();
}
@Override
public boolean isAttributeWritable(@NotNull String name, @NotNull TypeEvalContext context) {
if (isDefinition() || PyUtil.isObjectClass(getPyClass())) return true;
/*
The only difference between Py2 and Py3+ is that the following case is considered as writable in Py3+:
class A:
attr = "attr"
__slots__ = ("attr")
A().attr
Py3+ raises ValueError about conflict between __slots__ and class variable.
This case is handled in com.jetbrains.python.inspections.PyDunderSlotsInspection.Visitor.processSlot.
*/
if (LanguageLevel.forElement(getPyClass()).isPython2()) {
return attributeIsWritableInPy2(name, context);
}
else {
return attributeIsWritableInPy3(name, context);
}
}
private boolean attributeIsWritableInPy2(@NotNull String name, @NotNull TypeEvalContext context) {
final List<String> slots = getPyClass().getSlots(context);
return slots == null ||
slots.contains(name) && getPyClass().findClassAttribute(name, true, context) == null ||
getPyClass().findProperty(name, true, context) != null;
}
private boolean attributeIsWritableInPy3(@NotNull String name, @NotNull TypeEvalContext context) {
boolean classAttrIsFound = false;
boolean slotIsFound = false;
for (PyClassLikeType type : Iterables.concat(Collections.singletonList(this), getAncestorTypes(context))) {
if (!(type instanceof PyClassType)) return true;
final PyClass cls = ((PyClassType)type).getPyClass();
if (PyUtil.isObjectClass(cls)) {
continue;
}
if (!cls.isNewStyleClass(context)) return true;
final List<String> ownSlots = cls.getOwnSlots();
if (ownSlots == null || ownSlots.contains(PyNames.DICT)) {
return true;
}
if (cls.findProperty(name, false, context) != null) {
return true;
}
if (!classAttrIsFound) {
classAttrIsFound = cls.findClassAttribute(name, false, context) != null;
if (ownSlots.contains(name)) {
if (classAttrIsFound) return true;
slotIsFound = true;
}
}
}
return slotIsFound && !classAttrIsFound;
}
@Nullable
public static PyClassTypeImpl createTypeByQName(@NotNull final PsiElement anchor,
@NotNull final String classQualifiedName,

View File

@@ -3,7 +3,7 @@ class Singleton(object):
data = {}
def foo(self):
self.data = {'a': 1}
self.<warning descr="'Singleton' object has no attribute 'data'">data</warning> = {'a': 1}
Singleton.data = {'a': 1}
Singleton().__class__.data = {'a': 1}