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 dicts or sets, 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