Skip to content

Commit 3e6e729

Browse files
committed
Python: Remove some FPs for ContainsNonContainer.ql
First fix handles the case where there's interference from a class-based decorator on a function. In this case, _technically_ we have an instance of the decorator class, but in practice this decorator will (hopefully) forward all accesses to the thing it wraps. The second fix has to do with methods that are added dynamically using `setattr`. In this case, we cannot be sure that the relevant methods are actually missing.
1 parent c6a7aa2 commit 3e6e729

File tree

2 files changed

+61
-0
lines changed

2 files changed

+61
-0
lines changed

python/ql/src/Expressions/ContainsNonContainer.ql

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,35 @@ predicate rhs_in_expr(Expr rhs, Compare cmp) {
2121
)
2222
}
2323

24+
/**
25+
* Holds if `origin` is the result of applying a class as a decorator to a function.
26+
* Such decorator classes act as proxies, and the runtime value of the decorated
27+
* attribute may be of a different type than the decorator class itself.
28+
*/
29+
predicate isDecoratorApplication(DataFlow::LocalSourceNode origin) {
30+
exists(FunctionExpr fe | origin.asExpr() = fe.getADecoratorCall())
31+
}
32+
33+
/**
34+
* Holds if `cls` has methods dynamically added via `setattr`, so we cannot
35+
* statically determine its full interface.
36+
*/
37+
predicate hasDynamicMethods(Class cls) {
38+
exists(CallNode setattr_call |
39+
setattr_call.getFunction().(NameNode).getId() = "setattr" and
40+
setattr_call.getArg(0).(NameNode).getId() = cls.getName() and
41+
setattr_call.getScope() = cls.getScope()
42+
)
43+
}
44+
2445
from Compare cmp, DataFlow::LocalSourceNode origin, DataFlow::Node rhs, Class cls
2546
where
2647
origin = classInstanceTracker(cls) and
2748
origin.flowsTo(rhs) and
2849
not DuckTyping::isContainer(cls) and
2950
not DuckTyping::hasUnresolvedBase(getADirectSuperclass*(cls)) and
51+
not isDecoratorApplication(origin) and
52+
not hasDynamicMethods(cls) and
3053
rhs_in_expr(rhs.asExpr(), cmp)
3154
select cmp, "This test may raise an Exception as the $@ may be of non-container class $@.", origin,
3255
"target", cls, cls.getName()

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,3 +279,41 @@ def local():
279279
def apply(f):
280280
pass
281281
apply(foo)([1])
282+
283+
# Class used as a decorator: the runtime value at attribute access is the
284+
# function's return value, not the decorator class instance.
285+
class cached_property(object):
286+
def __init__(self, func):
287+
self.func = func
288+
def __get__(self, obj, cls):
289+
val = self.func(obj)
290+
setattr(obj, self.func.__name__, val)
291+
return val
292+
293+
class MyForm(object):
294+
@cached_property
295+
def changed_data(self):
296+
return [1, 2, 3]
297+
298+
def test_decorator_class(form):
299+
f = MyForm()
300+
# OK: cached_property is a descriptor; the actual runtime value is a list.
301+
if "name" in f.changed_data:
302+
pass
303+
304+
# Class with dynamically added methods via setattr: we cannot statically
305+
# determine its full interface, so we should not flag it.
306+
class DynamicProxy(object):
307+
def __init__(self, args):
308+
self._args = args
309+
310+
for method_name in ["__contains__", "__iter__", "__len__"]:
311+
def wrapper(self, *args, __method_name=method_name):
312+
pass
313+
setattr(DynamicProxy, method_name, wrapper)
314+
315+
def test_dynamic_methods():
316+
proxy = DynamicProxy(())
317+
# OK: __contains__ is added dynamically via setattr.
318+
if "name" in proxy:
319+
pass

0 commit comments

Comments
 (0)