fix: 第三方库压测 (aiohttp) 修复 async/星号/while True/嵌套推导式
- 星号解包 a, *rest = ... 漏收集 → E002 (aiohttp multipart parts); 修复 _collect_target/_collect_tuple_names/_flow_collect_stores 三处 - async with / async for 完全未处理 → E004 一片 (aiohttp client resp/msg); 修复 L1 visit_AsyncWith/AsyncFor + L3 (With,AsyncWith)/(For,AsyncFor) 分支 - while True: 循环体必执行至少一次 → 其赋值确定 (client_reqrep message) - 嵌套推导式按 gen 交错 (内层 iter 可用外层 target) (cookiejar cookie); 推导式 if 的 walrus 先于 elt 登记 (cookiejar key) - 效果: aiohttp 67 文件 E004 59→2, E002 8→1; 新增 6 回归测试; 299 passed
This commit is contained in:
1 parent
7494d57aec
commit
082efd62cc
2 files changed
+103
-12
No files matched your search
+31
-12
@@ -192,9 +192,11 @@ class _ModuleCollector(ast.NodeVisitor):
|
|||||||
self._collect_uses(node.value)
|
self._collect_uses(node.value)
|
||||||
|
|
||||||
def _collect_tuple_names(self, target, scope):
|
def _collect_tuple_names(self, target, scope):
|
||||||
"""递归收集赋值目标名字 (含 a, b = ... 解包)"""
|
"""递归收集赋值目标名字 (含 a, b = ... / a, *rest = ... 解包)"""
|
||||||
if isinstance(target, ast.Name):
|
if isinstance(target, ast.Name):
|
||||||
scope.setdefault(target.id, None)
|
scope.setdefault(target.id, None)
|
||||||
|
elif isinstance(target, ast.Starred):
|
||||||
|
self._collect_tuple_names(target.value, scope)
|
||||||
elif isinstance(target, (ast.Tuple, ast.List)):
|
elif isinstance(target, (ast.Tuple, ast.List)):
|
||||||
for elt in target.elts:
|
for elt in target.elts:
|
||||||
self._collect_tuple_names(elt, scope)
|
self._collect_tuple_names(elt, scope)
|
||||||
@@ -290,6 +292,8 @@ class _ScopeAnalyzer(ast.NodeVisitor):
|
|||||||
def _collect_target(self, target: ast.AST, scope: Dict[str, str]):
|
def _collect_target(self, target: ast.AST, scope: Dict[str, str]):
|
||||||
if isinstance(target, ast.Name):
|
if isinstance(target, ast.Name):
|
||||||
scope[target.id] = "变量"
|
scope[target.id] = "变量"
|
||||||
|
elif isinstance(target, ast.Starred):
|
||||||
|
self._collect_target(target.value, scope)
|
||||||
elif isinstance(target, (ast.Tuple, ast.List)):
|
elif isinstance(target, (ast.Tuple, ast.List)):
|
||||||
for elt in target.elts:
|
for elt in target.elts:
|
||||||
self._collect_target(elt, scope)
|
self._collect_target(elt, scope)
|
||||||
@@ -357,6 +361,11 @@ class _ScopeAnalyzer(ast.NodeVisitor):
|
|||||||
for n in ast.walk(gen.target):
|
for n in ast.walk(gen.target):
|
||||||
if isinstance(n, ast.Name):
|
if isinstance(n, ast.Name):
|
||||||
self._declare(n.id)
|
self._declare(n.id)
|
||||||
|
# 推导式 if 条件里的 walrus (key := ...) 先于 elt 求值 → 提前登记
|
||||||
|
for cond in gen.ifs:
|
||||||
|
for n in ast.walk(cond):
|
||||||
|
if isinstance(n, ast.NamedExpr) and isinstance(n.target, ast.Name):
|
||||||
|
self._declare(n.target.id)
|
||||||
self.generic_visit(node)
|
self.generic_visit(node)
|
||||||
|
|
||||||
def visit_ListComp(self, node): self._visit_comprehension(node)
|
def visit_ListComp(self, node): self._visit_comprehension(node)
|
||||||
@@ -412,6 +421,8 @@ class _ScopeAnalyzer(ast.NodeVisitor):
|
|||||||
lst=sub.func.value.id)
|
lst=sub.func.value.id)
|
||||||
self.generic_visit(node)
|
self.generic_visit(node)
|
||||||
|
|
||||||
|
visit_AsyncFor = visit_For
|
||||||
|
|
||||||
def visit_With(self, node: ast.With):
|
def visit_With(self, node: ast.With):
|
||||||
if self.scope_stack:
|
if self.scope_stack:
|
||||||
for item in node.items:
|
for item in node.items:
|
||||||
@@ -424,6 +435,8 @@ class _ScopeAnalyzer(ast.NodeVisitor):
|
|||||||
self._declare(n.id)
|
self._declare(n.id)
|
||||||
self.generic_visit(node)
|
self.generic_visit(node)
|
||||||
|
|
||||||
|
visit_AsyncWith = visit_With
|
||||||
|
|
||||||
def visit_ExceptHandler(self, node: ast.ExceptHandler):
|
def visit_ExceptHandler(self, node: ast.ExceptHandler):
|
||||||
if node.name:
|
if node.name:
|
||||||
if self.scope_stack:
|
if self.scope_stack:
|
||||||
@@ -518,9 +531,11 @@ def _check_syntax(source: str) -> Tuple[List[Dict], Optional[ast.Module]]:
|
|||||||
|
|
||||||
|
|
||||||
def _flow_collect_stores(target, defined: set):
|
def _flow_collect_stores(target, defined: set):
|
||||||
"""登记赋值目标到确定集合"""
|
"""登记赋值目标到确定集合 (含 a, *rest = ... 星号解包)"""
|
||||||
if isinstance(target, ast.Name):
|
if isinstance(target, ast.Name):
|
||||||
defined.add(target.id)
|
defined.add(target.id)
|
||||||
|
elif isinstance(target, ast.Starred):
|
||||||
|
_flow_collect_stores(target.value, defined)
|
||||||
elif isinstance(target, (ast.Tuple, ast.List)):
|
elif isinstance(target, (ast.Tuple, ast.List)):
|
||||||
for elt in target.elts:
|
for elt in target.elts:
|
||||||
_flow_collect_stores(elt, defined)
|
_flow_collect_stores(elt, defined)
|
||||||
@@ -569,20 +584,19 @@ def _flow_check_loads(node, locals_set, defined, analyzer):
|
|||||||
|
|
||||||
|
|
||||||
def _flow_check_comp(node, locals_set, defined, analyzer):
|
def _flow_check_comp(node, locals_set, defined, analyzer):
|
||||||
"""推导式: target 在 elt/条件内确定有值 (Python3 推导式独立作用域, 不泄漏到外层)"""
|
"""推导式: target 在 elt/条件内确定有值 (Python3 推导式独立作用域, 不泄漏到外层)。
|
||||||
|
嵌套推导式按 gen 交错: gen1.iter → gen1.target → gen2.iter(可用外层 target) → ... → ifs → elt"""
|
||||||
d = set(defined)
|
d = set(defined)
|
||||||
for gen in node.generators:
|
for gen in node.generators:
|
||||||
_flow_check_loads(gen.iter, locals_set, d, analyzer)
|
_flow_check_loads(gen.iter, locals_set, d, analyzer)
|
||||||
for gen in node.generators:
|
|
||||||
_flow_collect_stores(gen.target, d)
|
_flow_collect_stores(gen.target, d)
|
||||||
|
for cond in gen.ifs:
|
||||||
|
_flow_check_loads(cond, locals_set, d, analyzer) # walrus 在此登记
|
||||||
if isinstance(node, ast.DictComp):
|
if isinstance(node, ast.DictComp):
|
||||||
_flow_check_loads(node.key, locals_set, d, analyzer)
|
_flow_check_loads(node.key, locals_set, d, analyzer)
|
||||||
_flow_check_loads(node.value, locals_set, d, analyzer)
|
_flow_check_loads(node.value, locals_set, d, analyzer)
|
||||||
else:
|
else:
|
||||||
_flow_check_loads(node.elt, locals_set, d, analyzer)
|
_flow_check_loads(node.elt, locals_set, d, analyzer)
|
||||||
for gen in node.generators:
|
|
||||||
for cond in gen.ifs:
|
|
||||||
_flow_check_loads(cond, locals_set, d, analyzer)
|
|
||||||
|
|
||||||
|
|
||||||
def _match_bound_names(pattern) -> set:
|
def _match_bound_names(pattern) -> set:
|
||||||
@@ -613,11 +627,11 @@ def _match_bound_names(pattern) -> set:
|
|||||||
|
|
||||||
|
|
||||||
def _flow_terminates(stmts) -> bool:
|
def _flow_terminates(stmts) -> bool:
|
||||||
"""语句列表是否必然终止 (末尾 return/raise, 或 if 两分支都终止)"""
|
"""语句列表是否必然终止/不流出 (return/raise/continue/break, 或 if 两分支都终止)"""
|
||||||
if not stmts:
|
if not stmts:
|
||||||
return False
|
return False
|
||||||
last = stmts[-1]
|
last = stmts[-1]
|
||||||
if isinstance(last, (ast.Return, ast.Raise)):
|
if isinstance(last, (ast.Return, ast.Raise, ast.Continue, ast.Break)):
|
||||||
return True
|
return True
|
||||||
if isinstance(last, ast.If):
|
if isinstance(last, ast.If):
|
||||||
return _flow_terminates(last.body) and _flow_terminates(last.orelse)
|
return _flow_terminates(last.body) and _flow_terminates(last.orelse)
|
||||||
@@ -643,7 +657,7 @@ def _flow_statements(stmts, locals_set, defined, analyzer):
|
|||||||
if isinstance(stmt.target, ast.Name):
|
if isinstance(stmt.target, ast.Name):
|
||||||
defined.add(stmt.target.id)
|
defined.add(stmt.target.id)
|
||||||
_flow_check_loads(stmt.value, locals_set, defined, analyzer)
|
_flow_check_loads(stmt.value, locals_set, defined, analyzer)
|
||||||
elif isinstance(stmt, ast.For):
|
elif isinstance(stmt, (ast.For, ast.AsyncFor)):
|
||||||
_flow_check_loads(stmt.iter, locals_set, defined, analyzer)
|
_flow_check_loads(stmt.iter, locals_set, defined, analyzer)
|
||||||
d0 = set(defined)
|
d0 = set(defined)
|
||||||
d_inner = set(defined)
|
d_inner = set(defined)
|
||||||
@@ -661,7 +675,12 @@ def _flow_statements(stmts, locals_set, defined, analyzer):
|
|||||||
_flow_statements(stmt.body, locals_set, d_inner, analyzer)
|
_flow_statements(stmt.body, locals_set, d_inner, analyzer)
|
||||||
d_orelse = set(d0)
|
d_orelse = set(d0)
|
||||||
_flow_statements(stmt.orelse, locals_set, d_orelse, analyzer)
|
_flow_statements(stmt.orelse, locals_set, d_orelse, analyzer)
|
||||||
defined.clear(); defined.update(d_orelse)
|
is_true_loop = isinstance(stmt.test, ast.Constant) and stmt.test.value is True
|
||||||
|
if is_true_loop:
|
||||||
|
# while True: 循环体必执行至少一次 → 循环体赋值确定 (aiohttp message 场景)
|
||||||
|
defined.clear(); defined.update(d_inner)
|
||||||
|
else:
|
||||||
|
defined.clear(); defined.update(d_orelse)
|
||||||
elif isinstance(stmt, ast.If):
|
elif isinstance(stmt, ast.If):
|
||||||
_flow_check_loads(stmt.test, locals_set, defined, analyzer)
|
_flow_check_loads(stmt.test, locals_set, defined, analyzer)
|
||||||
d1 = set(defined)
|
d1 = set(defined)
|
||||||
@@ -699,7 +718,7 @@ def _flow_statements(stmts, locals_set, defined, analyzer):
|
|||||||
d_main = d_else # 正常路径流出 → handler 变量在正常路径不确定
|
d_main = d_else # 正常路径流出 → handler 变量在正常路径不确定
|
||||||
_flow_statements(stmt.finalbody, locals_set, d_main, analyzer) # finally 总是执行
|
_flow_statements(stmt.finalbody, locals_set, d_main, analyzer) # finally 总是执行
|
||||||
defined.clear(); defined.update(d_main)
|
defined.clear(); defined.update(d_main)
|
||||||
elif isinstance(stmt, ast.With):
|
elif isinstance(stmt, (ast.With, ast.AsyncWith)):
|
||||||
for item in stmt.items:
|
for item in stmt.items:
|
||||||
_flow_check_loads(item.context_expr, locals_set, defined, analyzer)
|
_flow_check_loads(item.context_expr, locals_set, defined, analyzer)
|
||||||
if item.optional_vars:
|
if item.optional_vars:
|
||||||
|
|||||||
@@ -500,6 +500,78 @@ class TestDataFlow:
|
|||||||
" return 0\n")
|
" return 0\n")
|
||||||
assert "E004" not in {d["code"] for d in pd.analyze(src, "t.py")}
|
assert "E004" not in {d["code"] for d in pd.analyze(src, "t.py")}
|
||||||
|
|
||||||
|
def test_continue_branch_terminates(self, pd):
|
||||||
|
"""elif 分支 continue → 不流出, 后续 fdata 确定 (requests/models.py 场景)"""
|
||||||
|
src = ("def f(fp):\n"
|
||||||
|
" if isinstance(fp, str):\n"
|
||||||
|
" d = fp\n"
|
||||||
|
" elif hasattr(fp, 'read'):\n"
|
||||||
|
" d = fp.read()\n"
|
||||||
|
" elif fp is None:\n"
|
||||||
|
" continue\n"
|
||||||
|
" else:\n"
|
||||||
|
" d = fp\n"
|
||||||
|
" return d\n")
|
||||||
|
assert "E004" not in {d["code"] for d in pd.analyze(src, "t.py")}
|
||||||
|
|
||||||
|
def test_break_branch_terminates(self, pd):
|
||||||
|
src = ("def f(items):\n"
|
||||||
|
" for x in items:\n"
|
||||||
|
" if x > 10:\n"
|
||||||
|
" break\n"
|
||||||
|
" else:\n"
|
||||||
|
" y = x\n"
|
||||||
|
" return y\n")
|
||||||
|
assert "E004" in {d["code"] for d in pd.analyze(src, "t.py")} # break 路径 y 未定义
|
||||||
|
|
||||||
|
# ── 第三方库压测回归 (async with/for, 星号解包, while True, 嵌套推导式) ──
|
||||||
|
def test_async_with_bind_ok(self, pd):
|
||||||
|
src = ("async def f():\n"
|
||||||
|
" async with get() as resp:\n"
|
||||||
|
" return resp.status\n")
|
||||||
|
assert "E004" not in {d["code"] for d in pd.analyze(src, "t.py")}
|
||||||
|
|
||||||
|
def test_async_for_bind_ok(self, pd):
|
||||||
|
src = ("async def f():\n"
|
||||||
|
" async for msg in ws:\n"
|
||||||
|
" return msg.data\n")
|
||||||
|
assert "E004" not in {d["code"] for d in pd.analyze(src, "t.py")}
|
||||||
|
|
||||||
|
def test_starred_unpack_ok(self, pd):
|
||||||
|
"""a, *rest = ... 星号解包不得误报 (aiohttp multipart parts 场景)"""
|
||||||
|
src = ("def f(h):\n"
|
||||||
|
" a, *parts = h.split(';')\n"
|
||||||
|
" while parts:\n"
|
||||||
|
" parts.pop(0)\n"
|
||||||
|
" return a\n")
|
||||||
|
assert "E002" not in {d["code"] for d in pd.analyze(src, "t.py")}
|
||||||
|
assert "E004" not in {d["code"] for d in pd.analyze(src, "t.py")}
|
||||||
|
|
||||||
|
def test_while_true_body_defines(self, pd):
|
||||||
|
"""while True: 循环体必执行至少一次 → 其赋值确定 (aiohttp client_reqrep message)"""
|
||||||
|
src = ("def f():\n"
|
||||||
|
" while True:\n"
|
||||||
|
" m = read()\n"
|
||||||
|
" if m:\n"
|
||||||
|
" break\n"
|
||||||
|
" return m\n")
|
||||||
|
assert "E004" not in {d["code"] for d in pd.analyze(src, "t.py")}
|
||||||
|
|
||||||
|
def test_nested_comp_outer_target_in_inner_iter(self, pd):
|
||||||
|
"""嵌套推导式: 内层 iter 可用外层 target (aiohttp cookiejar cookie 场景)"""
|
||||||
|
src = ("def f(items):\n"
|
||||||
|
" return [m for (d, p), c in items for name, m in c.items()]\n")
|
||||||
|
assert "E004" not in {d["code"] for d in pd.analyze(src, "t.py")}
|
||||||
|
|
||||||
|
def test_comp_if_walrus_elt_uses(self, pd):
|
||||||
|
"""推导式 if 里的 walrus 先于 elt 求值 (aiohttp cookiejar key 场景)"""
|
||||||
|
src = ("def f(items):\n"
|
||||||
|
" expirations = {}\n"
|
||||||
|
" return [key for (d, p), c in items for name, m in c.items()\n"
|
||||||
|
" if (key := (d, p, name)) in expirations]\n")
|
||||||
|
assert "E002" not in {d["code"] for d in pd.analyze(src, "t.py")}
|
||||||
|
assert "E004" not in {d["code"] for d in pd.analyze(src, "t.py")}
|
||||||
|
|
||||||
|
|
||||||
# ── code_editor 写文件后自动检测集成 ──
|
# ── code_editor 写文件后自动检测集成 ──
|
||||||
class TestAutoDetectIntegration:
|
class TestAutoDetectIntegration:
|
||||||
|
|||||||
Reference in new issue
Block a user