-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Python: Modernize 3 quality queries for comparison methods #20038
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
joefarebrother
wants to merge
15
commits into
github:main
Choose a base branch
from
joefarebrother:python-qual-comparison
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+549
−556
Open
Changes from 11 commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
fccdc30
Modernize incomplete ordering query
joefarebrother e71af8f
Move to subfolder
joefarebrother 4c5c4e0
Move inconsistentEquality and equals-hash-mismatch to subfolder
joefarebrother eb1b5a3
Modernize inconsistent equality
joefarebrother a687b60
Modernise equals-hash-mismatch
joefarebrother 8fb9bdd
move equals attr test to equals attr folder
joefarebrother 083d258
Add/update unit tests
joefarebrother 843a6c8
Remove total order check from equals not equals (doesn't make sense t…
joefarebrother 58f503d
Update docs for incomplete ordering + inconsistent hashing
joefarebrother ea48fcc
Update doc for equalsNotEquals
joefarebrother 61af4e4
Add changenote and update integraion test output
joefarebrother f784bb0
Fix qldoc errors + typos
joefarebrother 0f04a8b
Update integration test output
joefarebrother 15115f5
Remove old tests
joefarebrother 3a27758
Remove old py2-specific tests
joefarebrother File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
4 changes: 3 additions & 1 deletion
4
python/ql/integration-tests/query-suite/python-code-quality-extended.qls.expected
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
4 changes: 3 additions & 1 deletion
4
python/ql/integration-tests/query-suite/python-code-quality.qls.expected
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
<!DOCTYPE qhelp PUBLIC | ||
"-//Semmle//qhelp//EN" | ||
"qhelp.dtd"> | ||
<qhelp> | ||
|
||
<overview> | ||
<p>A hashable class has an <code>__eq__</code> method, and a <code>__hash__</code> 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. | ||
</p> | ||
|
||
<p>Note that defining an <code>__eq__</code> method without defining a <code>__hash__</code> method automatically makes the class unhashable in Python 3. | ||
(even if a superclass defines a hash method).</p> | ||
|
||
</overview> | ||
<recommendation> | ||
|
||
<p> | ||
If a <code>__hash__</code> method is defined, ensure a compatible <code>__eq__</code> method is also defined. | ||
</p> | ||
|
||
<p> | ||
To explicitly declare a class as unhashable, set <code>__hash__ = None</code>, rather than defining a <code>__hash__</code> method that always | ||
raises an exception. Otherwise, the class would be incorrectly identified as hashable by an <code>isinstance(obj, collections.abc.Hashable)</code> call. | ||
</p> | ||
|
||
</recommendation> | ||
<example> | ||
<p>In the following example, the <code>A</code> class defines an hash method but | ||
no equality method. Equality will be determined by object identity, which may not be the expected behaviour. | ||
</p> | ||
|
||
<sample src="examples/EqualsOrHash.py" /> | ||
|
||
</example> | ||
<references> | ||
|
||
|
||
<li>Python Language Reference: <a href="http://docs.python.org/reference/datamodel.html#object.__hash__">object.__hash__</a>.</li> | ||
<li>Python Glossary: <a href="http://docs.python.org/3/glossary.html#term-hashable">hashable</a>.</li> | ||
|
||
|
||
</references> | ||
</qhelp> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
<!DOCTYPE qhelp PUBLIC | ||
"-//Semmle//qhelp//EN" | ||
"qhelp.dtd"> | ||
<qhelp> | ||
|
||
<overview> | ||
<p>In order to ensure the <code>==</code> and <code>!=</code> operators behave consistently as expected (i.e. they should be negations of each other), care should be taken when implementing the | ||
<code>__eq__</code> and <code>__ne__</code> special methods.</p> | ||
|
||
<p>In Python 3, if the <code>__eq__</code> method is defined in a class while the <code>__ne__</code> is not, | ||
then the <code>!=</code> operator will automatically delegate to the <code>__eq__</code> method in the expected way. | ||
</p> | ||
|
||
<p>However, if the <code>__ne__</code> method is defined without a corresponding <code>__eq__</code> method, | ||
the <code>==</code> operator will still default to object identity (equivalent to the <code>is</code> operator), while the <code>!=</code> | ||
operator will use the <code>__ne__</code> method, which may be inconsistent. | ||
|
||
<p>Additionally, if the <code>__ne__</code> method is defined on a superclass, and the subclass defines its own <cde>__eq__</code> method without overriding | ||
the superclass <code>__ne__</code> method, the <code>!=</code> operator will use this superclass <code>__ne__</code> method, rather than automatically delegating | ||
to <code>__eq__</code>, which may be incorrect. | ||
|
||
</overview> | ||
<recommendation> | ||
|
||
<p>Ensure that when an <code>__ne__</code> method is defined, the <code>__eq__</code> method is also defined, and their results are consistent. | ||
In most cases, the <code>__ne__</code> method does not need to be defined at all, as the default behavior is to delegate to <code>__eq__</code> and negate the result. </p> | ||
|
||
</recommendation> | ||
<example> | ||
<p>In the following example, <code>A</code> defines a <code>__ne__</code> method, but not an <code>__eq__</code> method. | ||
This leads to inconsistent results between equality and inequality operators. | ||
</p> | ||
|
||
<sample src="examples/EqualsOrNotEquals1.py" /> | ||
|
||
<p>In the following example, <code>C</code> defines an <code>__eq__</code> method, but its <code>__ne__</code> implementation is inherited from <code>B</code>, | ||
which is not consistent with the equality operation. | ||
</p> | ||
|
||
<sample src="examples/EqualsOrNotEquals2.py" /> | ||
|
||
</example> | ||
<references> | ||
|
||
|
||
<li>Python Language Reference: <a href="http://docs.python.org/3/reference/datamodel.html#object.__ne__">object.__ne__</a>, | ||
<a href="http://docs.python.org/3/reference/expressions.html#comparisons">Comparisons</a>.</li> | ||
|
||
|
||
</references> | ||
</qhelp> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() |
38 changes: 38 additions & 0 deletions
38
python/ql/src/Classes/Comparisons/IncompleteOrdering.qhelp
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
<!DOCTYPE qhelp PUBLIC | ||
"-//Semmle//qhelp//EN" | ||
"qhelp.dtd"> | ||
<qhelp> | ||
<overview> | ||
<p> A class that implements the rich comparison operators | ||
(<code>__lt__</code>, <code>__gt__</code>, <code>__le__</code>, or <code>__ge__</code>) should ensure that all four | ||
comparison operations <code><</code>, <code><=</code>, <code>></code>, and <code>>=</code> function as expected, consistent | ||
with expected mathematical rules. | ||
In Python 3, this is ensured by implementing one of <code>__lt__</code> or <code>__gt__</code>, and one of <code>__le__</code> or <code>__ge__</code>. | ||
If the ordering is not consistent with default equality, then <code>__eq__</code> should also be implemented. | ||
</p> | ||
|
||
</overview> | ||
<recommendation> | ||
<p>Ensure that at least one of <code>__lt__</code> or <code>__gt__</code> and at least one of <code>__le__</code> or <code>__ge__</code> is defined. | ||
</p> | ||
|
||
<p> | ||
The <code>functools.total_ordering</code> 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.</p> | ||
|
||
</recommendation> | ||
<example> | ||
<p>In the following example, only the <code>__lt__</code> operator has been implemented, which would lead to unexpected | ||
errors if the <code><=</code> or <code>>=</code> operators are used on <code>A</code> instances. | ||
The <code>__le__</code> method should also be defined, as well as <code>__eq__</code> in this case.</p> | ||
<sample src="examples/IncompleteOrdering.py" /> | ||
|
||
</example> | ||
<references> | ||
|
||
<li>Python Language Reference: <a href="http://docs.python.org/3/reference/datamodel.html#object.__lt__">Rich comparisons in Python</a>.</li> | ||
|
||
|
||
</references> | ||
</qhelp> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)) |
15 changes: 15 additions & 0 deletions
15
python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals1.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
21 changes: 21 additions & 0 deletions
21
python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals2.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
joefarebrother marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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) |
8 changes: 8 additions & 0 deletions
8
python/ql/src/Classes/Comparisons/examples/IncompleteOrdering.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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.` | ||
joefarebrother marked this conversation as resolved.
Show resolved
Hide resolved
|
||
def __lt__(self, other): | ||
return self.i < other.i | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.