PY-25263 Attribute initialization via property set implemented

This commit is contained in:
Anton Bragin
2018-04-11 11:13:42 +03:00
parent c80bb01339
commit 4aeb385d94
5 changed files with 107 additions and 19 deletions
@@ -21,6 +21,7 @@ import com.intellij.psi.PsiElementVisitor;
import com.intellij.util.ThreeState;
import com.jetbrains.python.PyBundle;
import com.jetbrains.python.PyNames;
import com.jetbrains.python.codeInsight.controlflow.ControlFlowCache;
import com.jetbrains.python.inspections.quickfix.PyMoveAttributeToInitQuickFix;
import com.jetbrains.python.psi.Property;
import com.jetbrains.python.psi.PyClass;
@@ -29,10 +30,12 @@ import com.jetbrains.python.psi.PyTargetExpression;
import com.jetbrains.python.psi.impl.PyClassImpl;
import com.jetbrains.python.psi.types.TypeEvalContext;
import com.jetbrains.python.testing.PythonUnitTestUtil;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -40,8 +43,7 @@ import java.util.Map;
/**
* User: ktisha
*
* Inspection to detect situations, where instance attribute
* defined outside __init__ function
* Inspection to detect situations, where instance attribute is defined outside __init__ function.
*/
public class PyAttributeOutsideInitInspection extends PyInspection {
@Nls
@@ -74,46 +76,84 @@ public class PyAttributeOutsideInitInspection extends PyInspection {
if (!isApplicable(containingClass, myTypeEvalContext)) {
return;
}
final PyFunction.Modifier modifier = node.getModifier();
if (modifier != null) return;
final List<PyTargetExpression> classAttributes = containingClass.getClassAttributes();
Map<String, PyTargetExpression> attributesInInit = new HashMap<>();
for (PyTargetExpression classAttr : classAttributes) {
attributesInInit.put(classAttr.getName(), classAttr);
if (node.getModifier() != null) {
return;
}
final List<PyTargetExpression> classAttributes = containingClass.getClassAttributes();
final Map<String, Property> properties = containingClass.getProperties();
final Map<String, PyTargetExpression> attributesInInit = new HashMap<>();
StreamEx.of(classAttributes)
.filter(attribute -> !properties.containsKey(attribute.getName()))
.forEach(attribute -> attributesInInit.put(attribute.getName(), attribute));
final PyFunction initMethod = containingClass.findMethodByName(PyNames.INIT, false, null);
if (initMethod != null) {
PyClassImpl.collectInstanceAttributes(initMethod, attributesInInit);
}
for (PyClass superClass : containingClass.getAncestorClasses(myTypeEvalContext)) {
final PyFunction superInit = superClass.findMethodByName(PyNames.INIT, false, null);
if (superInit != null)
if (superInit != null) {
PyClassImpl.collectInstanceAttributes(superInit, attributesInInit);
}
for (PyTargetExpression classAttr : superClass.getClassAttributes()) {
attributesInInit.put(classAttr.getName(), classAttr);
}
}
Map<String, PyTargetExpression> attributes = new HashMap<>();
final Map<String, PyTargetExpression> attributes = new HashMap<>();
PyClassImpl.collectInstanceAttributes(node, attributes);
for (Map.Entry<String, PyTargetExpression> attribute : attributes.entrySet()) {
String attributeName = attribute.getKey();
for (PyTargetExpression attribute : attributes.values()) {
final String attributeName = attribute.getName();
if (attributeName == null) continue;
final Property property = containingClass.findProperty(attributeName, true, null);
if (!attributesInInit.containsKey(attributeName) && property == null) {
registerProblem(attribute.getValue(), PyBundle.message("INSP.attribute.$0.outside.init", attributeName),
if (!attributesInInit.containsKey(attributeName) &&
!isDefinedByProperty(attribute, properties.values(), attributesInInit)) {
registerProblem(attribute, PyBundle.message("INSP.attribute.$0.outside.init", attributeName),
new PyMoveAttributeToInitQuickFix());
}
}
}
}
private static boolean isApplicable(@NotNull PyClass containingClass, @NotNull TypeEvalContext context) {
return !PythonUnitTestUtil.isTestClass(containingClass, ThreeState.UNSURE, context) && !containingClass.isSubclass("django.db.models.base.Model", context);
private static boolean isDefinedByProperty(@NotNull PyTargetExpression attribute,
@NotNull Collection<Property> properties,
@NotNull Map<String, PyTargetExpression> attributesInInit) {
return StreamEx.of(properties)
.filter(it -> isSetBy(attribute, it))
.anyMatch(it -> attributesInInit.containsKey(it.getName()));
}
private static boolean isApplicable(@NotNull PyClass containingClass, @NotNull TypeEvalContext context) {
return !PythonUnitTestUtil.isTestClass(containingClass, ThreeState.UNSURE, context) &&
!containingClass.isSubclass("django.db.models.base.Model", context);
}
@Nullable
private static Collection<PyTargetExpression> getSetterTargetExpressions(@NotNull Property property) {
if (!property.getSetter().isDefined() || property.getSetter().value() == null) {
return null;
}
final PyFunction setter = property.getSetter().value().asMethod();
if (setter == null) {
return null;
}
return ControlFlowCache.getScope(setter).getTargetExpressions();
}
/**
* Check whether the {@code property} sets the {@code attribute} provided.
*/
private static boolean isSetBy(@NotNull PyTargetExpression attribute, @NotNull Property property) {
final Collection<PyTargetExpression> propertyTargetExpressions = getSetterTargetExpressions(property);
return propertyTargetExpressions != null &&
attribute.getName() != null &&
StreamEx.of(propertyTargetExpressions)
.map(targetExpression -> targetExpression.getName())
.nonNull()
.anyMatch(name -> name.equals(attribute.getName()));
}
}
@@ -0,0 +1,11 @@
class C(object):
def __init__(self, value):
self.x = value
def getx(self):
return self._x
def setx(self, value):
self._x = value # False positive for self._x
x = property(getx, setx, doc="The 'x' property.")
@@ -0,0 +1,11 @@
class C(object):
def __init__(self):
self.x = None
@property
def x(self):
return self._x
@x.setter
def x(self, value):
self._x = value # False positive for self._x
@@ -0,0 +1,11 @@
class C(object):
def __init__(self, value):
pass
def getx(self):
return self._x
def setx(self, value):
<weak_warning descr="Instance attribute _x defined outside __init__">self._x</weak_warning> = value
x = property(getx, setx, doc="The 'x' property.")
@@ -70,6 +70,21 @@ public class PyAttributeOutsideInitInspectionTest extends PyInspectionTestCase {
doTest();
}
// PY-25263
public void testProperty() {
doTest();
}
// PY-25263
public void testPropertyAnnotation() {
doTest();
}
// PY-25263
public void testPropertyNotSetInInit() {
doTest();
}
@NotNull
@Override
protected Class<? extends PyInspection> getInspectionClass() {