From f040ad8dacd465bd0830e6af4a8203a2585ff221 Mon Sep 17 00:00:00 2001 From: Rasmus Wriedt Larsen Date: Thu, 20 Oct 2022 14:32:48 +0200 Subject: [PATCH] Python: Add test of `__new__` handling --- .../dataflow/calls/new_cls_param.py | 16 ++++++++++++ .../experimental/dataflow/fieldflow/test.py | 26 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 python/ql/test/experimental/dataflow/calls/new_cls_param.py diff --git a/python/ql/test/experimental/dataflow/calls/new_cls_param.py b/python/ql/test/experimental/dataflow/calls/new_cls_param.py new file mode 100644 index 00000000000..38274a9e160 --- /dev/null +++ b/python/ql/test/experimental/dataflow/calls/new_cls_param.py @@ -0,0 +1,16 @@ +# We want to ensure that the __new__ method is considered a classmethod even though it +# doesn't have a decorator. This means that the `cls` parameter should be considered a +# reference to the class (or subclass), and not an instance of the class. We can detect +# this from looking at the arguments passed in the `cls.foo` call. if we see a `self` +# argument, this means it has correct behavior (because we're targeting a classmethod), +# if there is no `self` argument, this means we've only considered `cls` to be a class +# instance, since we don't want to pass that to the `cls` parameter of the classmethod `WithNewImpl.foo`. + +class WithNewImpl(object): + def __new__(cls): + print("WithNewImpl.foo") + cls.foo() # $ call=cls.foo() callType=CallTypeClassMethod MISSING: arg[self]=cls + + @classmethod + def foo(cls): + print("WithNewImpl.foo") diff --git a/python/ql/test/experimental/dataflow/fieldflow/test.py b/python/ql/test/experimental/dataflow/fieldflow/test.py index 68bb71bd278..5f1f6f47058 100644 --- a/python/ql/test/experimental/dataflow/fieldflow/test.py +++ b/python/ql/test/experimental/dataflow/fieldflow/test.py @@ -385,6 +385,32 @@ def test_potential_crosstalk_same_class(cond=True): SINK_F(objx2.x) +class NewTest(object): + def __new__(cls, arg): + cls.foo = arg + return super().__new__(cls) # $ unresolved_call=super().__new__(..) + +@expects(4) # $ unresolved_call=expects(..) unresolved_call=expects(..)(..) +def test__new__(): + # we want to make sure that we DON'T pass the synthetic pre-update node for + # the class instance to __new__, like we do for __init__. + nt = NewTest(SOURCE) + # the __new__ implementation sets the foo attribute on THE CLASS itself. The + # attribute lookup on the class instance will go to the class itself when the + # attribute isn't defined on the class instance, so we will actually see `nt.foo` + # contain the source, but the point of this test is that we should see identical + # behavior between NewTest.foo and nt.foo, which we dont! + # + # Also note that we currently (October 2022) dont' model writes to classes very + # well. + + SINK(NewTest.foo) # $ MISSING: flow="SOURCE, l:-10 -> NewTest.foo" + SINK(nt.foo) # $ flow="SOURCE, l:-11 -> nt.foo" + + NewTest.foo = NONSOURCE + SINK_F(NewTest.foo) + SINK_F(nt.foo) # $ SPURIOUS: flow="SOURCE, l:-15 -> nt.foo" + # ------------------------------------------------------------------------------ # Global scope # ------------------------------------------------------------------------------