From fccdc30ac544ca6af17c132b69814543102d969f Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Tue, 8 Jul 2025 11:10:38 +0100 Subject: [PATCH 01/15] Modernize incomplete ordering query --- python/ql/src/Classes/IncompleteOrdering.ql | 80 +++++++++------------ 1 file changed, 32 insertions(+), 48 deletions(-) diff --git a/python/ql/src/Classes/IncompleteOrdering.ql b/python/ql/src/Classes/IncompleteOrdering.ql index d6cd1230ece6..bbb6ca5cf6dc 100644 --- a/python/ql/src/Classes/IncompleteOrdering.ql +++ b/python/ql/src/Classes/IncompleteOrdering.ql @@ -2,7 +2,8 @@ * @name Incomplete ordering * @description Class defines one or more ordering method but does not define all 4 ordering comparison methods * @kind problem - * @tags reliability + * @tags quality + * reliability * correctness * @problem.severity warning * @sub-severity low @@ -11,63 +12,46 @@ */ import python +import semmle.python.dataflow.new.internal.DataFlowDispatch +import semmle.python.ApiGraphs -predicate total_ordering(Class cls) { - exists(Attribute a | a = cls.getADecorator() | a.getName() = "total_ordering") - or - exists(Name n | n = cls.getADecorator() | n.getId() = "total_ordering") -} - -string ordering_name(int n) { - result = "__lt__" and n = 1 - or - result = "__le__" and n = 2 - or - result = "__gt__" and n = 3 - or - result = "__ge__" and n = 4 +predicate totalOrdering(Class cls) { + cls.getADecorator() = + API::moduleImport("functools").getMember("total_ordering").asSource().asExpr() } -predicate overrides_ordering_method(ClassValue c, string name) { - name = ordering_name(_) and - ( - c.declaresAttribute(name) - or - exists(ClassValue sup | sup = c.getASuperType() and not sup = Value::named("object") | - sup.declaresAttribute(name) - ) - ) +Function getMethod(Class cls, string name) { + result = cls.getAMethod() and + result.getName() = name } -string unimplemented_ordering(ClassValue c, int n) { - not c = Value::named("object") and - not overrides_ordering_method(c, result) and - result = ordering_name(n) +predicate definesStrictOrdering(Class cls, Function meth) { + meth = getMethod(cls, "__lt__") + or + not exists(getMethod(cls, "__lt__")) and + meth = getMethod(cls, "__gt__") } -string unimplemented_ordering_methods(ClassValue c, int n) { - n = 0 and result = "" and exists(unimplemented_ordering(c, _)) +predicate definesNonStrictOrdering(Class cls, Function meth) { + meth = getMethod(cls, "__le__") or - exists(string prefix, int nm1 | n = nm1 + 1 and prefix = unimplemented_ordering_methods(c, nm1) | - prefix = "" and result = unimplemented_ordering(c, n) - or - result = prefix and not exists(unimplemented_ordering(c, n)) and n < 5 - or - prefix != "" and result = prefix + " or " + unimplemented_ordering(c, n) - ) + not exists(getMethod(cls, "__le__")) and + meth = getMethod(cls, "__ge__") } -Value ordering_method(ClassValue c, string name) { - /* If class doesn't declare a method then don't blame this class (the superclass will be blamed). */ - name = ordering_name(_) and result = c.declaredAttribute(name) +predicate missingComparison(Class cls, Function defined, string missing) { + definesStrictOrdering(cls, defined) and + not definesNonStrictOrdering(getADirectSuperclass*(cls), _) and + missing = "__le__ or __ge__" + or + definesNonStrictOrdering(cls, defined) and + not definesStrictOrdering(getADirectSuperclass*(cls), _) and + missing = "__lt__ or __gt__" } -from ClassValue c, Value ordering, string name +from Class cls, Function defined, string missing where - not c.failedInference(_) and - not total_ordering(c.getScope()) and - ordering = ordering_method(c, name) and - exists(unimplemented_ordering(c, _)) -select c, - "Class " + c.getName() + " implements $@, but does not implement " + - unimplemented_ordering_methods(c, 4) + ".", ordering, name + not totalOrdering(cls) and + missingComparison(cls, defined, missing) +select cls, "This class implements $@, but does not implement an " + missing + " method.", defined, + defined.getName() From e71af8fd6d2b834a1de6629a82896900c79b1c11 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Tue, 8 Jul 2025 11:14:19 +0100 Subject: [PATCH 02/15] Move to subfolder --- python/ql/src/Classes/{ => Comparisons}/IncompleteOrdering.qhelp | 0 python/ql/src/Classes/{ => Comparisons}/IncompleteOrdering.ql | 0 .../src/Classes/{ => Comparisons/examples}/IncompleteOrdering.py | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename python/ql/src/Classes/{ => Comparisons}/IncompleteOrdering.qhelp (100%) rename python/ql/src/Classes/{ => Comparisons}/IncompleteOrdering.ql (100%) rename python/ql/src/Classes/{ => Comparisons/examples}/IncompleteOrdering.py (100%) diff --git a/python/ql/src/Classes/IncompleteOrdering.qhelp b/python/ql/src/Classes/Comparisons/IncompleteOrdering.qhelp similarity index 100% rename from python/ql/src/Classes/IncompleteOrdering.qhelp rename to python/ql/src/Classes/Comparisons/IncompleteOrdering.qhelp diff --git a/python/ql/src/Classes/IncompleteOrdering.ql b/python/ql/src/Classes/Comparisons/IncompleteOrdering.ql similarity index 100% rename from python/ql/src/Classes/IncompleteOrdering.ql rename to python/ql/src/Classes/Comparisons/IncompleteOrdering.ql diff --git a/python/ql/src/Classes/IncompleteOrdering.py b/python/ql/src/Classes/Comparisons/examples/IncompleteOrdering.py similarity index 100% rename from python/ql/src/Classes/IncompleteOrdering.py rename to python/ql/src/Classes/Comparisons/examples/IncompleteOrdering.py From 4c5c4e06c3b0c0d80cf930a2910fe5a668bf21c4 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Tue, 8 Jul 2025 11:33:47 +0100 Subject: [PATCH 03/15] Move inconsistentEquality and equals-hash-mismatch to subfolder --- python/ql/src/Classes/{ => Comparisons}/EqualsOrHash.qhelp | 0 python/ql/src/Classes/{ => Comparisons}/EqualsOrHash.ql | 0 python/ql/src/Classes/{ => Comparisons}/EqualsOrNotEquals.qhelp | 0 python/ql/src/Classes/{ => Comparisons}/EqualsOrNotEquals.ql | 0 python/ql/src/Classes/{ => Comparisons/examples}/EqualsOrHash.py | 0 .../src/Classes/{ => Comparisons/examples}/EqualsOrNotEquals.py | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename python/ql/src/Classes/{ => Comparisons}/EqualsOrHash.qhelp (100%) rename python/ql/src/Classes/{ => Comparisons}/EqualsOrHash.ql (100%) rename python/ql/src/Classes/{ => Comparisons}/EqualsOrNotEquals.qhelp (100%) rename python/ql/src/Classes/{ => Comparisons}/EqualsOrNotEquals.ql (100%) rename python/ql/src/Classes/{ => Comparisons/examples}/EqualsOrHash.py (100%) rename python/ql/src/Classes/{ => Comparisons/examples}/EqualsOrNotEquals.py (100%) diff --git a/python/ql/src/Classes/EqualsOrHash.qhelp b/python/ql/src/Classes/Comparisons/EqualsOrHash.qhelp similarity index 100% rename from python/ql/src/Classes/EqualsOrHash.qhelp rename to python/ql/src/Classes/Comparisons/EqualsOrHash.qhelp diff --git a/python/ql/src/Classes/EqualsOrHash.ql b/python/ql/src/Classes/Comparisons/EqualsOrHash.ql similarity index 100% rename from python/ql/src/Classes/EqualsOrHash.ql rename to python/ql/src/Classes/Comparisons/EqualsOrHash.ql diff --git a/python/ql/src/Classes/EqualsOrNotEquals.qhelp b/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.qhelp similarity index 100% rename from python/ql/src/Classes/EqualsOrNotEquals.qhelp rename to python/ql/src/Classes/Comparisons/EqualsOrNotEquals.qhelp diff --git a/python/ql/src/Classes/EqualsOrNotEquals.ql b/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.ql similarity index 100% rename from python/ql/src/Classes/EqualsOrNotEquals.ql rename to python/ql/src/Classes/Comparisons/EqualsOrNotEquals.ql diff --git a/python/ql/src/Classes/EqualsOrHash.py b/python/ql/src/Classes/Comparisons/examples/EqualsOrHash.py similarity index 100% rename from python/ql/src/Classes/EqualsOrHash.py rename to python/ql/src/Classes/Comparisons/examples/EqualsOrHash.py diff --git a/python/ql/src/Classes/EqualsOrNotEquals.py b/python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals.py similarity index 100% rename from python/ql/src/Classes/EqualsOrNotEquals.py rename to python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals.py From eb1b5a35d790d851bbbd469915a0288f6b01ad4f Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Tue, 8 Jul 2025 15:33:59 +0100 Subject: [PATCH 04/15] Modernize inconsistent equality --- python/ql/lib/semmle/python/Class.qll | 6 ++ .../src/Classes/Comparisons/Comparisons.qll | 10 ++++ .../Classes/Comparisons/EqualsOrNotEquals.ql | 56 ++++++++----------- .../Classes/Comparisons/IncompleteOrdering.ql | 25 +++------ .../Comparisons/examples/EqualsOrNotEquals.py | 24 ++++++++ python/ql/src/Classes/Equality.qll | 25 +++++++-- 6 files changed, 92 insertions(+), 54 deletions(-) create mode 100644 python/ql/src/Classes/Comparisons/Comparisons.qll diff --git a/python/ql/lib/semmle/python/Class.qll b/python/ql/lib/semmle/python/Class.qll index 52c6c5aa389b..58a6504b547c 100644 --- a/python/ql/lib/semmle/python/Class.qll +++ b/python/ql/lib/semmle/python/Class.qll @@ -91,6 +91,12 @@ class Class extends Class_, Scope, AstNode { /** Gets a method defined in this class */ Function getAMethod() { result.getScope() = this } + /** Gets the method defined in this class with the specified name, if any. */ + Function getMethod(string name) { + result = this.getAMethod() and + result.getName() = name + } + override Location getLocation() { py_scope_location(result, this) } /** Gets the scope (module, class or function) in which this class is defined */ diff --git a/python/ql/src/Classes/Comparisons/Comparisons.qll b/python/ql/src/Classes/Comparisons/Comparisons.qll new file mode 100644 index 000000000000..b835b07ef44a --- /dev/null +++ b/python/ql/src/Classes/Comparisons/Comparisons.qll @@ -0,0 +1,10 @@ +/** Helper definitions for reasoning about comparison methods. */ + +import python +import semmle.python.ApiGraphs + +/** Holds if `cls` has the `functools.total_ordering` decorator. */ +predicate totalOrdering(Class cls) { + cls.getADecorator() = + API::moduleImport("functools").getMember("total_ordering").asSource().asExpr() +} diff --git a/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.ql b/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.ql index adac5a20e87a..feeada866827 100644 --- a/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.ql +++ b/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.ql @@ -2,7 +2,8 @@ * @name Inconsistent equality and inequality * @description Defining only an equality method or an inequality method for a class violates the object model. * @kind problem - * @tags reliability + * @tags quality + * reliability * correctness * @problem.severity warning * @sub-severity high @@ -11,38 +12,29 @@ */ import python -import Equality +import Comparisons +import semmle.python.dataflow.new.internal.DataFlowDispatch +import Classes.Equality -string equals_or_ne() { result = "__eq__" or result = "__ne__" } - -predicate total_ordering(Class cls) { - exists(Attribute a | a = cls.getADecorator() | a.getName() = "total_ordering") +predicate missingEquality(Class cls, Function defined, string missing) { + defined = cls.getMethod("__ne__") and + not exists(cls.getMethod("__eq__")) and + missing = "__eq__" or - exists(Name n | n = cls.getADecorator() | n.getId() = "total_ordering") -} - -CallableValue implemented_method(ClassValue c, string name) { - result = c.declaredAttribute(name) and name = equals_or_ne() -} - -string unimplemented_method(ClassValue c) { - not c.declaresAttribute(result) and result = equals_or_ne() -} - -predicate violates_equality_contract( - ClassValue c, string present, string missing, CallableValue method -) { - missing = unimplemented_method(c) and - method = implemented_method(c, present) and - not c.failedInference(_) and - not total_ordering(c.getScope()) and - /* Python 3 automatically implements __ne__ if __eq__ is defined, but not vice-versa */ - not (major_version() = 3 and present = "__eq__" and missing = "__ne__") and - not method.getScope() instanceof DelegatingEqualityMethod and - not c.lookup(missing).(CallableValue).getScope() instanceof DelegatingEqualityMethod + // In python 3, __ne__ automatically delegates to __eq__ if its not defined in the hierarchy + // However if it is defined in a superclass (and isn't a delegation method) then it will use the superclass method (which may be incorrect) + defined = cls.getMethod("__eq__") and + not exists(cls.getMethod("__ne__")) and + exists(Function neMeth | + neMeth = getADirectSuperclass+(cls).getMethod("__ne__") and + not neMeth instanceof DelegatingEqualityMethod + ) and + missing = "__ne__" } -from ClassValue c, string present, string missing, CallableValue method -where violates_equality_contract(c, present, missing, method) -select method, "Class $@ implements " + present + " but does not implement " + missing + ".", c, - c.getName() +from Class cls, Function defined, string missing +where + not totalOrdering(cls) and + missingEquality(cls, defined, missing) +select cls, "This class implements $@, but does not implement " + missing + ".", defined, + defined.getName() diff --git a/python/ql/src/Classes/Comparisons/IncompleteOrdering.ql b/python/ql/src/Classes/Comparisons/IncompleteOrdering.ql index bbb6ca5cf6dc..882321cc3f5f 100644 --- a/python/ql/src/Classes/Comparisons/IncompleteOrdering.ql +++ b/python/ql/src/Classes/Comparisons/IncompleteOrdering.ql @@ -14,29 +14,20 @@ import python import semmle.python.dataflow.new.internal.DataFlowDispatch import semmle.python.ApiGraphs - -predicate totalOrdering(Class cls) { - cls.getADecorator() = - API::moduleImport("functools").getMember("total_ordering").asSource().asExpr() -} - -Function getMethod(Class cls, string name) { - result = cls.getAMethod() and - result.getName() = name -} +import Comparisons predicate definesStrictOrdering(Class cls, Function meth) { - meth = getMethod(cls, "__lt__") + meth = cls.getMethod("__lt__") or - not exists(getMethod(cls, "__lt__")) and - meth = getMethod(cls, "__gt__") + not exists(cls.getMethod("__lt__")) and + meth = cls.getMethod("__gt__") } predicate definesNonStrictOrdering(Class cls, Function meth) { - meth = getMethod(cls, "__le__") + meth = cls.getMethod("__le__") or - not exists(getMethod(cls, "__le__")) and - meth = getMethod(cls, "__ge__") + not exists(cls.getMethod("__le__")) and + meth = cls.getMethod("__ge__") } predicate missingComparison(Class cls, Function defined, string missing) { @@ -53,5 +44,5 @@ from Class cls, Function defined, string missing where not totalOrdering(cls) and missingComparison(cls, defined, missing) -select cls, "This class implements $@, but does not implement an " + missing + " method.", defined, +select cls, "This class implements $@, but does not implement " + missing + ".", defined, defined.getName() diff --git a/python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals.py b/python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals.py index 7e1ece7685c5..32bc26d47370 100644 --- a/python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals.py +++ b/python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals.py @@ -30,3 +30,27 @@ def __eq__(self, other): def __ne__(self, other): # Improved: equality and inequality method defined (hash method still missing) return not self == other + + +class A: + def __init__(self, a): + self.a = a + + def __eq__(self, other): + print("A eq") + return self.a == other.a + + def __ne__(self, other): + print("A ne") + return self.a != other.a + +class B(A): + def __init__(self, a, b): + self.a = a + self.b = b + + def __eq__(self, other): + print("B eq") + return self.a == other.a and self.b == other.b + +print(B(1,2) != B(1,3)) diff --git a/python/ql/src/Classes/Equality.qll b/python/ql/src/Classes/Equality.qll index 347f5057c38c..08162399e3e9 100644 --- a/python/ql/src/Classes/Equality.qll +++ b/python/ql/src/Classes/Equality.qll @@ -1,4 +1,7 @@ +/** Utility definitions for reasoning about equality methods. */ + import python +import semmle.python.dataflow.new.DataFlow private Attribute dictAccess(LocalVariable var) { result.getName() = "__dict__" and @@ -59,16 +62,28 @@ class IdentityEqMethod extends Function { /** An (in)equality method that delegates to its complement */ class DelegatingEqualityMethod extends Function { DelegatingEqualityMethod() { - exists(Return ret, UnaryExpr not_, Compare comp, Cmpop op, Parameter p0, Parameter p1 | + exists(Return ret, UnaryExpr not_, Expr comp, Parameter p0, Parameter p1 | ret.getScope() = this and ret.getValue() = not_ and not_.getOp() instanceof Not and - not_.getOperand() = comp and - comp.compares(p0.getVariable().getAnAccess(), op, p1.getVariable().getAnAccess()) + not_.getOperand() = comp | - this.getName() = "__eq__" and op instanceof NotEq + exists(Cmpop op | + comp.(Compare).compares(p0.getVariable().getAnAccess(), op, p1.getVariable().getAnAccess()) + | + this.getName() = "__eq__" and op instanceof NotEq + or + this.getName() = "__ne__" and op instanceof Eq + ) or - this.getName() = "__ne__" and op instanceof Eq + exists(DataFlow::MethodCallNode call, string name | + call.calls(DataFlow::exprNode(p0.getVariable().getAnAccess()), name) and + call.getArg(0).asExpr() = p1.getVariable().getAnAccess() + | + this.getName() = "__eq__" and name = "__ne__" + or + this.getName() = "__ne__" and name = "__eq__" + ) ) } } From a687b60af987f948ef7df79b8d2825f930512ca4 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Wed, 9 Jul 2025 13:32:13 +0100 Subject: [PATCH 05/15] Modernise equals-hash-mismatch --- .../src/Classes/Comparisons/EqualsOrHash.ql | 53 +++---------------- 1 file changed, 8 insertions(+), 45 deletions(-) diff --git a/python/ql/src/Classes/Comparisons/EqualsOrHash.ql b/python/ql/src/Classes/Comparisons/EqualsOrHash.ql index 4c8cf2c11699..4e73cef92fd2 100644 --- a/python/ql/src/Classes/Comparisons/EqualsOrHash.ql +++ b/python/ql/src/Classes/Comparisons/EqualsOrHash.ql @@ -14,50 +14,13 @@ import python -CallableValue defines_equality(ClassValue c, string name) { - ( - name = "__eq__" - or - major_version() = 2 and name = "__cmp__" - ) and - result = c.declaredAttribute(name) +predicate missingEquality(Class cls, Function defined) { + defined = cls.getMethod("__hash__") and + not exists(cls.getMethod("__eq__")) + // In python 3, the case of defined eq without hash automatically makes the class unhashable (even if a superclass defined hash) + // So this is not an issue. } -CallableValue implemented_method(ClassValue c, string name) { - result = defines_equality(c, name) - or - result = c.declaredAttribute("__hash__") and name = "__hash__" -} - -string unimplemented_method(ClassValue c) { - not exists(defines_equality(c, _)) and - ( - result = "__eq__" and major_version() = 3 - or - major_version() = 2 and result = "__eq__ or __cmp__" - ) - or - /* Python 3 automatically makes classes unhashable if __eq__ is defined, but __hash__ is not */ - not c.declaresAttribute(result) and result = "__hash__" and major_version() = 2 -} - -/** Holds if this class is unhashable */ -predicate unhashable(ClassValue cls) { - cls.lookup("__hash__") = Value::named("None") - or - cls.lookup("__hash__").(CallableValue).neverReturns() -} - -predicate violates_hash_contract(ClassValue c, string present, string missing, Value method) { - not unhashable(c) and - missing = unimplemented_method(c) and - method = implemented_method(c, present) and - not c.failedInference(_) -} - -from ClassValue c, string present, string missing, CallableValue method -where - violates_hash_contract(c, present, missing, method) and - exists(c.getScope()) // Suppress results that aren't from source -select method, "Class $@ implements " + present + " but does not define " + missing + ".", c, - c.getName() +from Class cls, Function defined +where missingEquality(cls, defined) +select cls, "This class implements $@, but does not implement __eq__.", defined, defined.getName() From 8fb9bdd0afb985b3d7e566db40177f4452286e6e Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Wed, 9 Jul 2025 15:25:21 +0100 Subject: [PATCH 06/15] move equals attr test to equals attr folder --- .../equals-attr/DefineEqualsWhenAddingAttributes.expected | 1 + .../Classes/{equals-hash => equals-attr}/attr_eq_test.py | 0 .../Classes/equals-hash/DefineEqualsWhenAddingFields.expected | 1 - .../Classes/equals-hash/DefineEqualsWhenAddingFields.qlref | 1 - 4 files changed, 1 insertion(+), 2 deletions(-) rename python/ql/test/query-tests/Classes/{equals-hash => equals-attr}/attr_eq_test.py (100%) delete mode 100644 python/ql/test/query-tests/Classes/equals-hash/DefineEqualsWhenAddingFields.expected delete mode 100644 python/ql/test/query-tests/Classes/equals-hash/DefineEqualsWhenAddingFields.qlref diff --git a/python/ql/test/query-tests/Classes/equals-attr/DefineEqualsWhenAddingAttributes.expected b/python/ql/test/query-tests/Classes/equals-attr/DefineEqualsWhenAddingAttributes.expected index e69de29bb2d1..2f5a5a249f5f 100644 --- a/python/ql/test/query-tests/Classes/equals-attr/DefineEqualsWhenAddingAttributes.expected +++ b/python/ql/test/query-tests/Classes/equals-attr/DefineEqualsWhenAddingAttributes.expected @@ -0,0 +1 @@ +| attr_eq_test.py:21:1:21:27 | class BadColorPoint | The class 'BadColorPoint' does not override $@, but adds the new attribute $@. | attr_eq_test.py:10:5:10:28 | Function Point.__eq__ | '__eq__' | attr_eq_test.py:25:9:25:19 | Attribute | _color | diff --git a/python/ql/test/query-tests/Classes/equals-hash/attr_eq_test.py b/python/ql/test/query-tests/Classes/equals-attr/attr_eq_test.py similarity index 100% rename from python/ql/test/query-tests/Classes/equals-hash/attr_eq_test.py rename to python/ql/test/query-tests/Classes/equals-attr/attr_eq_test.py diff --git a/python/ql/test/query-tests/Classes/equals-hash/DefineEqualsWhenAddingFields.expected b/python/ql/test/query-tests/Classes/equals-hash/DefineEqualsWhenAddingFields.expected deleted file mode 100644 index 2f5a5a249f5f..000000000000 --- a/python/ql/test/query-tests/Classes/equals-hash/DefineEqualsWhenAddingFields.expected +++ /dev/null @@ -1 +0,0 @@ -| attr_eq_test.py:21:1:21:27 | class BadColorPoint | The class 'BadColorPoint' does not override $@, but adds the new attribute $@. | attr_eq_test.py:10:5:10:28 | Function Point.__eq__ | '__eq__' | attr_eq_test.py:25:9:25:19 | Attribute | _color | diff --git a/python/ql/test/query-tests/Classes/equals-hash/DefineEqualsWhenAddingFields.qlref b/python/ql/test/query-tests/Classes/equals-hash/DefineEqualsWhenAddingFields.qlref deleted file mode 100644 index e542a6176ad4..000000000000 --- a/python/ql/test/query-tests/Classes/equals-hash/DefineEqualsWhenAddingFields.qlref +++ /dev/null @@ -1 +0,0 @@ -Classes/DefineEqualsWhenAddingAttributes.ql \ No newline at end of file From 083d258585b0a226763b5301867c213b66456d25 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Fri, 11 Jul 2025 15:10:45 +0100 Subject: [PATCH 07/15] Add/update unit tests --- .../src/Classes/Comparisons/Comparisons.qll | 6 +- .../Classes/equals-hash/EqualsOrHash.expected | 2 + .../Classes/equals-hash/EqualsOrHash.qlref | 2 + .../Classes/equals-hash/equalsHash.py | 19 +++ .../EqualsOrNotEquals.expected | 2 + .../equals-not-equals/EqualsOrNotEquals.py | 147 ++++++++++++++++++ .../equals-not-equals/EqualsOrNotEquals.qlref | 2 + .../IncompleteOrdering.expected | 3 +- .../IncompleteOrdering.qlref | 3 +- .../incomplete_ordering.py | 30 +++- 10 files changed, 208 insertions(+), 8 deletions(-) create mode 100644 python/ql/test/query-tests/Classes/equals-hash/EqualsOrHash.expected create mode 100644 python/ql/test/query-tests/Classes/equals-hash/EqualsOrHash.qlref create mode 100644 python/ql/test/query-tests/Classes/equals-hash/equalsHash.py create mode 100644 python/ql/test/query-tests/Classes/equals-not-equals/EqualsOrNotEquals.expected create mode 100644 python/ql/test/query-tests/Classes/equals-not-equals/EqualsOrNotEquals.py create mode 100644 python/ql/test/query-tests/Classes/equals-not-equals/EqualsOrNotEquals.qlref diff --git a/python/ql/src/Classes/Comparisons/Comparisons.qll b/python/ql/src/Classes/Comparisons/Comparisons.qll index b835b07ef44a..5c049410c696 100644 --- a/python/ql/src/Classes/Comparisons/Comparisons.qll +++ b/python/ql/src/Classes/Comparisons/Comparisons.qll @@ -5,6 +5,8 @@ import semmle.python.ApiGraphs /** Holds if `cls` has the `functools.total_ordering` decorator. */ predicate totalOrdering(Class cls) { - cls.getADecorator() = - API::moduleImport("functools").getMember("total_ordering").asSource().asExpr() + API::moduleImport("functools") + .getMember("total_ordering") + .asSource() + .flowsTo(DataFlow::exprNode(cls.getADecorator())) } diff --git a/python/ql/test/query-tests/Classes/equals-hash/EqualsOrHash.expected b/python/ql/test/query-tests/Classes/equals-hash/EqualsOrHash.expected new file mode 100644 index 000000000000..bd584939b43d --- /dev/null +++ b/python/ql/test/query-tests/Classes/equals-hash/EqualsOrHash.expected @@ -0,0 +1,2 @@ +| equalsHash.py:13:1:13:8 | Class C | This class implements $@, but does not implement __eq__. | equalsHash.py:14:5:14:23 | Function __hash__ | __hash__ | +| equalsHash.py:17:1:17:11 | Class D | This class implements $@, but does not implement __eq__. | equalsHash.py:18:5:18:23 | Function __hash__ | __hash__ | diff --git a/python/ql/test/query-tests/Classes/equals-hash/EqualsOrHash.qlref b/python/ql/test/query-tests/Classes/equals-hash/EqualsOrHash.qlref new file mode 100644 index 000000000000..e531bbc62e32 --- /dev/null +++ b/python/ql/test/query-tests/Classes/equals-hash/EqualsOrHash.qlref @@ -0,0 +1,2 @@ +query: Classes/Comparisons/EqualsOrHash.ql +postprocess: utils/test/InlineExpectationsTestQuery.ql \ No newline at end of file diff --git a/python/ql/test/query-tests/Classes/equals-hash/equalsHash.py b/python/ql/test/query-tests/Classes/equals-hash/equalsHash.py new file mode 100644 index 000000000000..6b3ec5d2b02a --- /dev/null +++ b/python/ql/test/query-tests/Classes/equals-hash/equalsHash.py @@ -0,0 +1,19 @@ +class A: + def __eq__(self, other): + return True + + def __hash__(self, other): + return 7 + +# B is automatically non-hashable - so eq without hash never needs to alert +class B: + def __eq__(self, other): + return True + +class C: # $ Alert + def __hash__(self): + return 5 + +class D(A): # $ Alert + def __hash__(self): + return 4 \ No newline at end of file diff --git a/python/ql/test/query-tests/Classes/equals-not-equals/EqualsOrNotEquals.expected b/python/ql/test/query-tests/Classes/equals-not-equals/EqualsOrNotEquals.expected new file mode 100644 index 000000000000..ceec3c1cef98 --- /dev/null +++ b/python/ql/test/query-tests/Classes/equals-not-equals/EqualsOrNotEquals.expected @@ -0,0 +1,2 @@ +| EqualsOrNotEquals.py:14:1:14:8 | Class B | This class implements $@, but does not implement __eq__. | EqualsOrNotEquals.py:19:5:19:28 | Function __ne__ | __ne__ | +| EqualsOrNotEquals.py:37:1:37:11 | Class D | This class implements $@, but does not implement __ne__. | EqualsOrNotEquals.py:43:5:43:28 | Function __eq__ | __eq__ | diff --git a/python/ql/test/query-tests/Classes/equals-not-equals/EqualsOrNotEquals.py b/python/ql/test/query-tests/Classes/equals-not-equals/EqualsOrNotEquals.py new file mode 100644 index 000000000000..2052118e749a --- /dev/null +++ b/python/ql/test/query-tests/Classes/equals-not-equals/EqualsOrNotEquals.py @@ -0,0 +1,147 @@ +class A: + def __init__(self, a): + self.a = a + + # OK: __ne__ if not defined delegates to eq automatically + def __eq__(self, other): + return self.a == other.a + +assert (A(1) == A(1)) +assert not (A(1) == A(2)) +assert not (A(1) != A(1)) +assert (A(1) != A(2)) + +class B: # $ Alert + def __init__(self, b): + self.b = b + + # BAD: eq defaults to `is` + def __ne__(self, other): + return self.b != other.b + +assert not (B(1) == B(1)) # potentially unexpected +assert not (B(2) == B(2)) +assert not (B(1) != B(1)) +assert (B(1) != B(2)) + +class C: + def __init__(self, c): + self.c = c + + def __eq__(self, other): + return self.c == other.c + + def __ne__(self, other): + return self.c != other.c + +class D(C): # $ Alert + def __init__(self, c, d): + super().__init__(c) + self.d = d + + # BAD: ne is not defined, but the superclass ne is used instead of delegating, which may be incorrect + def __eq__(self, other): + return self.c == other.c and self.d == other.d + +assert (D(1,2) == D(1,2)) +assert not (D(1,2) == D(1,3)) +assert (D(1,2) != D(3,2)) +assert not (D(1,2) != D(1,3)) # Potentially unexpected + +class E: + def __init__(self, e): + self.e = e + + def __eq__(self, other): + return self.e == other.e + + def __ne__(self, other): + return not self.__eq__(other) + +class F(E): + def __init__(self, e, f): + super().__init__(e) + self.f = f + + # OK: superclass ne delegates to eq + def __eq__(self, other): + return self.e == other.e and self.f == other.f + +assert (F(1,2) == F(1,2)) +assert not (F(1,2) == F(1,3)) +assert (F(1,2) != F(3,2)) +assert (F(1,2) != F(1,3)) + +# Variations + +class E2: + def __init__(self, e): + self.e = e + + def __eq__(self, other): + return self.e == other.e + + def __ne__(self, other): + return not self == other + +class F2(E2): + def __init__(self, e, f): + super().__init__(e) + self.f = f + + # OK: superclass ne delegates to eq + def __eq__(self, other): + return self.e == other.e and self.f == other.f + +assert (F2(1,2) == F2(1,2)) +assert not (F2(1,2) == F2(1,3)) +assert (F2(1,2) != F2(3,2)) +assert (F2(1,2) != F2(1,3)) + +class E3: + def __init__(self, e): + self.e = e + + def __eq__(self, other): + return self.e == other.e + + def __ne__(self, other): + return not other.__eq__(self) + +class F3(E3): + def __init__(self, e, f): + super().__init__(e) + self.f = f + + # OK: superclass ne delegates to eq + def __eq__(self, other): + return self.e == other.e and self.f == other.f + +assert (F3(1,2) == F3(1,2)) +assert not (F3(1,2) == F3(1,3)) +assert (F3(1,2) != F3(3,2)) +assert (F3(1,2) != F3(1,3)) + +class E4: + def __init__(self, e): + self.e = e + + def __eq__(self, other): + return self.e == other.e + + def __ne__(self, other): + return not other == self + +class F4(E4): + def __init__(self, e, f): + super().__init__(e) + self.f = f + + # OK: superclass ne delegates to eq + def __eq__(self, other): + return self.e == other.e and self.f == other.f + +assert (F4(1,2) == F4(1,2)) +assert not (F4(1,2) == F4(1,3)) +assert (F4(1,2) != F4(3,2)) +assert (F4(1,2) != F4(1,3)) \ No newline at end of file diff --git a/python/ql/test/query-tests/Classes/equals-not-equals/EqualsOrNotEquals.qlref b/python/ql/test/query-tests/Classes/equals-not-equals/EqualsOrNotEquals.qlref new file mode 100644 index 000000000000..9b1e8646c0e3 --- /dev/null +++ b/python/ql/test/query-tests/Classes/equals-not-equals/EqualsOrNotEquals.qlref @@ -0,0 +1,2 @@ +query: Classes/Comparisons/EqualsOrNotEquals.ql +postprocess: utils/test/InlineExpectationsTestQuery.ql \ No newline at end of file diff --git a/python/ql/test/query-tests/Classes/incomplete-ordering/IncompleteOrdering.expected b/python/ql/test/query-tests/Classes/incomplete-ordering/IncompleteOrdering.expected index d376a0023353..94df0ad1d326 100644 --- a/python/ql/test/query-tests/Classes/incomplete-ordering/IncompleteOrdering.expected +++ b/python/ql/test/query-tests/Classes/incomplete-ordering/IncompleteOrdering.expected @@ -1 +1,2 @@ -| incomplete_ordering.py:3:1:3:26 | class PartOrdered | Class PartOrdered implements $@, but does not implement __le__ or __gt__ or __ge__. | incomplete_ordering.py:13:5:13:28 | Function PartOrdered.__lt__ | __lt__ | +| incomplete_ordering.py:3:1:3:26 | Class LtWithoutLe | This class implements $@, but does not implement __le__ or __ge__. | incomplete_ordering.py:13:5:13:28 | Function __lt__ | __lt__ | +| incomplete_ordering.py:28:1:28:17 | Class LendGeNoLt | This class implements $@, but does not implement __lt__ or __gt__. | incomplete_ordering.py:29:5:29:28 | Function __le__ | __le__ | diff --git a/python/ql/test/query-tests/Classes/incomplete-ordering/IncompleteOrdering.qlref b/python/ql/test/query-tests/Classes/incomplete-ordering/IncompleteOrdering.qlref index 3387dad807a7..cb15c6a47ba5 100644 --- a/python/ql/test/query-tests/Classes/incomplete-ordering/IncompleteOrdering.qlref +++ b/python/ql/test/query-tests/Classes/incomplete-ordering/IncompleteOrdering.qlref @@ -1 +1,2 @@ -Classes/IncompleteOrdering.ql \ No newline at end of file +query: Classes/Comparisons/IncompleteOrdering.ql +postprocess: utils/test/InlineExpectationsTestQuery.ql \ No newline at end of file diff --git a/python/ql/test/query-tests/Classes/incomplete-ordering/incomplete_ordering.py b/python/ql/test/query-tests/Classes/incomplete-ordering/incomplete_ordering.py index 3c7514d7a838..2645819c43b1 100644 --- a/python/ql/test/query-tests/Classes/incomplete-ordering/incomplete_ordering.py +++ b/python/ql/test/query-tests/Classes/incomplete-ordering/incomplete_ordering.py @@ -1,6 +1,6 @@ #Incomplete ordering -class PartOrdered(object): +class LtWithoutLe(object): # $ Alert def __eq__(self, other): return self is other @@ -13,6 +13,28 @@ def __hash__(self): def __lt__(self, other): return False -#Don't blame a sub-class for super-class's sins. -class DerivedPartOrdered(PartOrdered): - pass \ No newline at end of file +# Don't alert on subclass +class LtWithoutLeSub(LtWithoutLe): + pass + +class LeSub(LtWithoutLe): + def __le__(self, other): + return self < other or self == other + +class GeSub(LtWithoutLe): + def __ge__(self, other): + return self > other or self == other + +class LendGeNoLt: # $ Alert + def __le__(self, other): + return True + + def __ge__(self, other): + return other <= self + +from functools import total_ordering + +@total_ordering +class Total: + def __le__(self, other): + return True \ No newline at end of file From 843a6c8012471c9966bbe8cbb2e6e18c0118fb3e Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Fri, 11 Jul 2025 15:12:59 +0100 Subject: [PATCH 08/15] Remove total order check from equals not equals (doesn't make sense there; total order doesn't define eq or ne methods at all) --- python/ql/src/Classes/Comparisons/Comparisons.qll | 12 ------------ .../ql/src/Classes/Comparisons/EqualsOrNotEquals.ql | 5 +---- .../ql/src/Classes/Comparisons/IncompleteOrdering.ql | 9 ++++++++- 3 files changed, 9 insertions(+), 17 deletions(-) delete mode 100644 python/ql/src/Classes/Comparisons/Comparisons.qll diff --git a/python/ql/src/Classes/Comparisons/Comparisons.qll b/python/ql/src/Classes/Comparisons/Comparisons.qll deleted file mode 100644 index 5c049410c696..000000000000 --- a/python/ql/src/Classes/Comparisons/Comparisons.qll +++ /dev/null @@ -1,12 +0,0 @@ -/** Helper definitions for reasoning about comparison methods. */ - -import python -import semmle.python.ApiGraphs - -/** Holds if `cls` has the `functools.total_ordering` decorator. */ -predicate totalOrdering(Class cls) { - API::moduleImport("functools") - .getMember("total_ordering") - .asSource() - .flowsTo(DataFlow::exprNode(cls.getADecorator())) -} diff --git a/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.ql b/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.ql index feeada866827..25aafea6db2d 100644 --- a/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.ql +++ b/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.ql @@ -12,7 +12,6 @@ */ import python -import Comparisons import semmle.python.dataflow.new.internal.DataFlowDispatch import Classes.Equality @@ -33,8 +32,6 @@ predicate missingEquality(Class cls, Function defined, string missing) { } from Class cls, Function defined, string missing -where - not totalOrdering(cls) and - missingEquality(cls, defined, missing) +where missingEquality(cls, defined, missing) select cls, "This class implements $@, but does not implement " + missing + ".", defined, defined.getName() diff --git a/python/ql/src/Classes/Comparisons/IncompleteOrdering.ql b/python/ql/src/Classes/Comparisons/IncompleteOrdering.ql index 882321cc3f5f..2a09b2810585 100644 --- a/python/ql/src/Classes/Comparisons/IncompleteOrdering.ql +++ b/python/ql/src/Classes/Comparisons/IncompleteOrdering.ql @@ -14,7 +14,14 @@ import python import semmle.python.dataflow.new.internal.DataFlowDispatch import semmle.python.ApiGraphs -import Comparisons + +/** Holds if `cls` has the `functools.total_ordering` decorator. */ +predicate totalOrdering(Class cls) { + API::moduleImport("functools") + .getMember("total_ordering") + .asSource() + .flowsTo(DataFlow::exprNode(cls.getADecorator())) +} predicate definesStrictOrdering(Class cls, Function meth) { meth = cls.getMethod("__lt__") From 58f503de38cbd8e2cd9dc07a209a6fdfb4fb4376 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Fri, 11 Jul 2025 23:08:50 +0100 Subject: [PATCH 09/15] Update docs for incomplete ordering + inconsistent hashing --- .../Classes/Comparisons/EqualsOrHash.qhelp | 38 ++++++------ .../src/Classes/Comparisons/EqualsOrHash.ql | 2 +- .../Classes/Comparisons/EqualsOrNotEquals.ql | 2 +- .../Comparisons/IncompleteOrdering.qhelp | 30 +++++----- .../Classes/Comparisons/IncompleteOrdering.ql | 2 +- .../Comparisons/examples/EqualsOrHash.py | 60 +++---------------- .../Comparisons/examples/EqualsOrNotEquals.py | 23 ------- .../examples/IncompleteOrdering.py | 6 +- 8 files changed, 49 insertions(+), 114 deletions(-) diff --git a/python/ql/src/Classes/Comparisons/EqualsOrHash.qhelp b/python/ql/src/Classes/Comparisons/EqualsOrHash.qhelp index 28579a095f70..562ad7be1bd6 100644 --- a/python/ql/src/Classes/Comparisons/EqualsOrHash.qhelp +++ b/python/ql/src/Classes/Comparisons/EqualsOrHash.qhelp @@ -4,42 +4,40 @@ -

In order to conform to the object model, classes that define their own equality method should also -define their own hash method, or be unhashable. If the hash method is not defined then the hash of the -super class is used. This is unlikely to result in the expected behavior.

+

A hashable class has an __eq__ method, and a __hash__ method that agrees with equality. +When a hash method is defined, an equality method should also be defined; otherwise object identity is used for equality comparisons +which may not be intended. +

-

A class can be made unhashable by setting its __hash__ attribute to None.

- -

In Python 3, if you define a class-level equality method and omit a __hash__ method -then the class is automatically marked as unhashable.

+

Note that defining an __eq__ method without defining a __hash__ method automatically makes the class unhashable in Python 3. +(even if a superclass defines a hash method).

-

When you define an __eq__ method for a class, remember to implement a __hash__ method or set -__hash__ = None.

+

+If a __hash__ method is defined, ensure a compatible __eq__ method is also defined. +

+ +

+To explicitly declare a class as unhashable, set __hash__ = None, rather than defining a __hash__ method that always +raises an exception. Otherwise, the class would be incorrectly identified as hashable by an isinstance(obj, collections.abc.Hashable) call. +

-

In the following example the Point class defines an equality method but -no hash method. If hash is called on this class then the hash method defined for object -is used. This is unlikely to give the required behavior. The PointUpdated class -is better as it defines both an equality and a hash method. -If Point was not to be used in dicts or sets, then it could be defined as -UnhashablePoint below. +

In the following example, the A class defines an hash method but +no equality method. Equality will be determined by object identity, which may not be the expected behaviour.

-

-To comply fully with the object model this class should also define an inequality method (identified -by a separate rule).

- +
  • Python Language Reference: object.__hash__.
  • -
  • Python Glossary: hashable.
  • +
  • Python Glossary: hashable.
  • diff --git a/python/ql/src/Classes/Comparisons/EqualsOrHash.ql b/python/ql/src/Classes/Comparisons/EqualsOrHash.ql index 4e73cef92fd2..54393cf1573f 100644 --- a/python/ql/src/Classes/Comparisons/EqualsOrHash.ql +++ b/python/ql/src/Classes/Comparisons/EqualsOrHash.ql @@ -1,6 +1,6 @@ /** * @name Inconsistent equality and hashing - * @description Defining equality for a class without also defining hashability (or vice-versa) violates the object model. + * @description Defining a hash operation without defining equality may be a mistake. * @kind problem * @tags quality * reliability diff --git a/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.ql b/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.ql index 25aafea6db2d..ea025f39c2fc 100644 --- a/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.ql +++ b/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.ql @@ -1,6 +1,6 @@ /** * @name Inconsistent equality and inequality - * @description Defining only an equality method or an inequality method for a class violates the object model. + * @description Class definitions of equality and inequality operators may be inconsistent. * @kind problem * @tags quality * reliability diff --git a/python/ql/src/Classes/Comparisons/IncompleteOrdering.qhelp b/python/ql/src/Classes/Comparisons/IncompleteOrdering.qhelp index 7983985ccee0..abb4faef59c3 100644 --- a/python/ql/src/Classes/Comparisons/IncompleteOrdering.qhelp +++ b/python/ql/src/Classes/Comparisons/IncompleteOrdering.qhelp @@ -3,32 +3,34 @@ "qhelp.dtd"> -

    A class that implements an ordering operator -(__lt__, __gt__, __le__ or __ge__) should implement -all four in order that ordering between two objects is consistent and obeys the usual mathematical rules. -If the ordering is inconsistent with default equality, then __eq__ and __ne__ -should also be implemented. +

    A class that implements the rich comparison operators +(__lt__, __gt__, __le__, or __ge__) should ensure that all four +comparison operations <, <=, >, and >= function as expected, consistent +with expected mathematical rules. +In Python 3, this is ensured by implementing one of __lt__ or __gt__, and one of __le__ or __ge__. +If the ordering is not consistent with default equality, then __eq__ should also be implemented.

    -

    Ensure that all four ordering comparisons are implemented as well as __eq__ and -__ne__ if required.

    +

    Ensure that at least one of __lt__ or __gt__ and at least one of __le__ or __ge__ is defined. +

    -

    It is not necessary to manually implement all four comparisons, -the functools.total_ordering class decorator can be used.

    +

    +The functools.total_ordering class decorator can be used to automatically implement all four comparison methods from a single one, +which is typically the cleanest way to ensure all necessary comparison methods are implemented consistently.

    -

    In this example only the __lt__ operator has been implemented which could lead to -inconsistent behavior. __gt__, __le__, __ge__, and in this case, -__eq__ and __ne__ should be implemented.

    - +

    In the following example, only the __lt__ operator has been implemented, which would lead to unexpected +errors if the <= or >= operators are used on A instances. +The __le__ method should also be defined, as well as __eq__ in this case.

    +
    -
  • Python Language Reference: Rich comparisons in Python.
  • +
  • Python Language Reference: Rich comparisons in Python.
  • diff --git a/python/ql/src/Classes/Comparisons/IncompleteOrdering.ql b/python/ql/src/Classes/Comparisons/IncompleteOrdering.ql index 2a09b2810585..e35f0c1a715f 100644 --- a/python/ql/src/Classes/Comparisons/IncompleteOrdering.ql +++ b/python/ql/src/Classes/Comparisons/IncompleteOrdering.ql @@ -1,6 +1,6 @@ /** * @name Incomplete ordering - * @description Class defines one or more ordering method but does not define all 4 ordering comparison methods + * @description Class defines ordering comparison methods, but does not define both strict and nonstrict ordering methods, to ensure all four comparison operators behave as expected. * @kind problem * @tags quality * reliability diff --git a/python/ql/src/Classes/Comparisons/examples/EqualsOrHash.py b/python/ql/src/Classes/Comparisons/examples/EqualsOrHash.py index e89c75b30ada..601ce2b18d0b 100644 --- a/python/ql/src/Classes/Comparisons/examples/EqualsOrHash.py +++ b/python/ql/src/Classes/Comparisons/examples/EqualsOrHash.py @@ -1,52 +1,8 @@ -# Incorrect: equality method defined but class contains no hash method -class Point(object): - - def __init__(self, x, y): - self._x = x - self._y = y - - def __repr__(self): - return 'Point(%r, %r)' % (self._x, self._y) - - def __eq__(self, other): - if not isinstance(other, Point): - return False - return self._x == other._x and self._y == other._y - - -# Improved: equality and hash method defined (inequality method still missing) -class PointUpdated(object): - - def __init__(self, x, y): - self._x = x - self._y = y - - def __repr__(self): - return 'Point(%r, %r)' % (self._x, self._y) - - def __eq__(self, other): - if not isinstance(other, Point): - return False - return self._x == other._x and self._y == other._y - - def __hash__(self): - return hash(self._x) ^ hash(self._y) - -# Improved: equality method defined and class instances made unhashable -class UnhashablePoint(object): - - def __init__(self, x, y): - self._x = x - self._y = y - - def __repr__(self): - return 'Point(%r, %r)' % (self._x, self._y) - - def __eq__(self, other): - if not isinstance(other, Point): - return False - return self._x == other._x and self._y == other._y - - #Tell the interpreter that instances of this class cannot be hashed - __hash__ = None - +class A: + def __init__(self, a, b): + self.a = a + self.b = b + + # No equality method is defined + def __hash__(self): + return hash((self.a, self.b)) diff --git a/python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals.py b/python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals.py index 32bc26d47370..080c9b8f6e47 100644 --- a/python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals.py +++ b/python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals.py @@ -31,26 +31,3 @@ def __ne__(self, other): # Improved: equality and inequality method defined (ha return not self == other - -class A: - def __init__(self, a): - self.a = a - - def __eq__(self, other): - print("A eq") - return self.a == other.a - - def __ne__(self, other): - print("A ne") - return self.a != other.a - -class B(A): - def __init__(self, a, b): - self.a = a - self.b = b - - def __eq__(self, other): - print("B eq") - return self.a == other.a and self.b == other.b - -print(B(1,2) != B(1,3)) diff --git a/python/ql/src/Classes/Comparisons/examples/IncompleteOrdering.py b/python/ql/src/Classes/Comparisons/examples/IncompleteOrdering.py index 78b306880b03..7ea0f0f82a7b 100644 --- a/python/ql/src/Classes/Comparisons/examples/IncompleteOrdering.py +++ b/python/ql/src/Classes/Comparisons/examples/IncompleteOrdering.py @@ -1,6 +1,8 @@ -class IncompleteOrdering(object): +class A: def __init__(self, i): self.i = i + # BAD: le is not defined, so `A(1) <= A(2) would result in an error.` def __lt__(self, other): - return self.i < other.i \ No newline at end of file + return self.i < other.i + \ No newline at end of file From ea48fcca8f55b76ed0383182734363b385c9b4cf Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Mon, 14 Jul 2025 10:49:28 +0100 Subject: [PATCH 10/15] Update doc for equalsNotEquals --- .../Comparisons/EqualsOrNotEquals.qhelp | 42 ++++++++++++------- .../Comparisons/IncompleteOrdering.qhelp | 3 +- .../Comparisons/examples/EqualsOrNotEquals.py | 33 --------------- .../examples/EqualsOrNotEquals1.py | 15 +++++++ .../examples/EqualsOrNotEquals2.py | 21 ++++++++++ 5 files changed, 66 insertions(+), 48 deletions(-) delete mode 100644 python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals.py create mode 100644 python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals1.py create mode 100644 python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals2.py diff --git a/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.qhelp b/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.qhelp index c49f3d2529ed..49e825d7ef49 100644 --- a/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.qhelp +++ b/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.qhelp @@ -4,33 +4,47 @@ -

    In order to conform to the object model, classes should define either no equality methods, or both -an equality and an inequality method. If only one of __eq__ or __ne__ is -defined then the method from the super class is used. This is unlikely to result in the expected -behavior.

    +

    In order to ensure the == and != operators behave consistently as expected (i.e. they should be negations of each other), care should be taken when implementing the +__eq__ and __ne__ special methods.

    + +

    In Python 3, if the __eq__ method is defined in a class while the __ne__ is not, +then the != operator will automatically delegate to the __eq__ method in the expected way. +

    + +

    However, if the __ne__ method is defined without a corresponding __eq__ method, + the == operator will still default to object identity (equivalent to the is operator), while the != + operator will use the __ne__ method, which may be inconsistent. + +

    Additionally, if the __ne__ method is defined on a superclass, and the subclass defines its own __eq__ method without overriding +the superclass __ne__ method, the != operator will use this superclass __ne__ method, rather than automatically delegating +to __eq__, which may be incorrect. -

    When you define an equality or an inequality method for a class, remember to implement both an -__eq__ method and an __ne__ method.

    +

    Ensure that when an __ne__ method is defined, the __eq__ method is also defined, and their results are consistent. +In most cases, the __ne__ method does not need to be defined at all, as the default behavior is to delegate to __eq__ and negate the result.

    -

    In the following example the PointOriginal class defines an equality method but -no inequality method. If this class is tested for inequality then a type error will be raised. The -PointUpdated class is better as it defines both an equality and an inequality method. To -comply fully with the object model this class should also define a hash method (identified by -a separate rule).

    +

    In the following example, A defines a __ne__ method, but not an __eq__ method. +This leads to inconsistent results between equality and inequality operators. +

    + + + +

    In the following example, C defines an __eq__ method, but its __ne__ implementation is inherited from B, +which is not consistent with the equality operation. +

    - +
    -
  • Python Language Reference: object.__ne__, -Comparisons.
  • +
  • Python Language Reference: object.__ne__, +Comparisons.
  • diff --git a/python/ql/src/Classes/Comparisons/IncompleteOrdering.qhelp b/python/ql/src/Classes/Comparisons/IncompleteOrdering.qhelp index abb4faef59c3..6bffaed7b87b 100644 --- a/python/ql/src/Classes/Comparisons/IncompleteOrdering.qhelp +++ b/python/ql/src/Classes/Comparisons/IncompleteOrdering.qhelp @@ -17,7 +17,8 @@ If the ordering is not consistent with default equality, then __eq__

    -The functools.total_ordering class decorator can be used to automatically implement all four comparison methods from a single one, +The functools.total_ordering class decorator can be used to automatically implement all four comparison methods from a +single one, which is typically the cleanest way to ensure all necessary comparison methods are implemented consistently.

    diff --git a/python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals.py b/python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals.py deleted file mode 100644 index 080c9b8f6e47..000000000000 --- a/python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals.py +++ /dev/null @@ -1,33 +0,0 @@ -class PointOriginal(object): - - def __init__(self, x, y): - self._x, x - self._y = y - - def __repr__(self): - return 'Point(%r, %r)' % (self._x, self._y) - - def __eq__(self, other): # Incorrect: equality is defined but inequality is not - if not isinstance(other, Point): - return False - return self._x == other._x and self._y == other._y - - -class PointUpdated(object): - - def __init__(self, x, y): - self._x, x - self._y = y - - def __repr__(self): - return 'Point(%r, %r)' % (self._x, self._y) - - def __eq__(self, other): - if not isinstance(other, Point): - return False - return self._x == other._x and self._y == other._y - - def __ne__(self, other): # Improved: equality and inequality method defined (hash method still missing) - return not self == other - - diff --git a/python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals1.py b/python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals1.py new file mode 100644 index 000000000000..2f749ebeb9e3 --- /dev/null +++ b/python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals1.py @@ -0,0 +1,15 @@ +class A: + def __init__(self, a): + self.a = a + + # BAD: ne is defined, but not eq. + def __ne__(self, other): + if not isinstance(other, A): + return NotImplemented + return self.a != other.a + +x = A(1) +y = A(1) + +print(x == y) # Prints False (potentially unexpected - object identity is used) +print(x != y) # Prints False diff --git a/python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals2.py b/python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals2.py new file mode 100644 index 000000000000..051108be9c55 --- /dev/null +++ b/python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals2.py @@ -0,0 +1,21 @@ +class B: + def __init__(self, b): + self.b = b + + def __eq__(self, other): + return self.b == other.b + + def __ne__(self, other): + return self.b != other.b + +class C(B): + def __init__(self, b, c): + super().init(b) + self.c = c + + # BAD: eq is defined, but != will use superclass ne method, which is not consistent + def __eq__(self, other): + return self.b == other.b and self.c == other.c + +print(C(1,2) == C(1,3)) # Prints False +print(C(1,2) != C(1,3)) # Prints False (potentially unexpected) \ No newline at end of file From 61af4e451484502a6ff651f3735b4196c2ce944b Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Mon, 14 Jul 2025 11:00:05 +0100 Subject: [PATCH 11/15] Add changenote and update integraion test output --- .../query-suite/python-code-quality-extended.qls.expected | 4 +++- .../query-suite/python-code-quality.qls.expected | 4 +++- python/ql/src/change-notes/2025-07-14-comparisons.md | 4 ++++ 3 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 python/ql/src/change-notes/2025-07-14-comparisons.md diff --git a/python/ql/integration-tests/query-suite/python-code-quality-extended.qls.expected b/python/ql/integration-tests/query-suite/python-code-quality-extended.qls.expected index 960972c508c8..cbc32fbd4ca7 100644 --- a/python/ql/integration-tests/query-suite/python-code-quality-extended.qls.expected +++ b/python/ql/integration-tests/query-suite/python-code-quality-extended.qls.expected @@ -1,6 +1,8 @@ +ql/python/ql/src/Classes/Comparisons/EqualsOrHash.ql +ql/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.ql +ql/python/ql/src/Classes/Comparisons/IncompleteOrdering.ql ql/python/ql/src/Classes/ConflictingAttributesInBaseClasses.ql ql/python/ql/src/Classes/DefineEqualsWhenAddingAttributes.ql -ql/python/ql/src/Classes/EqualsOrHash.ql ql/python/ql/src/Classes/InconsistentMRO.ql ql/python/ql/src/Classes/InitCallsSubclass/InitCallsSubclassMethod.ql ql/python/ql/src/Classes/MissingCallToDel.ql diff --git a/python/ql/integration-tests/query-suite/python-code-quality.qls.expected b/python/ql/integration-tests/query-suite/python-code-quality.qls.expected index 960972c508c8..cbc32fbd4ca7 100644 --- a/python/ql/integration-tests/query-suite/python-code-quality.qls.expected +++ b/python/ql/integration-tests/query-suite/python-code-quality.qls.expected @@ -1,6 +1,8 @@ +ql/python/ql/src/Classes/Comparisons/EqualsOrHash.ql +ql/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.ql +ql/python/ql/src/Classes/Comparisons/IncompleteOrdering.ql ql/python/ql/src/Classes/ConflictingAttributesInBaseClasses.ql ql/python/ql/src/Classes/DefineEqualsWhenAddingAttributes.ql -ql/python/ql/src/Classes/EqualsOrHash.ql ql/python/ql/src/Classes/InconsistentMRO.ql ql/python/ql/src/Classes/InitCallsSubclass/InitCallsSubclassMethod.ql ql/python/ql/src/Classes/MissingCallToDel.ql diff --git a/python/ql/src/change-notes/2025-07-14-comparisons.md b/python/ql/src/change-notes/2025-07-14-comparisons.md new file mode 100644 index 000000000000..a8a2bdacf316 --- /dev/null +++ b/python/ql/src/change-notes/2025-07-14-comparisons.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* The queries `py/incomplete-ordering`, `py/inconsistent-equality`, and `py/equals-hash-mismatch` have been modernized; no longer relying on outdated libraries, improved documentation, and no longer producing alerts for problems specific to Python 2. \ No newline at end of file From f784bb0a35ed785abad01968b999844db2d20732 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Mon, 14 Jul 2025 14:26:49 +0100 Subject: [PATCH 12/15] Fix qldoc errors + typos --- python/ql/src/Classes/Comparisons/EqualsOrNotEquals.qhelp | 4 +++- .../ql/src/Classes/Comparisons/examples/EqualsOrNotEquals2.py | 2 +- .../ql/src/Classes/Comparisons/examples/IncompleteOrdering.py | 2 +- python/ql/test/query-tests/Classes/equals-hash/equalsHash.py | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.qhelp b/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.qhelp index 49e825d7ef49..74f20d9f0c51 100644 --- a/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.qhelp +++ b/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.qhelp @@ -14,10 +14,12 @@ then the != operator will automatically delegate to the __eq_

    However, if the __ne__ method is defined without a corresponding __eq__ method, the == operator will still default to object identity (equivalent to the is operator), while the != operator will use the __ne__ method, which may be inconsistent. +

    -

    Additionally, if the __ne__ method is defined on a superclass, and the subclass defines its own __eq__ method without overriding +

    Additionally, if the __ne__ method is defined on a superclass, and the subclass defines its own __eq__ method without overriding the superclass __ne__ method, the != operator will use this superclass __ne__ method, rather than automatically delegating to __eq__, which may be incorrect. +

    diff --git a/python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals2.py b/python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals2.py index 051108be9c55..9b76a2536a58 100644 --- a/python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals2.py +++ b/python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals2.py @@ -10,7 +10,7 @@ def __ne__(self, other): class C(B): def __init__(self, b, c): - super().init(b) + super().__init__(b) self.c = c # BAD: eq is defined, but != will use superclass ne method, which is not consistent diff --git a/python/ql/src/Classes/Comparisons/examples/IncompleteOrdering.py b/python/ql/src/Classes/Comparisons/examples/IncompleteOrdering.py index 7ea0f0f82a7b..5a18e3936209 100644 --- a/python/ql/src/Classes/Comparisons/examples/IncompleteOrdering.py +++ b/python/ql/src/Classes/Comparisons/examples/IncompleteOrdering.py @@ -2,7 +2,7 @@ class A: def __init__(self, i): self.i = i - # BAD: le is not defined, so `A(1) <= A(2) would result in an error.` + # BAD: le is not defined, so `A(1) <= A(2)` would result in an error. def __lt__(self, other): return self.i < other.i \ No newline at end of file diff --git a/python/ql/test/query-tests/Classes/equals-hash/equalsHash.py b/python/ql/test/query-tests/Classes/equals-hash/equalsHash.py index 6b3ec5d2b02a..c9e1e47350f8 100644 --- a/python/ql/test/query-tests/Classes/equals-hash/equalsHash.py +++ b/python/ql/test/query-tests/Classes/equals-hash/equalsHash.py @@ -2,7 +2,7 @@ class A: def __eq__(self, other): return True - def __hash__(self, other): + def __hash__(self): return 7 # B is automatically non-hashable - so eq without hash never needs to alert From 0f04a8b2c0eacfba575a0cc9ae41f3e38c5b3721 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Mon, 14 Jul 2025 14:35:12 +0100 Subject: [PATCH 13/15] Update integration test output --- .../query-suite/python-security-and-quality.qls.expected | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/python/ql/integration-tests/query-suite/python-security-and-quality.qls.expected b/python/ql/integration-tests/query-suite/python-security-and-quality.qls.expected index 170d9f442f92..c7e6e0caad5f 100644 --- a/python/ql/integration-tests/query-suite/python-security-and-quality.qls.expected +++ b/python/ql/integration-tests/query-suite/python-security-and-quality.qls.expected @@ -1,8 +1,8 @@ +ql/python/ql/src/Classes/Comparisons/EqualsOrHash.ql +ql/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.ql +ql/python/ql/src/Classes/Comparisons/IncompleteOrdering.ql ql/python/ql/src/Classes/ConflictingAttributesInBaseClasses.ql ql/python/ql/src/Classes/DefineEqualsWhenAddingAttributes.ql -ql/python/ql/src/Classes/EqualsOrHash.ql -ql/python/ql/src/Classes/EqualsOrNotEquals.ql -ql/python/ql/src/Classes/IncompleteOrdering.ql ql/python/ql/src/Classes/InconsistentMRO.ql ql/python/ql/src/Classes/InitCallsSubclass/InitCallsSubclassMethod.ql ql/python/ql/src/Classes/MissingCallToDel.ql From 15115f50c1914acce65cd1ee786bc831fe6345a9 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Tue, 15 Jul 2025 09:50:21 +0100 Subject: [PATCH 14/15] Remove old tests --- .../Classes/equals-hash/EqualsOrHash.expected | 1 - .../Classes/equals-hash/EqualsOrHash.qlref | 1 - .../Classes/equals-hash/equals_hash.py | 63 ------------------- .../equals-ne/EqualsOrNotEquals.expected | 1 - .../Classes/equals-ne/EqualsOrNotEquals.qlref | 1 - .../3/query-tests/Classes/equals-ne/test.py | 10 --- 6 files changed, 77 deletions(-) delete mode 100644 python/ql/test/3/query-tests/Classes/equals-hash/EqualsOrHash.expected delete mode 100644 python/ql/test/3/query-tests/Classes/equals-hash/EqualsOrHash.qlref delete mode 100644 python/ql/test/3/query-tests/Classes/equals-hash/equals_hash.py delete mode 100644 python/ql/test/3/query-tests/Classes/equals-ne/EqualsOrNotEquals.expected delete mode 100644 python/ql/test/3/query-tests/Classes/equals-ne/EqualsOrNotEquals.qlref delete mode 100644 python/ql/test/3/query-tests/Classes/equals-ne/test.py diff --git a/python/ql/test/3/query-tests/Classes/equals-hash/EqualsOrHash.expected b/python/ql/test/3/query-tests/Classes/equals-hash/EqualsOrHash.expected deleted file mode 100644 index 87cf5d1e4645..000000000000 --- a/python/ql/test/3/query-tests/Classes/equals-hash/EqualsOrHash.expected +++ /dev/null @@ -1 +0,0 @@ -| equals_hash.py:24:5:24:23 | Function Hash.__hash__ | Class $@ implements __hash__ but does not define __eq__. | equals_hash.py:19:1:19:19 | class Hash | Hash | diff --git a/python/ql/test/3/query-tests/Classes/equals-hash/EqualsOrHash.qlref b/python/ql/test/3/query-tests/Classes/equals-hash/EqualsOrHash.qlref deleted file mode 100644 index 7eb0f07e51cc..000000000000 --- a/python/ql/test/3/query-tests/Classes/equals-hash/EqualsOrHash.qlref +++ /dev/null @@ -1 +0,0 @@ -Classes/EqualsOrHash.ql \ No newline at end of file diff --git a/python/ql/test/3/query-tests/Classes/equals-hash/equals_hash.py b/python/ql/test/3/query-tests/Classes/equals-hash/equals_hash.py deleted file mode 100644 index d5a58d0b78c2..000000000000 --- a/python/ql/test/3/query-tests/Classes/equals-hash/equals_hash.py +++ /dev/null @@ -1,63 +0,0 @@ -#Equals and hash - -class Eq(object): - - def __init__(self, data): - self.data = data - - def __eq__(self, other): - return self.data == other.data - -class Ne(object): - - def __init__(self, data): - self.data = data - - def __ne__(self, other): - return self.data != other.data - -class Hash(object): - - def __init__(self, data): - self.data = data - - def __hash__(self): - return hash(self.data) - -class Unhashable1(object): - - __hash__ = None - - -class EqOK1(Unhashable1): - - def __eq__(self, other): - return False - - def __ne__(self, other): - return True - -class Unhashable2(object): - - #Not the idiomatic way of doing it, but not uncommon either - def __hash__(self): - raise TypeError("unhashable object") - - -class EqOK2(Unhashable2): - - def __eq__(self, other): - return False - - def __ne__(self, other): - return True - -class ReflectiveNotEquals(object): - - def __ne__(self, other): - return not self == other - -class EqOK3(ReflectiveNotEquals, Unhashable1): - - def __eq__(self, other): - return self.data == other.data diff --git a/python/ql/test/3/query-tests/Classes/equals-ne/EqualsOrNotEquals.expected b/python/ql/test/3/query-tests/Classes/equals-ne/EqualsOrNotEquals.expected deleted file mode 100644 index 7e9c94581207..000000000000 --- a/python/ql/test/3/query-tests/Classes/equals-ne/EqualsOrNotEquals.expected +++ /dev/null @@ -1 +0,0 @@ -| test.py:9:5:9:28 | Function NotOK2.__ne__ | Class $@ implements __ne__ but does not implement __eq__. | test.py:7:1:7:13 | class NotOK2 | NotOK2 | diff --git a/python/ql/test/3/query-tests/Classes/equals-ne/EqualsOrNotEquals.qlref b/python/ql/test/3/query-tests/Classes/equals-ne/EqualsOrNotEquals.qlref deleted file mode 100644 index 163a9f3b6675..000000000000 --- a/python/ql/test/3/query-tests/Classes/equals-ne/EqualsOrNotEquals.qlref +++ /dev/null @@ -1 +0,0 @@ -Classes/EqualsOrNotEquals.ql \ No newline at end of file diff --git a/python/ql/test/3/query-tests/Classes/equals-ne/test.py b/python/ql/test/3/query-tests/Classes/equals-ne/test.py deleted file mode 100644 index 15097820bf46..000000000000 --- a/python/ql/test/3/query-tests/Classes/equals-ne/test.py +++ /dev/null @@ -1,10 +0,0 @@ - -class OK: - - def __eq__(self, other): - return False - -class NotOK2: - - def __ne__(self, other): - return True From 3a27758d858c9ce6fa53940f87522d4afec6139c Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Tue, 15 Jul 2025 13:38:48 +0100 Subject: [PATCH 15/15] Remove old py2-specific tests --- .../Classes/equals-hash/EqualsOrHash.expected | 2 - .../Classes/equals-hash/EqualsOrHash.qlref | 1 - .../equals-hash/EqualsOrNotEquals.expected | 2 - .../equals-hash/EqualsOrNotEquals.qlref | 1 - .../Classes/equals-hash/equals_hash.py | 63 ------------------- 5 files changed, 69 deletions(-) delete mode 100644 python/ql/test/2/query-tests/Classes/equals-hash/EqualsOrHash.expected delete mode 100644 python/ql/test/2/query-tests/Classes/equals-hash/EqualsOrHash.qlref delete mode 100644 python/ql/test/2/query-tests/Classes/equals-hash/EqualsOrNotEquals.expected delete mode 100644 python/ql/test/2/query-tests/Classes/equals-hash/EqualsOrNotEquals.qlref delete mode 100644 python/ql/test/2/query-tests/Classes/equals-hash/equals_hash.py diff --git a/python/ql/test/2/query-tests/Classes/equals-hash/EqualsOrHash.expected b/python/ql/test/2/query-tests/Classes/equals-hash/EqualsOrHash.expected deleted file mode 100644 index 916a9bb4454b..000000000000 --- a/python/ql/test/2/query-tests/Classes/equals-hash/EqualsOrHash.expected +++ /dev/null @@ -1,2 +0,0 @@ -| equals_hash.py:8:5:8:28 | Function Eq.__eq__ | Class $@ implements __eq__ but does not define __hash__. | equals_hash.py:3:1:3:17 | class Eq | Eq | -| equals_hash.py:24:5:24:23 | Function Hash.__hash__ | Class $@ implements __hash__ but does not define __eq__ or __cmp__. | equals_hash.py:19:1:19:19 | class Hash | Hash | diff --git a/python/ql/test/2/query-tests/Classes/equals-hash/EqualsOrHash.qlref b/python/ql/test/2/query-tests/Classes/equals-hash/EqualsOrHash.qlref deleted file mode 100644 index 7eb0f07e51cc..000000000000 --- a/python/ql/test/2/query-tests/Classes/equals-hash/EqualsOrHash.qlref +++ /dev/null @@ -1 +0,0 @@ -Classes/EqualsOrHash.ql \ No newline at end of file diff --git a/python/ql/test/2/query-tests/Classes/equals-hash/EqualsOrNotEquals.expected b/python/ql/test/2/query-tests/Classes/equals-hash/EqualsOrNotEquals.expected deleted file mode 100644 index 04e395c668bb..000000000000 --- a/python/ql/test/2/query-tests/Classes/equals-hash/EqualsOrNotEquals.expected +++ /dev/null @@ -1,2 +0,0 @@ -| equals_hash.py:8:5:8:28 | Function Eq.__eq__ | Class $@ implements __eq__ but does not implement __ne__. | equals_hash.py:3:1:3:17 | class Eq | Eq | -| equals_hash.py:16:5:16:28 | Function Ne.__ne__ | Class $@ implements __ne__ but does not implement __eq__. | equals_hash.py:11:1:11:17 | class Ne | Ne | diff --git a/python/ql/test/2/query-tests/Classes/equals-hash/EqualsOrNotEquals.qlref b/python/ql/test/2/query-tests/Classes/equals-hash/EqualsOrNotEquals.qlref deleted file mode 100644 index 163a9f3b6675..000000000000 --- a/python/ql/test/2/query-tests/Classes/equals-hash/EqualsOrNotEquals.qlref +++ /dev/null @@ -1 +0,0 @@ -Classes/EqualsOrNotEquals.ql \ No newline at end of file diff --git a/python/ql/test/2/query-tests/Classes/equals-hash/equals_hash.py b/python/ql/test/2/query-tests/Classes/equals-hash/equals_hash.py deleted file mode 100644 index 447250a5375c..000000000000 --- a/python/ql/test/2/query-tests/Classes/equals-hash/equals_hash.py +++ /dev/null @@ -1,63 +0,0 @@ -#Equals and hash - -class Eq(object): - - def __init__(self, data): - self.data = data - - def __eq__(self, other): - return self.data == other.data - -class Ne(object): - - def __init__(self, data): - self.data = data - - def __ne__(self, other): - return self.data != other.data - -class Hash(object): - - def __init__(self, data): - self.data = data - - def __hash__(self): - return hash(self.data) - -class Unhashable1(object): - - __hash__ = None - - -class EqOK1(Unhashable1): - - def __eq__(self, other): - return False - - def __ne__(self, other): - return True - -class Unhashable2(object): - - #Not the idiomatic way of doing it, but not uncommon either - def __hash__(self): - raise TypeError("unhashable object") - - -class EqOK2(Unhashable2): - - def __eq__(self, other): - return False - - def __ne__(self, other): - return True - -class ReflectiveNotEquals(object): - - def __ne__(self, other): - return not self == other - -class EqOK3(ReflectiveNotEquals, Unhashable1): - - def __eq__(self, other): - return self.data == other.data