mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-04-18 20:41:22 +07:00
Introduce PyClassType.attributeIsWritable to check if member could be created or updated (PY-27866)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}.
|
||||
*/
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
Reference in New Issue
Block a user