Skip to content

Commit 415857c

Browse files
committed
Fix FP for py/should-use-with
1 parent d721446 commit 415857c

3 files changed

Lines changed: 34 additions & 6 deletions

File tree

python/ql/src/Statements/ShouldUseWithStatement.ql

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
*/
1414

1515
import python
16+
private import semmle.python.dataflow.new.DataFlow
1617
private import semmle.python.dataflow.new.internal.DataFlowDispatch
1718

1819
predicate calls_close(Call c) { exists(Attribute a | c.getFunc() = a and a.getName() = "close") }
@@ -23,11 +24,37 @@ predicate only_stmt_in_finally(Try t, Call c) {
2324
)
2425
}
2526

26-
from Call close, Try t, Class cls
27+
/**
28+
* Holds if `read` is an attribute read that re-exposes an instance of `cls` held in an
29+
* instance attribute, for example `BufferedRWPair.reader`.
30+
*
31+
* Instance-attribute type tracking can launder such an instance out of a field. The object
32+
* is owned by the enclosing instance, so its lifetime spans that instance and cannot be
33+
* expressed with a `with` statement; closing it in a `finally` block is therefore not a
34+
* candidate for refactoring.
35+
*/
36+
private predicate launderedAttrRead(Class cls, DataFlow::AttrRead read) {
37+
read = classInstanceTracker(cls)
38+
}
39+
40+
/** Type tracking forward from an attribute read that re-exposes an instance held in a field. */
41+
private DataFlow::TypeTrackingNode launderedInstance(Class cls, DataFlow::TypeTracker t) {
42+
t.start() and
43+
launderedAttrRead(cls, result)
44+
or
45+
exists(DataFlow::TypeTracker t2 | result = launderedInstance(cls, t2).track(t2, t))
46+
}
47+
48+
from Call close, Try t, Class cls, DataFlow::Node closeTarget
2749
where
2850
only_stmt_in_finally(t, close) and
2951
calls_close(close) and
30-
classInstanceTracker(cls).asExpr() = close.getFunc().(Attribute).getObject() and
52+
closeTarget.asExpr() = close.getFunc().(Attribute).getObject() and
53+
closeTarget = classInstanceTracker(cls) and
54+
// Don't report closing a resource that is held in an instance attribute (e.g. `self.reader`).
55+
// Such flow is introduced by instance-attribute type tracking; the object's lifetime is tied
56+
// to the enclosing instance and cannot be expressed with a `with` statement.
57+
not launderedInstance(cls, DataFlow::TypeTracker::end()).flowsTo(closeTarget) and
3158
DuckTyping::isContextManager(cls)
3259
select close,
3360
"Instance of context-manager class $@ is closed in a finally block. Consider using 'with' statement.",
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
11
| test.py:168:9:168:17 | Attribute() | Instance of context-manager class $@ is closed in a finally block. Consider using 'with' statement. | test.py:151:1:151:17 | Class CM | CM |
2-
| test.py:182:13:182:26 | Attribute() | Instance of context-manager class $@ is closed in a finally block. Consider using 'with' statement. | test.py:151:1:151:17 | Class CM | CM |

python/ql/test/query-tests/Statements/general/test.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,8 +167,10 @@ def no_with():
167167
finally:
168168
f.close()
169169

170-
# Should use context manager, with the resource held in an instance attribute
171-
# (caught via instance-attribute type tracking).
170+
# Should not use a 'with' statement here: the resource is held in an instance
171+
# attribute, so its lifetime spans the enclosing instance and cannot be expressed
172+
# with a 'with' statement. Instance-attribute type tracking can launder the
173+
# instance out of the field, but this must not be reported.
172174
class HoldsCM(object):
173175

174176
def __init__(self):
@@ -179,7 +181,7 @@ def no_with_attribute(self):
179181
self.f.write("Hello ")
180182
self.f.write(" World\n")
181183
finally:
182-
self.f.close()
184+
self.f.close() # No alert: re-exposes a field, not a local resource.
183185

184186
#Assert without side-effect
185187
def assert_ok(seq):

0 commit comments

Comments
 (0)