Compare commits

..

1 Commits

Author SHA1 Message Date
Owen Mansel-Chan
7126b95b16 Add test with MISSING alerts 2026-06-11 07:22:17 +02:00
6 changed files with 45 additions and 74 deletions

View File

@@ -1,4 +0,0 @@
---
category: minorAnalysis
---
* Python type tracking now follows values stored in instance attributes such as `self.attr` across instance methods on the same class. As a result, analysis is more likely to recognize user-defined objects that are stored on `self` and used later in other methods, which may produce additional results.

View File

@@ -172,8 +172,6 @@ module TypeTrackingInput implements Shared::TypeTrackingInput<Location> {
/** Holds if there is a level step from `nodeFrom` to `nodeTo`, which does not depend on the call graph. */ /** Holds if there is a level step from `nodeFrom` to `nodeTo`, which does not depend on the call graph. */
predicate levelStepNoCall(Node nodeFrom, LocalSourceNode nodeTo) { predicate levelStepNoCall(Node nodeFrom, LocalSourceNode nodeTo) {
TypeTrackerSummaryFlow::levelStepNoCall(nodeFrom, nodeTo) TypeTrackerSummaryFlow::levelStepNoCall(nodeFrom, nodeTo)
or
localFieldStep(nodeFrom, nodeTo)
} }
/** /**
@@ -319,51 +317,6 @@ module TypeTrackingInput implements Shared::TypeTrackingInput<Location> {
) )
} }
/**
* Holds if `ref` accesses attribute `attr` of `self`, where `self` is the first
* parameter of an instance method of `cls` (i.e. an access of the form `self.attr`).
*
* Static methods and class methods are excluded, since their first parameter is not a
* `self` instance reference.
*/
private predicate selfAttrRef(Class cls, string attr, DataFlowPublic::AttrRef ref) {
exists(Function method, Name selfUse |
method = cls.getAMethod() and
not DataFlowDispatch::isStaticmethod(method) and
not DataFlowDispatch::isClassmethod(method) and
selfUse.getVariable() = method.getArg(0).(Name).getVariable() and
ref.getObject().asCfgNode().getNode() = selfUse and
ref.mayHaveAttributeName(attr)
)
}
/**
* Holds if `nodeFrom` is written to attribute `self.attr` in some instance method of a
* class, and `nodeTo` reads attribute `self.attr` in some (possibly different) instance
* method of the same class.
*
* This models flow through instance attributes (`self.foo`): a value stored into
* `self.foo` in one method can be read from `self.foo` in another method. Type-tracking
* handles the store and read steps via `AttrWrite`/`AttrRead`, but on its own it cannot
* relate the `self` of the writing method to the `self` of the reading method. Following
* the approach used for Ruby and JavaScript, we model this directly as a level step from
* the written value to the read reference, for any pair of methods on the class (not
* just from `__init__`).
*
* This is an over-approximation: it is instance-insensitive (it does not distinguish
* between different instances of the same class) and order-insensitive (it does not
* require the write to happen before the read), matching the precision of
* instance-attribute handling for Ruby and JavaScript.
*/
private predicate localFieldStep(Node nodeFrom, LocalSourceNode nodeTo) {
exists(Class cls, string attr, DataFlowPublic::AttrWrite write, DataFlowPublic::AttrRead read |
selfAttrRef(cls, attr, write) and
nodeFrom = write.getValue() and
selfAttrRef(cls, attr, read) and
nodeTo = read
)
}
/** /**
* Holds if data can flow from `node1` to `node2` in a way that discards call contexts. * Holds if data can flow from `node1` to `node2` in a way that discards call contexts.
*/ */

View File

@@ -1,6 +1,9 @@
testFailures testFailures
debug_callableNotUnique debug_callableNotUnique
pointsTo_found_typeTracker_notFound pointsTo_found_typeTracker_notFound
| code/class_attr_assign.py:10:9:10:27 | ControlFlowNode for Attribute() | my_func |
| code/class_attr_assign.py:11:9:11:25 | ControlFlowNode for Attribute() | my_func |
| code/class_attr_assign.py:26:9:26:25 | ControlFlowNode for Attribute() | DummyObject.method |
| code/class_super.py:50:1:50:6 | ControlFlowNode for Attribute() | outside_def | | code/class_super.py:50:1:50:6 | ControlFlowNode for Attribute() | outside_def |
| code/conditional_in_argument.py:18:5:18:11 | ControlFlowNode for Attribute() | X.bar | | code/conditional_in_argument.py:18:5:18:11 | ControlFlowNode for Attribute() | X.bar |
| code/func_defined_outside_class.py:21:1:21:11 | ControlFlowNode for Attribute() | A.foo | | code/func_defined_outside_class.py:21:1:21:11 | ControlFlowNode for Attribute() | A.foo |

View File

@@ -7,8 +7,8 @@ class Foo(object):
self.direct_ref = my_func self.direct_ref = my_func
def later(self): def later(self):
self.indirect_ref() # $ pt=my_func tt=my_func self.indirect_ref() # $ pt=my_func MISSING: tt=my_func
self.direct_ref() # $ pt=my_func tt=my_func self.direct_ref() # $ pt=my_func MISSING: tt=my_func
foo = Foo(my_func) # $ tt=Foo.__init__ foo = Foo(my_func) # $ tt=Foo.__init__
foo.later() # $ pt,tt=Foo.later foo.later() # $ pt,tt=Foo.later
@@ -23,7 +23,7 @@ class Bar(object):
self.obj = DummyObject() self.obj = DummyObject()
def later(self): def later(self):
self.obj.method() # $ pt=DummyObject.method tt=DummyObject.method self.obj.method() # $ pt=DummyObject.method MISSING: tt=DummyObject.method
bar = Bar(my_func) # $ tt=Bar.__init__ bar = Bar(my_func) # $ tt=Bar.__init__

View File

@@ -151,10 +151,10 @@ class MyClass2(object):
self.foo = tracked # $ tracked=foo tracked self.foo = tracked # $ tracked=foo tracked
def print_foo(self): # $ MISSING: tracked=foo def print_foo(self): # $ MISSING: tracked=foo
print(self.foo) # $ tracked MISSING: tracked=foo print(self.foo) # $ MISSING: tracked=foo tracked
def possibly_uncalled_method(self): # $ MISSING: tracked=foo def possibly_uncalled_method(self): # $ MISSING: tracked=foo
print(self.foo) # $ tracked MISSING: tracked=foo print(self.foo) # $ MISSING: tracked=foo tracked
instance = MyClass2() instance = MyClass2()
print(instance.foo) # $ MISSING: tracked=foo tracked print(instance.foo) # $ MISSING: tracked=foo tracked

View File

@@ -9,28 +9,47 @@ cursor.executemany("some sql", (42,)) # $ getSql="some sql"
cursor.close() cursor.close()
# Connection stored in a class attribute (`self._conn`) and used in another method. # ---------------------------------------------------------------------------
# # Connection stored in a class attribute and accessed via various patterns
# This is detected because type tracking includes a level step modelling flow through # ---------------------------------------------------------------------------
# instance attributes: a value written to `self._conn` in one method (here `__init__`) can
# be read back from `self._conn` (directly or via a getter) in any other method on the same
# class. This follows the same approach used for instance fields in Ruby and JavaScript. class WrapperA:
class Database:
def __init__(self): def __init__(self):
self._conn = dbapi.connect(address="hostname", port=300, user="username") self._conn = dbapi.connect(address="hostname", port=300, user="username", pass_arg="testpass")
def get_connection(self): def get_connection(self):
return self._conn return self._conn
def run_via_getter(self):
conn = self.get_connection()
cursor = conn.cursor()
cursor.execute("getter sql") # $ getSql="getter sql"
def run_direct(self): # Getter called on a fresh constructor result
self._conn.execute("direct sql") # $ getSql="direct sql" conn_a1 = WrapperA().get_connection()
cursor_a1 = conn_a1.cursor()
cursor_a1.execute("some sql", (42,)) # $ MISSING: getSql="some sql"
# Getter called via a stored wrapper instance
wrapper_instance = WrapperA()
conn_a2 = wrapper_instance.get_connection()
cursor_a2 = conn_a2.cursor()
cursor_a2.execute("some sql", (42,)) # $ MISSING: getSql="some sql"
# Direct attribute access on a fresh constructor result
conn_b = WrapperA()._conn
cursor_b = conn_b.cursor()
cursor_b.execute("some sql", (42,)) # $ MISSING: getSql="some sql"
db = Database() class WrapperB:
db.run_via_getter() """Stores the connection under a different attribute name."""
db.run_direct()
def __init__(self):
self._hana = dbapi.connect(address="hostname", port=300, user="username", pass_arg="testpass")
def cursor(self):
return self._hana.cursor()
# Direct attribute access on a stored instance (mirrors hdb_con3 in the issue)
conn_c = WrapperB()._hana
cursor_c = conn_c.cursor()
cursor_c.execute("some sql", (42,)) # $ MISSING: getSql="some sql"