Skip to content

Commit a2ec5b3

Browse files
committed
Python: Get rid of isinstance FPs
Eliminates cases where we explicitly check whether the object in question is an instance of (a subclass of) a built-in container type.
1 parent 73ef50c commit a2ec5b3

File tree

2 files changed

+52
-0
lines changed

2 files changed

+52
-0
lines changed

python/ql/src/Expressions/ContainsNonContainer.ql

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import python
1515
import semmle.python.dataflow.new.DataFlow
1616
private import semmle.python.dataflow.new.internal.DataFlowDispatch
17+
private import semmle.python.ApiGraphs
1718

1819
predicate rhs_in_expr(Expr rhs, Compare cmp) {
1920
exists(Cmpop op, int i | cmp.getOp(i) = op and cmp.getComparator(i) = rhs |
@@ -42,6 +43,35 @@ predicate hasDynamicMethods(Class cls) {
4243
)
4344
}
4445

46+
/**
47+
* Holds if `cls_arg` references a known container builtin type, either directly
48+
* or as an element of a tuple.
49+
*/
50+
private predicate isContainerTypeArg(DataFlow::Node cls_arg) {
51+
cls_arg =
52+
API::builtin(["list", "tuple", "set", "frozenset", "dict", "str", "bytes", "bytearray"])
53+
.getAValueReachableFromSource()
54+
or
55+
isContainerTypeArg(DataFlow::exprNode(cls_arg.asExpr().(Tuple).getAnElt()))
56+
}
57+
58+
/**
59+
* Holds if `rhs` is guarded by an `isinstance` check that tests for
60+
* a container type.
61+
*/
62+
predicate guardedByIsinstanceContainer(DataFlow::Node rhs) {
63+
exists(
64+
DataFlow::GuardNode guard, DataFlow::CallCfgNode isinstance_call, DataFlow::LocalSourceNode src
65+
|
66+
isinstance_call = API::builtin("isinstance").getACall() and
67+
src.flowsTo(isinstance_call.getArg(0)) and
68+
src.flowsTo(rhs) and
69+
isContainerTypeArg(isinstance_call.getArg(1)) and
70+
guard = isinstance_call.asCfgNode() and
71+
guard.controlsBlock(rhs.asCfgNode().getBasicBlock(), true)
72+
)
73+
}
74+
4575
from Compare cmp, DataFlow::LocalSourceNode origin, DataFlow::Node rhs, Class cls
4676
where
4777
origin = classInstanceTracker(cls) and
@@ -50,6 +80,7 @@ where
5080
not DuckTyping::hasUnresolvedBase(getADirectSuperclass*(cls)) and
5181
not isDecoratorApplication(origin) and
5282
not hasDynamicMethods(cls) and
83+
not guardedByIsinstanceContainer(rhs) and
5384
rhs_in_expr(rhs.asExpr(), cmp)
5485
select cmp, "This test may raise an Exception as the $@ may be of non-container class $@.", origin,
5586
"target", cls, cls.getName()

python/ql/test/query-tests/Expressions/general/expressions_test.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,3 +317,24 @@ def test_dynamic_methods():
317317
# OK: __contains__ is added dynamically via setattr.
318318
if "name" in proxy:
319319
pass
320+
321+
# isinstance guard should suppress non-container warning
322+
def guarded_contains(x):
323+
obj = XIter()
324+
if isinstance(obj, dict):
325+
if x in obj: # OK: guarded by isinstance
326+
pass
327+
328+
def guarded_contains_tuple(x):
329+
obj = XIter()
330+
if isinstance(obj, (list, dict, set)):
331+
if x in obj: # OK: guarded by isinstance with tuple of types
332+
pass
333+
334+
# Negated isinstance guard: early return when NOT a container
335+
def guarded_contains_negated(x):
336+
obj = XIter()
337+
if not isinstance(obj, dict):
338+
return
339+
if x in obj: # OK: guarded by negated isinstance + early return
340+
pass

0 commit comments

Comments
 (0)