|
| 1 | +/** |
| 2 | + * @name __eq__ not overridden when adding attributes |
| 3 | + * @description When adding new attributes to instances of a class, equality for that class needs to be defined. |
| 4 | + * @kind problem |
| 5 | + * @tags reliability |
| 6 | + * correctness |
| 7 | + * @problem.severity warning |
| 8 | + * @sub-severity high |
| 9 | + * @precision high |
| 10 | + * @id py/missing-equals |
| 11 | + */ |
| 12 | + |
| 13 | +import python |
| 14 | +import semmle.python.SelfAttribute |
| 15 | +import Equality |
| 16 | + |
| 17 | +predicate class_stores_to_attribute(ClassObject cls, SelfAttributeStore store, string name) { |
| 18 | + exists(FunctionObject f | f = cls.declaredAttribute(_) and store.getScope() = f.getFunction() and store.getName() = name) and |
| 19 | + /* Exclude classes used as metaclasses */ |
| 20 | + not cls.getASuperType() = theTypeType() |
| 21 | +} |
| 22 | + |
| 23 | +predicate should_override_eq(ClassObject cls, Object base_eq) { |
| 24 | + not cls.declaresAttribute("__eq__") and |
| 25 | + exists(ClassObject sup | sup = cls.getABaseType() and sup.declaredAttribute("__eq__") = base_eq | |
| 26 | + not exists(GenericEqMethod eq | eq.getScope() = sup.getPyClass()) and |
| 27 | + not exists(IdentityEqMethod eq | eq.getScope() = sup.getPyClass()) and |
| 28 | + not base_eq.(FunctionObject).getFunction() instanceof IdentityEqMethod and |
| 29 | + not base_eq = theObjectType().declaredAttribute("__eq__") |
| 30 | + ) |
| 31 | +} |
| 32 | + |
| 33 | +/** Does the non-overridden __eq__ method access the attribute, |
| 34 | + * which implies that the __eq__ method does not need to be overridden. |
| 35 | + */ |
| 36 | +predicate superclassEqExpectsAttribute(ClassObject cls, PyFunctionObject base_eq, string attrname) { |
| 37 | + not cls.declaresAttribute("__eq__") and |
| 38 | + exists(ClassObject sup | sup = cls.getABaseType() and sup.declaredAttribute("__eq__") = base_eq | |
| 39 | + exists(SelfAttributeRead store | |
| 40 | + store.getName() = attrname | |
| 41 | + store.getScope() = base_eq.getFunction() |
| 42 | + ) |
| 43 | + ) |
| 44 | +} |
| 45 | + |
| 46 | +from ClassObject cls, SelfAttributeStore store, Object base_eq |
| 47 | +where class_stores_to_attribute(cls, store, _) and should_override_eq(cls, base_eq) and |
| 48 | +/* Don't report overridden unittest.TestCase. -- TestCase overrides __eq__, but subclasses do not really need to. */ |
| 49 | +not cls.getASuperType().getName() = "TestCase" and |
| 50 | +not superclassEqExpectsAttribute(cls, base_eq, store.getName()) |
| 51 | + |
| 52 | +select cls, "The class '" + cls.getName() + "' does not override $@, but adds the new attribute $@.", base_eq, "'__eq__'", store, store.getName() |
0 commit comments