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 pFad - Phonifier reborn

    Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

    Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


    Alternative Proxies:

    Alternative Proxy

    pFad Proxy

    pFad v3 Proxy

    pFad v4 Proxy