mirror of
https://github.com/github/codeql.git
synced 2026-03-25 17:06:46 +01:00
This commit is a squash of 80 other commits. While developing, things changed majorly 2-3 times, and it just wasn't feasible to go back and write a really nice commit history. My apologies for this HUGE commit. Also, later on this is where I solved merge conflicts after flow-summaries PR was merged. For your amusement, I've included the original commit messages below. Python: Add proper argument/parameter positions Python: Handle normal function calls Python: Reduce dataflow-consistency warnings Previously there was a lot of failures for `uniqueEnclosingCallable` and `argHasPostUpdate` Removing the override of `getEnclosingCallable` in ParameterNode is probably the most controversial... although from my point of view it's a change for the better, since we're able to provide data-flow ParameterNodes for more of the AST parameter nodes. Python: Adjust `dataflow/calls` test Python: Implement `isParameterOf`/`argumentOf`/`OutNode` This makes the tests under `dataflow/basic` work as well 👍 (initially I had these as separate commits, but it felt like it was too much noise) Python: Accept fix for `dataflow/consistency` Python: Changes to `coverage/argumentRoutingTest.ql` Notice we gain a few new resolved arguments. We loose out on stuff due to: 1. not handling `*` or `**` in either arguments/parameters (yet) 2. not handling special calls (yet) Python: Small fix for `TestUtil/RoutingTest.qll` Since the helper predicates do not depend on this, moved outside class. Python: Accept changes to `dataflow/coverage/NormalDataflowTest.ql` Most of this is due to: - not handling any kinds of methods yet - not handling `*` or `**` Python: Small investigation of `test_deep_callgraph` Python: Accept changes to `coverage/localFlow.ql` I don't fully understand why the .expected file changed. Since we still have the desired flow, I'm not going to worry too much about it. with this commit, the `dataflow/coverage` tests passes 👍 Python: Minor doc update Python: Add staticmethod/classmethod to `dataflow/calls` Python: Handle method calls on class instances without trying to deal with any class inheritance, or staticmethod/classmethod at all. Notice that with this change, we only have a DataFlowCall for the calls that we can actually resolve. I'm not 100% sure if we need to add a `UnresolvedCall` subclass of `DataFlowCall` for MaD in the future, but it should be easy to do. I'm still unsure about the value of `classesCallGraph`, but have just accepted the changes. Python: Handle direct method calls `C.foo(C, arg0)` Python: Handle `@staticmethod` Python: Handle class method calls... but the code is shit WIP todo Rewrite method calls to be better also fixed a problem with `self` being an argument to the `x.staticmethod()` call :| Python: Add subclass tests Python: Split `class_advanced` test Python: Rewrite call-graph tests to be inline expectation (1/2) This adds inline expectations, next commit will remove old annotations code... but I thought it would be easier to review like this. Minor fixup Python: Add simple subclass support Python: more precise subclass lookup Still not 100% precise.. but it's better New ambiguous Python: Add test for `self.m()` and `cls.m()` calls Python: Handle `self.m()` and `cls.m()` calls Python: Add tests for `__init__` and `__new__` Python: Handle class calls Python: Fix `self` argument passing for class calls Now field-flow tests also pass 💪 (although the crosstalk fieldflow test changes were due to this specific commit) I also copied much of the setup for pre/post update nodes from Ruby, specifically having the abstract `PostUpdateNodeImpl` in DataFlowPrivate seemed like a nice change. Same for the setup with `TNode` definition having the specification directly in the body, instead of a `NeedsSyntheticPostUpdateNode` class. Python: Add new crosstalk test WIP Maybe needs a bit of refactoring, and to see how it all behaves with points-to Python: Add `super()` call-graph tests Python: Refactor MethodCall char-pred In anticipation of supporting `super(MyClass, self).foo()`, where the `self` argument doesn't come from an AttrNode, but from the second argument to super. Without `pragma[inline]` the optimizer found a terrible join-order -- this won't guarantee a good join-order for the future, but for now it was just so simple and could let me move on with life. Python: Add basic `super()` support I debated a little (with myself) whether I should really do `superTracker`, but I thought "why not" and just rolled with it. I did not confirm whether it was actually needed anywhere, that is if anyone does `ref = super; ref().foo()` -- although I certainly doubt it's very wide-spread. Python: InlineCallGraphTest: Allow non-unique callable name in different files Python: more MRO tests Python: Add MRO approximation for `super()` Although it's not 100% accurate, it seems to be on level with the one in points-to. Python: Remove some spurious targets for direct calls removal of TODO from refactoring remove TODOs class call support Python: Add contrived subclass call example Python: Remove more spurious call targets NOTE: I initially forgot to use `findFunctionAccordingToMroKnownStartingClass` instead of `findFunctionAccordingToMro` for __init__ and __new__, and since I did make that mistake myself, I wanted to add something to the test to highlight this fact, and make it viewable by PR reviewer... this will be fixed in the next commit. Python: Proper fix for spurious __init__ targets Python: Add call-graph example of class decorator Python: Support decorated classes in new call-graph Python: Add call-graph tests for `type(obj).meth()` Python: support `type(obj).meth()` Python: Add test for callable defined in function Python: Add test for callable as argument Current'y we don't find these with type-tracking, which is super mysterious. I did check that we have proper flow from the arguments to the parameters. Python: Found problem for callable as argument :| MAJOR WIP WIP commit IT WORKS AGAIN (but terrible performance) remove pragma[inline] remove oops Fix performance problem I tried to optimize it even further, but I didn't end up achieving anything :| Fix call-graph comparison add comparison version with easy lookup incomplete missing call-graph tests unhandled tests trying to replicate missing call-edge due to missing imports ... but it's hard also seems to be problems with the inline-expectation-value that I used, seems like it has both missing/unexpected results with same value Python: Add import-problem test Python: Add shadowing problem some cleanup of rewrite fix a little more cleanup Add consistency queries to call-graph tests Python: Add post-update nodes for `self` in implicit `super()` uses But we do need to discuss whether this is the right approach :O Fix for field-flow tests This came from more precise argument passing Fixed results in type-tracking Comes from better argument passing with super() and handling of functions with decorators fix of inline call graph tests Fixup call annotation test Many minor cleanups/fixes NewNormalCall -> NormalCall Python: Major restructuring + qldoc writing Python: Accept changes from pre/post update node .toString changes Python: Reduce `super` complexity !! WIP !! Python: Only pass self-reference if in same enclosing-callable Python: Add call-graph test with nested class This was inspired by the ImpliesDataflow test that showed missing flow for q_super, but at least for the call-graph, I'm not able to reproduce this missing result :| Python: Restrict `super()` to function defined directly on class Python: Accept fixes to ImpliesDataflow Python: Expand field-flow crosstalk tests
475 lines
12 KiB
Python
475 lines
12 KiB
Python
import sys
|
|
import os
|
|
|
|
sys.path.append(os.path.dirname(os.path.dirname((__file__)))) # $ unresolved_call=sys.path.append(..)
|
|
from testlib import expects
|
|
|
|
# These are defined so that we can evaluate the test code.
|
|
NONSOURCE = "not a source"
|
|
SOURCE = "source"
|
|
|
|
|
|
def is_source(x):
|
|
return x == "source" or x == b"source" or x == 42 or x == 42.0 or x == 42j
|
|
|
|
|
|
def SINK(x, *, not_present_at_runtime=False):
|
|
# not_present_at_runtime supports use-cases where we want flow from data-flow layer
|
|
# (so we want to use SINK), but we end up in a siaution where it's not possible to
|
|
# actually get flow from a source at runtime. The only use-case is for the
|
|
# cross-talk tests, where our ability to use if-then-else is limited because doing
|
|
# so would make cfg-splitting kick in, and that would solve the problem trivially
|
|
# (by the splitting).
|
|
if not_present_at_runtime:
|
|
print("OK")
|
|
return
|
|
|
|
if is_source(x):
|
|
print("OK")
|
|
else:
|
|
print("Unexpected flow", x)
|
|
|
|
|
|
def SINK_F(x):
|
|
if is_source(x):
|
|
print("Unexpected flow", x)
|
|
else:
|
|
print("OK")
|
|
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Actual tests
|
|
# ------------------------------------------------------------------------------
|
|
|
|
class MyObj(object):
|
|
def __init__(self, foo):
|
|
self.foo = foo
|
|
|
|
def setFoo(self, foo):
|
|
self.foo = foo
|
|
|
|
def setFoo(obj, x):
|
|
obj.foo = x
|
|
|
|
|
|
@expects(3) # $ unresolved_call=expects(..) unresolved_call=expects(..)(..)
|
|
def test_indirect_assign():
|
|
myobj = MyObj(NONSOURCE)
|
|
SINK_F(myobj.foo)
|
|
|
|
setFoo(myobj, SOURCE)
|
|
SINK(myobj.foo) # $ flow="SOURCE, l:-1 -> myobj.foo"
|
|
|
|
setFoo(myobj, NONSOURCE)
|
|
SINK_F(myobj.foo) # $ SPURIOUS: flow="SOURCE, l:-4 -> myobj.foo"
|
|
|
|
|
|
@expects(3) # $ unresolved_call=expects(..) unresolved_call=expects(..)(..)
|
|
def test_indirect_assign_method():
|
|
myobj = MyObj(NONSOURCE)
|
|
SINK_F(myobj.foo)
|
|
|
|
myobj.setFoo(SOURCE)
|
|
SINK(myobj.foo) # $ flow="SOURCE, l:-1 -> myobj.foo"
|
|
|
|
myobj.setFoo(NONSOURCE)
|
|
SINK_F(myobj.foo) # $ SPURIOUS: flow="SOURCE, l:-4 -> myobj.foo"
|
|
|
|
|
|
@expects(3) # $ unresolved_call=expects(..) unresolved_call=expects(..)(..)
|
|
def test_indirect_assign_bound_method():
|
|
myobj = MyObj(NONSOURCE)
|
|
SINK_F(myobj.foo)
|
|
|
|
sf = myobj.setFoo
|
|
|
|
sf(SOURCE)
|
|
SINK(myobj.foo) # $ flow="SOURCE, l:-1 -> myobj.foo"
|
|
|
|
sf(NONSOURCE)
|
|
SINK_F(myobj.foo) # $ SPURIOUS: flow="SOURCE, l:-4 -> myobj.foo"
|
|
|
|
|
|
@expects(3) # $ unresolved_call=expects(..) unresolved_call=expects(..)(..)
|
|
def test_direct_assign():
|
|
myobj = MyObj(NONSOURCE)
|
|
SINK_F(myobj.foo)
|
|
|
|
myobj.foo = SOURCE
|
|
SINK(myobj.foo) # $ flow="SOURCE, l:-1 -> myobj.foo"
|
|
|
|
myobj.foo = NONSOURCE
|
|
SINK_F(myobj.foo)
|
|
|
|
|
|
def test_direct_if_assign(cond = False):
|
|
myobj = MyObj(NONSOURCE)
|
|
myobj.foo = SOURCE
|
|
if cond:
|
|
myobj.foo = NONSOURCE
|
|
SINK_F(myobj.foo)
|
|
SINK(myobj.foo) # $ flow="SOURCE, l:-4 -> myobj.foo"
|
|
|
|
|
|
@expects(2) # $ unresolved_call=expects(..) unresolved_call=expects(..)(..)
|
|
def test_direct_if_always_assign(cond = True):
|
|
myobj = MyObj(NONSOURCE)
|
|
myobj.foo = SOURCE
|
|
if cond:
|
|
myobj.foo = NONSOURCE
|
|
SINK_F(myobj.foo)
|
|
else:
|
|
myobj.foo = NONSOURCE
|
|
SINK_F(myobj.foo)
|
|
SINK_F(myobj.foo)
|
|
|
|
|
|
def test_getattr():
|
|
myobj = MyObj(NONSOURCE)
|
|
myobj.foo = SOURCE
|
|
SINK(getattr(myobj, "foo")) # $ flow="SOURCE, l:-1 -> getattr(..)"
|
|
|
|
|
|
def test_setattr():
|
|
myobj = MyObj(NONSOURCE)
|
|
setattr(myobj, "foo", SOURCE)
|
|
SINK(myobj.foo) # $ flow="SOURCE, l:-1 -> myobj.foo"
|
|
|
|
|
|
def test_setattr_getattr():
|
|
myobj = MyObj(NONSOURCE)
|
|
setattr(myobj, "foo", SOURCE)
|
|
SINK(getattr(myobj, "foo")) # $ flow="SOURCE, l:-1 -> getattr(..)"
|
|
|
|
|
|
def test_setattr_getattr_overwrite():
|
|
myobj = MyObj(NONSOURCE)
|
|
setattr(myobj, "foo", SOURCE)
|
|
setattr(myobj, "foo", NONSOURCE)
|
|
SINK_F(getattr(myobj, "foo"))
|
|
|
|
|
|
def test_constructor_assign():
|
|
obj = MyObj(SOURCE)
|
|
SINK(obj.foo) # $ flow="SOURCE, l:-1 -> obj.foo"
|
|
|
|
|
|
def test_constructor_assign_kw():
|
|
obj = MyObj(foo=SOURCE)
|
|
SINK(obj.foo) # $ flow="SOURCE, l:-1 -> obj.foo"
|
|
|
|
|
|
def fields_with_local_flow(x):
|
|
obj = MyObj(x)
|
|
a = obj.foo
|
|
return a
|
|
|
|
def test_fields():
|
|
SINK(fields_with_local_flow(SOURCE)) # $ flow="SOURCE -> fields_with_local_flow(..)"
|
|
|
|
|
|
def call_with_source(func):
|
|
func(SOURCE)
|
|
|
|
|
|
def test_bound_method_passed_as_arg():
|
|
myobj = MyObj(NONSOURCE)
|
|
call_with_source(myobj.setFoo)
|
|
SINK(myobj.foo) # $ MISSING: flow="SOURCE, l:-5 -> foo.x"
|
|
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Nested Object
|
|
# ------------------------------------------------------------------------------
|
|
|
|
class NestedObj(object):
|
|
def __init__(self):
|
|
self.obj = MyObj("OK")
|
|
|
|
def getObj(self):
|
|
return self.obj
|
|
|
|
|
|
def test_nested_obj():
|
|
x = SOURCE
|
|
a = NestedObj()
|
|
a.obj.foo = x
|
|
SINK(a.obj.foo) # $ flow="SOURCE, l:-3 -> a.obj.foo"
|
|
|
|
|
|
def test_nested_obj_method():
|
|
x = SOURCE
|
|
a = NestedObj()
|
|
a.getObj().foo = x
|
|
SINK(a.obj.foo) # $ flow="SOURCE, l:-3 -> a.obj.foo"
|
|
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Field access on compound arguments
|
|
# ------------------------------------------------------------------------------
|
|
|
|
# TODO: Add support for this, see https://github.com/github/codeql/pull/10444
|
|
|
|
@expects(5) # $ unresolved_call=expects(..) unresolved_call=expects(..)(..)
|
|
def test_field_on_compound_arg(cond_true=True, cond_false=False):
|
|
class Ex:
|
|
def __init__(self):
|
|
self.attr = None
|
|
|
|
def set_attr(obj):
|
|
obj.attr = SOURCE
|
|
|
|
x = Ex()
|
|
y = Ex()
|
|
set_attr(x if cond_true else y)
|
|
SINK(x.attr) # $ MISSING: flow
|
|
|
|
x = Ex()
|
|
y = Ex()
|
|
set_attr(x if cond_false else y)
|
|
SINK(y.attr) # $ MISSING: flow
|
|
|
|
x = Ex()
|
|
y = Ex()
|
|
z = Ex()
|
|
set_attr(x if cond_false else (y if cond_true else z))
|
|
SINK_F(x.attr) # $ MISSING: flow
|
|
SINK(y.attr) # $ MISSING: flow
|
|
SINK_F(z.attr) # $ MISSING: flow
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Crosstalk test -- using different function based on conditional
|
|
# ------------------------------------------------------------------------------
|
|
# NOTE: These tests use `SINK(objy.y, not_present_at_runtime=True)` since it's not
|
|
# possible to use if-then-else statements, since that would make cfg-splitting kick in,
|
|
# and that would solve the problem trivially (by the splitting).
|
|
|
|
class CrosstalkTestX:
|
|
def __init__(self):
|
|
self.x = None
|
|
self.y = None
|
|
|
|
def setx(self, value):
|
|
self.x = value
|
|
|
|
def setvalue(self, value):
|
|
self.x = value
|
|
|
|
def do_nothing(self, value):
|
|
pass
|
|
|
|
|
|
class CrosstalkTestY:
|
|
def __init__(self):
|
|
self.x = None
|
|
self.y = None
|
|
|
|
def sety(self ,value):
|
|
self.y = value
|
|
|
|
def setvalue(self, value):
|
|
self.y = value
|
|
|
|
|
|
@expects(8) # $ unresolved_call=expects(..) unresolved_call=expects(..)(..)
|
|
def test_no_crosstalk_reference(cond=True):
|
|
objx = CrosstalkTestX()
|
|
SINK_F(objx.x)
|
|
SINK_F(objx.y)
|
|
|
|
objy = CrosstalkTestY()
|
|
SINK_F(objy.x)
|
|
SINK_F(objy.y)
|
|
|
|
if cond:
|
|
objx.setvalue(SOURCE)
|
|
else:
|
|
objy.setvalue(SOURCE)
|
|
|
|
SINK(objx.x) # $ flow="SOURCE, l:-4 -> objx.x"
|
|
SINK_F(objx.y)
|
|
SINK_F(objy.x)
|
|
SINK(objy.y, not_present_at_runtime=True) # $ flow="SOURCE, l:-5 -> objy.y"
|
|
|
|
|
|
@expects(8) # $ unresolved_call=expects(..) unresolved_call=expects(..)(..)
|
|
def test_potential_crosstalk_different_name(cond=True):
|
|
objx = CrosstalkTestX()
|
|
SINK_F(objx.x)
|
|
SINK_F(objx.y)
|
|
|
|
objy = CrosstalkTestY()
|
|
SINK_F(objy.x)
|
|
SINK_F(objy.y)
|
|
|
|
if cond:
|
|
func = objx.setx
|
|
else:
|
|
func = objy.sety
|
|
|
|
func(SOURCE)
|
|
|
|
SINK(objx.x) # $ flow="SOURCE, l:-2 -> objx.x"
|
|
SINK_F(objx.y)
|
|
SINK_F(objy.x)
|
|
SINK(objy.y, not_present_at_runtime=True) # $ flow="SOURCE, l:-5 -> objy.y"
|
|
|
|
|
|
@expects(8) # $ unresolved_call=expects(..) unresolved_call=expects(..)(..)
|
|
def test_potential_crosstalk_same_name(cond=True):
|
|
objx = CrosstalkTestX()
|
|
SINK_F(objx.x)
|
|
SINK_F(objx.y)
|
|
|
|
objy = CrosstalkTestY()
|
|
SINK_F(objy.x)
|
|
SINK_F(objy.y)
|
|
|
|
if cond:
|
|
func = objx.setvalue
|
|
else:
|
|
func = objy.setvalue
|
|
|
|
func(SOURCE)
|
|
|
|
SINK(objx.x) # $ flow="SOURCE, l:-2 -> objx.x"
|
|
SINK_F(objx.y)
|
|
SINK_F(objy.x)
|
|
SINK(objy.y, not_present_at_runtime=True) # $ flow="SOURCE, l:-5 -> objy.y"
|
|
|
|
|
|
@expects(10) # $ unresolved_call=expects(..) unresolved_call=expects(..)(..)
|
|
def test_potential_crosstalk_same_name_object_reference(cond=True):
|
|
objx = CrosstalkTestX()
|
|
SINK_F(objx.x)
|
|
SINK_F(objx.y)
|
|
|
|
objy = CrosstalkTestY()
|
|
SINK_F(objy.x)
|
|
SINK_F(objy.y)
|
|
|
|
if cond:
|
|
obj = objx
|
|
else:
|
|
obj = objy
|
|
|
|
obj.setvalue(SOURCE)
|
|
|
|
SINK(objx.x) # $ MISSING: flow="SOURCE, l:-2 -> objx.x"
|
|
SINK_F(objx.y)
|
|
SINK_F(objy.x)
|
|
SINK(objy.y, not_present_at_runtime=True) # $ MISSING: flow="SOURCE, l:-5 -> objy.y"
|
|
|
|
SINK(obj.x) # $ flow="SOURCE, l:-7 -> obj.x"
|
|
SINK(obj.y, not_present_at_runtime=True) # $ flow="SOURCE, l:-8 -> obj.y"
|
|
|
|
|
|
@expects(4) # $ unresolved_call=expects(..) unresolved_call=expects(..)(..)
|
|
def test_potential_crosstalk_same_class(cond=True):
|
|
objx1 = CrosstalkTestX()
|
|
SINK_F(objx1.x)
|
|
|
|
objx2 = CrosstalkTestX()
|
|
SINK_F(objx2.x)
|
|
|
|
if cond:
|
|
func = objx1.setvalue
|
|
else:
|
|
func = objx2.do_nothing
|
|
|
|
# We want to ensure that objx2.x does not end up getting tainted, since that would
|
|
# be cross-talk between the self arguments are their functions.
|
|
func(SOURCE)
|
|
|
|
SINK(objx1.x) # $ flow="SOURCE, l:-2 -> objx1.x"
|
|
SINK_F(objx2.x)
|
|
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Global scope
|
|
# ------------------------------------------------------------------------------
|
|
|
|
# since these are defined on global scope, and we still want to run them with
|
|
# `validTest.py`, we have them defined in a different file, and have hardcoded this
|
|
# number that reflects how many OK we expect to see ... Not an ideal solution, but at
|
|
# least we know that the tests are actually valid.
|
|
#
|
|
# Notice that since the tests are run in a random order, we cannot split the global
|
|
# scope tests into multiple functions, since we wouldn't know which one did the initial
|
|
# import that does all the printing :|
|
|
|
|
@expects(18 + 2) # $ unresolved_call=expects(..) unresolved_call=expects(..)(..)
|
|
def test_global_scope():
|
|
import fieldflow.test_global
|
|
|
|
fieldflow.test_global.func_defined_before() # $ unresolved_call=fieldflow.test_global.func_defined_before()
|
|
fieldflow.test_global.func_defined_after() # $ unresolved_call=fieldflow.test_global.func_defined_after()
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Global flow cases that doesn't work in this file, but works in test_global.py
|
|
# ------------------------------------------------------------------------------
|
|
|
|
# --------------------------------------
|
|
# method calls _before_ those ifs
|
|
# --------------------------------------
|
|
|
|
# def test_indirect_assign_method():
|
|
myobj2 = MyObj("OK")
|
|
myobj2.setFoo(SOURCE)
|
|
SINK(myobj2.foo) # $ flow="SOURCE, l:-1 -> myobj2.foo"
|
|
|
|
# def test_nested_obj_method():
|
|
x2 = SOURCE
|
|
a2 = NestedObj()
|
|
a2.getObj().foo = x2
|
|
SINK(a2.obj.foo) # $ flow="SOURCE, l:-3 -> a2.obj.foo"
|
|
|
|
|
|
# --------------------------------------
|
|
# using constructor
|
|
# --------------------------------------
|
|
|
|
# def test_constructor_assign():
|
|
obj2 = MyObj(SOURCE)
|
|
SINK(obj2.foo) # $ flow="SOURCE, l:-1 -> obj2.foo"
|
|
|
|
# apparently these if statements below makes a difference :O
|
|
# but one is not enough
|
|
cond = os.urandom(1)[0] > 128 # $ unresolved_call=os.urandom(..)
|
|
|
|
if cond:
|
|
pass
|
|
|
|
# def test_constructor_assign():
|
|
obj2 = MyObj(SOURCE)
|
|
SINK(obj2.foo) # $ flow="SOURCE, l:-1 -> obj2.foo"
|
|
|
|
if cond:
|
|
pass
|
|
|
|
# def test_constructor_assign():
|
|
obj2 = MyObj(SOURCE)
|
|
SINK(obj2.foo) # $ flow="SOURCE, l:-1 -> obj2.foo"
|
|
|
|
# def test_constructor_assign_kw():
|
|
obj3 = MyObj(foo=SOURCE)
|
|
SINK(obj3.foo) # $ flow="SOURCE, l:-1 -> obj3.foo"
|
|
|
|
# def test_fields():
|
|
SINK(fields_with_local_flow(SOURCE)) # $ flow="SOURCE -> fields_with_local_flow(..)"
|
|
|
|
# --------------------------------------
|
|
# method calls _after_ those ifs
|
|
# --------------------------------------
|
|
|
|
# def test_indirect_assign_method():
|
|
myobj2 = MyObj("OK")
|
|
myobj2.setFoo(SOURCE)
|
|
SINK(myobj2.foo) # $ flow="SOURCE, l:-1 -> myobj2.foo"
|
|
|
|
# def test_nested_obj_method():
|
|
x2 = SOURCE
|
|
a2 = NestedObj()
|
|
a2.getObj().foo = x2
|
|
SINK(a2.obj.foo) # $ flow="SOURCE, l:-3 -> a2.obj.foo"
|