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 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