Skip to content

Commit 8b472e6

Browse files
Your Nameclaude
andcommitted
fix(optional_dereference): suppress string-concat URL in .get() FP
Tornado test client uses self.get(\"/path?\" + params) where the URL is a BinOp (string concatenation). Walk to the leftmost operand — if it is a string literal starting with \"/\" or \"://\", treat the call as an HTTP request rather than a dict.get() returning Optional. Eliminates 27 false positives in mher/flower (Celery monitoring UI). Adds test: test_tornado_test_client_get_string_concat_not_flagged (169 total). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent c3ccbf9 commit 8b472e6

2 files changed

Lines changed: 39 additions & 0 deletions

File tree

failure_mode.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,21 @@ def visit_Assign(self, node):
282282
)
283283
):
284284
return
285+
# HTTP client .get("/path?" + params) — string concat where
286+
# the leftmost literal looks like a URL path or absolute URL.
287+
if len(call_args) >= 1 and isinstance(call_args[0], _ast.BinOp):
288+
left = call_args[0].left
289+
while isinstance(left, _ast.BinOp):
290+
left = left.left
291+
if (
292+
isinstance(left, _ast.Constant)
293+
and isinstance(left.value, str)
294+
and (
295+
left.value.startswith("/")
296+
or "://" in left.value
297+
)
298+
):
299+
return
285300
# Known HTTP client receiver names: requests.get(), session.get(),
286301
# self.client.get() (Django test client), async_client.get(), etc.
287302
_HTTP_CLIENTS = frozenset(

test_checker.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -669,6 +669,30 @@ def test_it(self):
669669
assert not v, "self.client.get() (Django test client) must not be flagged as optional"
670670

671671

672+
def test_tornado_test_client_get_string_concat_not_flagged(tmp_path):
673+
# Corpus: mher/flower — r = self.get('/api/tasks?' + '&'.join(...))
674+
# Tornado AsyncHTTPTestCase.get() returns HTTPResponse, never None.
675+
# String-concat URL: leftmost literal starts with '/'.
676+
_write_src(
677+
tmp_path,
678+
"test_tasks.py",
679+
"""
680+
import json
681+
682+
class TaskTest:
683+
def test_list(self):
684+
params = dict(limit=4, offset=0)
685+
r = self.get('/api/tasks?' + '&'.join(
686+
'%s=%s' % x for x in params.items()))
687+
table = json.loads(r.body.decode('utf-8'))
688+
self.assertEqual(200, r.code)
689+
""",
690+
)
691+
violations = check_codebase(tmp_path)
692+
v = [v for v in violations if v.context == "optional_dereference"]
693+
assert not v, "self.get('/path?' + params) Tornado test client must not be flagged as optional"
694+
695+
672696
def test_ternary_guard_not_flagged(tmp_path):
673697
# Corpus: paperless-ngx/suitenumerique — request.user if request else None
674698
# When `request` is tested as the ternary condition, access in the body is safe.

0 commit comments

Comments
 (0)