Skip to content

Commit c6a7aa2

Browse files
committed
Python: Exclude iterators guarded by isinstance checks
A common pattern is to check `isinstance(it, (list, tuple)` before proceeding with the iteration.
1 parent db8b990 commit c6a7aa2

File tree

2 files changed

+51
-5
lines changed

2 files changed

+51
-5
lines changed

python/ql/src/Statements/NonIteratorInForLoop.ql

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,47 @@
1313

1414
import python
1515
private import semmle.python.dataflow.new.internal.DataFlowDispatch
16+
private import semmle.python.ApiGraphs
1617

17-
from For loop, Expr iter, Class cls
18+
/**
19+
* Holds if `cls_arg` references a known iterable builtin type, either directly
20+
* (e.g. `list`) or as an element of a tuple (e.g. `(list, tuple)`).
21+
*/
22+
private predicate isIterableTypeArg(DataFlow::Node cls_arg) {
23+
cls_arg =
24+
API::builtin([
25+
"list", "tuple", "set", "frozenset", "dict", "str", "bytes", "bytearray", "range",
26+
"memoryview"
27+
]).getAValueReachableFromSource()
28+
or
29+
isIterableTypeArg(DataFlow::exprNode(cls_arg.asExpr().(Tuple).getAnElt()))
30+
}
31+
32+
/**
33+
* Holds if `iter` is guarded by an `isinstance` check that tests for
34+
* an iterable type (e.g. `list`, `tuple`, `set`, `dict`).
35+
*/
36+
predicate guardedByIsinstanceIterable(DataFlow::Node iter) {
37+
exists(
38+
ConditionBlock guard, DataFlow::CallCfgNode isinstance_call, DataFlow::LocalSourceNode src
39+
|
40+
isinstance_call = API::builtin("isinstance").getACall() and
41+
src.flowsTo(isinstance_call.getArg(0)) and
42+
src.flowsTo(iter) and
43+
isIterableTypeArg(isinstance_call.getArg(1)) and
44+
guard.getLastNode() = isinstance_call.asCfgNode() and
45+
guard.controls(iter.asCfgNode().getBasicBlock(), true)
46+
)
47+
}
48+
49+
from For loop, DataFlow::Node iter, Class cls
1850
where
19-
iter = loop.getIter() and
20-
classInstanceTracker(cls).asExpr() = iter and
51+
iter.asExpr() = loop.getIter() and
52+
iter = classInstanceTracker(cls) and
2153
not DuckTyping::isIterable(cls) and
2254
not DuckTyping::isDescriptor(cls) and
2355
not (loop.isAsync() and DuckTyping::hasMethod(cls, "__aiter__")) and
24-
not DuckTyping::hasUnresolvedBase(getADirectSuperclass*(cls))
25-
select loop, "This for-loop may attempt to iterate over a $@ of class $@.", iter,
56+
not DuckTyping::hasUnresolvedBase(getADirectSuperclass*(cls)) and
57+
not guardedByIsinstanceIterable(iter)
58+
select loop, "This for-loop may attempt to iterate over a $@ of class $@.", iter.asExpr(),
2659
"non-iterable instance", cls, cls.getName()

python/ql/test/query-tests/Statements/general/test.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,3 +174,16 @@ def assert_ok(seq):
174174
# False positive. ODASA-8042. Fixed in PR #2401.
175175
class false_positive:
176176
e = (x for x in [])
177+
178+
# isinstance guard should suppress non-iterable warning
179+
def guarded_iteration(x):
180+
ni = NonIterator()
181+
if isinstance(ni, (list, tuple)):
182+
for item in ni:
183+
pass
184+
185+
def guarded_iteration_single(x):
186+
ni = NonIterator()
187+
if isinstance(ni, list):
188+
for item in ni:
189+
pass

0 commit comments

Comments
 (0)