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/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
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/EqualsOrHash.qhelp b/python/ql/src/Classes/Comparisons/EqualsOrHash.qhelp
new file mode 100644
index 000000000000..562ad7be1bd6
--- /dev/null
+++ b/python/ql/src/Classes/Comparisons/EqualsOrHash.qhelp
@@ -0,0 +1,44 @@
+
+
+
+
+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.
+
+
+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).
+
+
+
+
+
+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 A
class defines an hash method but
+no equality method. Equality will be determined by object identity, which may not be the expected behaviour.
+
+
+
+
+
+
+
+
+Python Language Reference: object.__hash__.
+Python Glossary: hashable.
+
+
+
+
diff --git a/python/ql/src/Classes/Comparisons/EqualsOrHash.ql b/python/ql/src/Classes/Comparisons/EqualsOrHash.ql
new file mode 100644
index 000000000000..54393cf1573f
--- /dev/null
+++ b/python/ql/src/Classes/Comparisons/EqualsOrHash.ql
@@ -0,0 +1,26 @@
+/**
+ * @name Inconsistent equality and hashing
+ * @description Defining a hash operation without defining equality may be a mistake.
+ * @kind problem
+ * @tags quality
+ * reliability
+ * correctness
+ * external/cwe/cwe-581
+ * @problem.severity warning
+ * @sub-severity high
+ * @precision very-high
+ * @id py/equals-hash-mismatch
+ */
+
+import python
+
+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.
+}
+
+from Class cls, Function defined
+where missingEquality(cls, defined)
+select cls, "This class implements $@, but does not implement __eq__.", defined, defined.getName()
diff --git a/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.qhelp b/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.qhelp
new file mode 100644
index 000000000000..74f20d9f0c51
--- /dev/null
+++ b/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.qhelp
@@ -0,0 +1,53 @@
+
+
+
+
+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.
+
+
+
+
+
+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, 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.
+
+
+
+
diff --git a/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.ql b/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.ql
new file mode 100644
index 000000000000..ea025f39c2fc
--- /dev/null
+++ b/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.ql
@@ -0,0 +1,37 @@
+/**
+ * @name Inconsistent equality and inequality
+ * @description Class definitions of equality and inequality operators may be inconsistent.
+ * @kind problem
+ * @tags quality
+ * reliability
+ * correctness
+ * @problem.severity warning
+ * @sub-severity high
+ * @precision very-high
+ * @id py/inconsistent-equality
+ */
+
+import python
+import semmle.python.dataflow.new.internal.DataFlowDispatch
+import Classes.Equality
+
+predicate missingEquality(Class cls, Function defined, string missing) {
+ defined = cls.getMethod("__ne__") and
+ not exists(cls.getMethod("__eq__")) and
+ missing = "__eq__"
+ or
+ // 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 Class cls, Function defined, string 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.qhelp b/python/ql/src/Classes/Comparisons/IncompleteOrdering.qhelp
new file mode 100644
index 000000000000..6bffaed7b87b
--- /dev/null
+++ b/python/ql/src/Classes/Comparisons/IncompleteOrdering.qhelp
@@ -0,0 +1,38 @@
+
+
+
+ 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 at least one of __lt__
or __gt__
and at least one of __le__
or __ge__
is defined.
+
+
+
+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 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.
+
+
+
+
diff --git a/python/ql/src/Classes/Comparisons/IncompleteOrdering.ql b/python/ql/src/Classes/Comparisons/IncompleteOrdering.ql
new file mode 100644
index 000000000000..e35f0c1a715f
--- /dev/null
+++ b/python/ql/src/Classes/Comparisons/IncompleteOrdering.ql
@@ -0,0 +1,55 @@
+/**
+ * @name Incomplete ordering
+ * @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
+ * correctness
+ * @problem.severity warning
+ * @sub-severity low
+ * @precision very-high
+ * @id py/incomplete-ordering
+ */
+
+import python
+import semmle.python.dataflow.new.internal.DataFlowDispatch
+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()))
+}
+
+predicate definesStrictOrdering(Class cls, Function meth) {
+ meth = cls.getMethod("__lt__")
+ or
+ not exists(cls.getMethod("__lt__")) and
+ meth = cls.getMethod("__gt__")
+}
+
+predicate definesNonStrictOrdering(Class cls, Function meth) {
+ meth = cls.getMethod("__le__")
+ or
+ not exists(cls.getMethod("__le__")) and
+ meth = cls.getMethod("__ge__")
+}
+
+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 Class cls, Function defined, string missing
+where
+ not totalOrdering(cls) and
+ missingComparison(cls, defined, missing)
+select cls, "This class implements $@, but does not implement " + missing + ".", defined,
+ defined.getName()
diff --git a/python/ql/src/Classes/Comparisons/examples/EqualsOrHash.py b/python/ql/src/Classes/Comparisons/examples/EqualsOrHash.py
new file mode 100644
index 000000000000..601ce2b18d0b
--- /dev/null
+++ b/python/ql/src/Classes/Comparisons/examples/EqualsOrHash.py
@@ -0,0 +1,8 @@
+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/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..9b76a2536a58
--- /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
diff --git a/python/ql/src/Classes/Comparisons/examples/IncompleteOrdering.py b/python/ql/src/Classes/Comparisons/examples/IncompleteOrdering.py
new file mode 100644
index 000000000000..5a18e3936209
--- /dev/null
+++ b/python/ql/src/Classes/Comparisons/examples/IncompleteOrdering.py
@@ -0,0 +1,8 @@
+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
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__"
+ )
)
}
}
diff --git a/python/ql/src/Classes/EqualsOrHash.py b/python/ql/src/Classes/EqualsOrHash.py
deleted file mode 100644
index e89c75b30ada..000000000000
--- a/python/ql/src/Classes/EqualsOrHash.py
+++ /dev/null
@@ -1,52 +0,0 @@
-# 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
-
diff --git a/python/ql/src/Classes/EqualsOrHash.qhelp b/python/ql/src/Classes/EqualsOrHash.qhelp
deleted file mode 100644
index 28579a095f70..000000000000
--- a/python/ql/src/Classes/EqualsOrHash.qhelp
+++ /dev/null
@@ -1,46 +0,0 @@
-
-
-
-
-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 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.
-
-
-
-
-When you define an __eq__
method for a class, remember to implement a __hash__
method or set
-__hash__ = None
.
-
-
-
-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 dict
s or set
s, then it could be defined as
-UnhashablePoint
below.
-
-
-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.
-
-
-
-
diff --git a/python/ql/src/Classes/EqualsOrHash.ql b/python/ql/src/Classes/EqualsOrHash.ql
deleted file mode 100644
index 4c8cf2c11699..000000000000
--- a/python/ql/src/Classes/EqualsOrHash.ql
+++ /dev/null
@@ -1,63 +0,0 @@
-/**
- * @name Inconsistent equality and hashing
- * @description Defining equality for a class without also defining hashability (or vice-versa) violates the object model.
- * @kind problem
- * @tags quality
- * reliability
- * correctness
- * external/cwe/cwe-581
- * @problem.severity warning
- * @sub-severity high
- * @precision very-high
- * @id py/equals-hash-mismatch
- */
-
-import python
-
-CallableValue defines_equality(ClassValue c, string name) {
- (
- name = "__eq__"
- or
- major_version() = 2 and name = "__cmp__"
- ) and
- result = c.declaredAttribute(name)
-}
-
-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()
diff --git a/python/ql/src/Classes/EqualsOrNotEquals.py b/python/ql/src/Classes/EqualsOrNotEquals.py
deleted file mode 100644
index 7e1ece7685c5..000000000000
--- a/python/ql/src/Classes/EqualsOrNotEquals.py
+++ /dev/null
@@ -1,32 +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/EqualsOrNotEquals.qhelp b/python/ql/src/Classes/EqualsOrNotEquals.qhelp
deleted file mode 100644
index c49f3d2529ed..000000000000
--- a/python/ql/src/Classes/EqualsOrNotEquals.qhelp
+++ /dev/null
@@ -1,37 +0,0 @@
-
-
-
-
-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.
-
-
-
-
-When you define an equality or an inequality method for a class, remember to implement both an
-__eq__
method and an __ne__
method.
-
-
-
-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).
-
-
-
-
-
-
-
-Python Language Reference: object.__ne__,
-Comparisons.
-
-
-
-
diff --git a/python/ql/src/Classes/EqualsOrNotEquals.ql b/python/ql/src/Classes/EqualsOrNotEquals.ql
deleted file mode 100644
index adac5a20e87a..000000000000
--- a/python/ql/src/Classes/EqualsOrNotEquals.ql
+++ /dev/null
@@ -1,48 +0,0 @@
-/**
- * @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
- * correctness
- * @problem.severity warning
- * @sub-severity high
- * @precision very-high
- * @id py/inconsistent-equality
- */
-
-import python
-import 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")
- 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
-}
-
-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()
diff --git a/python/ql/src/Classes/IncompleteOrdering.py b/python/ql/src/Classes/IncompleteOrdering.py
deleted file mode 100644
index 78b306880b03..000000000000
--- a/python/ql/src/Classes/IncompleteOrdering.py
+++ /dev/null
@@ -1,6 +0,0 @@
-class IncompleteOrdering(object):
- def __init__(self, i):
- self.i = i
-
- def __lt__(self, other):
- return self.i < other.i
\ No newline at end of file
diff --git a/python/ql/src/Classes/IncompleteOrdering.qhelp b/python/ql/src/Classes/IncompleteOrdering.qhelp
deleted file mode 100644
index 7983985ccee0..000000000000
--- a/python/ql/src/Classes/IncompleteOrdering.qhelp
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
- 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.
-
-
-
-
-Ensure that all four ordering comparisons are implemented as well as __eq__
and
-__ne__
if required.
-
-It is not necessary to manually implement all four comparisons,
-the functools.total_ordering
class decorator can be used.
-
-
-
-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.
-
-
-
-
-
- Python Language Reference: Rich comparisons in Python.
-
-
-
-
diff --git a/python/ql/src/Classes/IncompleteOrdering.ql b/python/ql/src/Classes/IncompleteOrdering.ql
deleted file mode 100644
index d6cd1230ece6..000000000000
--- a/python/ql/src/Classes/IncompleteOrdering.ql
+++ /dev/null
@@ -1,73 +0,0 @@
-/**
- * @name Incomplete ordering
- * @description Class defines one or more ordering method but does not define all 4 ordering comparison methods
- * @kind problem
- * @tags reliability
- * correctness
- * @problem.severity warning
- * @sub-severity low
- * @precision very-high
- * @id py/incomplete-ordering
- */
-
-import python
-
-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 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)
- )
- )
-}
-
-string unimplemented_ordering(ClassValue c, int n) {
- not c = Value::named("object") and
- not overrides_ordering_method(c, result) and
- result = ordering_name(n)
-}
-
-string unimplemented_ordering_methods(ClassValue c, int n) {
- n = 0 and result = "" and exists(unimplemented_ordering(c, _))
- 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)
- )
-}
-
-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)
-}
-
-from ClassValue c, Value ordering, string name
-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
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
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
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
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
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..c9e1e47350f8
--- /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):
+ 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
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