Skip to content

Commit e04dea1

Browse files
Merge pull request #19554 from joefarebrother/python-qual-iter-not-return-self
Python: Modernize iter not returning self query
2 parents 0906d85 + 73f2770 commit e04dea1

File tree

9 files changed

+142
-30
lines changed

9 files changed

+142
-30
lines changed

python/ql/integration-tests/query-suite/python-code-quality.qls.expected

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
ql/python/ql/src/Functions/IterReturnsNonSelf.ql
12
ql/python/ql/src/Functions/NonCls.ql
23
ql/python/ql/src/Functions/NonSelf.ql
34
ql/python/ql/src/Functions/ReturnConsistentTupleSizes.ql

python/ql/src/Functions/IterReturnsNonSelf.qhelp

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,27 @@
33
"qhelp.dtd">
44
<qhelp>
55
<overview>
6-
<p>The <code>__iter__</code> method of an iterator should return self.
7-
This is important so that iterators can be used as sequences in any context
8-
that expect a sequence. To do so requires that <code>__iter__</code> is
9-
idempotent on iterators.</p>
10-
11-
<p>
12-
Note that sequences and mapping should return a new iterator, it is just the returned
13-
iterator that must obey this constraint.
6+
<p>Iterator classes (classes defining a <code>__next__</code> method) should have an <code>__iter__</code> method that returns the iterator itself.
7+
This ensures that the object is also an iterable; and behaves as expected when used anywhere an iterator or iterable is expected, such as in <code>for</code> loops.
148
</p>
159

10+
11+
1612
</overview>
1713
<recommendation>
18-
<p>Make the <code>__iter__</code> return self unless the class should not be an iterator,
19-
in which case rename the <code>next</code> (Python 2) or <code>__next__</code> (Python 3)
20-
to something else.</p>
14+
<p>Ensure that the <code>__iter__</code> method returns <code>self</code>, or is otherwise equivalent as an iterator to <code>self</code>.</p>
2115

2216
</recommendation>
2317
<example>
24-
<p>In this example the <code>Counter</code> class's <code>__iter__</code> method does not
25-
return self (or even an iterator). This will cause the program to fail when anyone attempts
26-
to use the iterator in a <code>for</code> loop or <code>in</code> statement.</p>
27-
<sample src="IterReturnsNonSelf.py" />
18+
<p>In the following example, the <code>MyRange</code> class's <code>__iter__</code> method does not return <code>self</code>.
19+
This would lead to unexpected results when used with a <code>for</code> loop or <code>in</code> statement.</p>
20+
<sample src="examples/IterReturnsNonSelf.py" />
2821

2922
</example>
3023
<references>
3124

32-
<li>Python Language Reference: <a href="http://docs.python.org/2.7/reference/datamodel.html#object.__iter__">object.__iter__</a>.</li>
33-
<li>Python Standard Library: <a href="http://docs.python.org/2/library/stdtypes.html#typeiter">Iterators</a>.</li>
25+
<li>Python Language Reference: <a href="http://docs.python.org/3/reference/datamodel.html#object.__iter__">object.__iter__</a>.</li>
26+
<li>Python Standard Library: <a href="http://docs.python.org/3/library/stdtypes.html#typeiter">Iterators</a>.</li>
3427

3528

3629
</references>

python/ql/src/Functions/IterReturnsNonSelf.ql

Lines changed: 69 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,87 @@
44
* @kind problem
55
* @tags reliability
66
* correctness
7+
* quality
78
* @problem.severity error
89
* @sub-severity low
910
* @precision high
1011
* @id py/iter-returns-non-self
1112
*/
1213

1314
import python
15+
import semmle.python.ApiGraphs
1416

15-
Function iter_method(ClassValue t) { result = t.lookup("__iter__").(FunctionValue).getScope() }
17+
/** Gets the __iter__ method of `c`. */
18+
Function iterMethod(Class c) { result = c.getAMethod() and result.getName() = "__iter__" }
1619

17-
predicate is_self(Name value, Function f) { value.getVariable() = f.getArg(0).(Name).getVariable() }
20+
/** Gets the `__next__` method of `c`. */
21+
Function nextMethod(Class c) { result = c.getAMethod() and result.getName() = "__next__" }
1822

19-
predicate returns_non_self(Function f) {
23+
/** Holds if `var` is a variable referring to the `self` parameter of `f`. */
24+
predicate isSelfVar(Function f, Name var) { var.getVariable() = f.getArg(0).(Name).getVariable() }
25+
26+
/** Holds if `e` is an expression that an iter function `f` should return. */
27+
predicate isGoodReturn(Function f, Expr e) {
28+
isSelfVar(f, e)
29+
or
30+
exists(DataFlow::CallCfgNode call, DataFlow::AttrRead read, DataFlow::Node selfNode |
31+
e = call.asExpr()
32+
|
33+
call = API::builtin("iter").getACall() and
34+
call.getArg(0) = read and
35+
read.accesses(selfNode, "__next__") and
36+
isSelfVar(f, selfNode.asExpr()) and
37+
call.getArg(1).asExpr() instanceof None
38+
)
39+
}
40+
41+
/** Holds if the iter method `f` does not return `self` or an equivalent. */
42+
predicate returnsNonSelf(Function f) {
2043
exists(f.getFallthroughNode())
2144
or
22-
exists(Return r | r.getScope() = f and not is_self(r.getValue(), f))
45+
exists(Return r | r.getScope() = f and not isGoodReturn(f, r.getValue()))
46+
}
47+
48+
/** Holds if `iter` and `next` methods are wrappers around some field. */
49+
predicate iterWrapperMethods(Function iter, Function next) {
50+
exists(string field |
51+
exists(Return r, DataFlow::Node self, DataFlow::AttrRead read |
52+
r.getScope() = iter and
53+
r.getValue() = [iterCall(read).asExpr(), read.asExpr()] and
54+
read.accesses(self, field) and
55+
isSelfVar(iter, self.asExpr())
56+
) and
57+
exists(Return r, DataFlow::Node self, DataFlow::AttrRead read |
58+
r.getScope() = next and
59+
r.getValue() = nextCall(read).asExpr() and
60+
read.accesses(self, field) and
61+
isSelfVar(next, self.asExpr())
62+
)
63+
)
64+
}
65+
66+
/** Gets a call to `iter(arg)` or `arg.__iter__()`. */
67+
private DataFlow::CallCfgNode iterCall(DataFlow::Node arg) {
68+
result.(DataFlow::MethodCallNode).calls(arg, "__iter__")
69+
or
70+
result = API::builtin("iter").getACall() and
71+
arg = result.getArg(0) and
72+
not exists(result.getArg(1))
73+
}
74+
75+
/** Gets a call to `next(arg)` or `arg.__next__()`. */
76+
private DataFlow::CallCfgNode nextCall(DataFlow::Node arg) {
77+
result.(DataFlow::MethodCallNode).calls(arg, "__next__")
2378
or
24-
exists(Return r | r.getScope() = f and not exists(r.getValue()))
79+
result = API::builtin("next").getACall() and
80+
arg = result.getArg(0)
2581
}
2682

27-
from ClassValue t, Function iter
28-
where t.isIterator() and iter = iter_method(t) and returns_non_self(iter)
29-
select t, "Class " + t.getName() + " is an iterator but its $@ method does not return 'self'.",
30-
iter, iter.getName()
83+
from Class c, Function iter, Function next
84+
where
85+
next = nextMethod(c) and
86+
iter = iterMethod(c) and
87+
returnsNonSelf(iter) and
88+
not iterWrapperMethods(iter, next)
89+
select iter, "Iter method of iterator $@ does not return `" + iter.getArg(0).getName() + "`.", c,
90+
c.getName()

python/ql/src/Functions/IterReturnsNonSelf.py renamed to python/ql/src/Functions/examples/IterReturnsNonSelf.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ def __init__(self, low, high):
44
self.high = high
55

66
def __iter__(self):
7-
return self.current
7+
return (self.current, self.high) # BAD: does not return `self`.
88

9-
def next(self):
9+
def __next__(self):
1010
if self.current > self.high:
11-
raise StopIteration
11+
return None
1212
self.current += 1
1313
return self.current - 1
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* The `py/iter-returns-non-self` query has been modernized, and no longer alerts for certain cases where an equivalent iterator is returned.

python/ql/test/query-tests/Functions/general/IterReturnsNonSelf.expected

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
| test.py:5:5:5:23 | Function __iter__ | Iter method of iterator $@ does not return `self`. | test.py:1:1:1:11 | Class Bad1 | Bad1 |
2+
| test.py:51:5:51:23 | Function __iter__ | Iter method of iterator $@ does not return `self`. | test.py:42:1:42:21 | Class FalsePositive1 | FalsePositive1 |
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
class Bad1:
2+
def __next__(self):
3+
return 0
4+
5+
def __iter__(self): # BAD: Iter does not return self
6+
yield 0
7+
8+
class Good1:
9+
def __next__(self):
10+
return 0
11+
12+
def __iter__(self): # GOOD: iter returns self
13+
return self
14+
15+
class Good2:
16+
def __init__(self):
17+
self._it = iter([0,0,0])
18+
19+
def __next__(self):
20+
return next(self._it)
21+
22+
def __iter__(self): # GOOD: iter and next are wrappers around a field
23+
return self._it.__iter__()
24+
25+
class Good3:
26+
def __init__(self):
27+
self._it = iter([0,0,0])
28+
29+
def __next__(self):
30+
return self._it.__next__()
31+
32+
def __iter__(self): # GOOD: iter and next are wrappers around a field
33+
return self._it
34+
35+
class Good4:
36+
def __next__(self):
37+
return 0
38+
39+
def __iter__(self): # GOOD: this is an equivalent iterator to `self`.
40+
return iter(self.__next__, None)
41+
42+
class FalsePositive1:
43+
def __init__(self):
44+
self._it = None
45+
46+
def __next__(self):
47+
if self._it is None:
48+
self._it = iter(self)
49+
return next(self._it)
50+
51+
def __iter__(self): # SPURIOUS, GOOD: implementation of next ensures the iterator is equivalent to the one returned by iter, but this is not detected.
52+
yield 0
53+
yield 0

0 commit comments

Comments
 (0)