fix: L1 元组解包漏收集 + L3 for-else/try-else 路径误报 (外部真实项目扫描)

- L1: a, b = ... 解包赋值目标未收集 → xq_r/xq_i 等误报 E002 (xiaoli_chat.py 27 条误报归零)
- L3: for-else 的 else 在无 break 时必执行(含 0 次) → 其赋值确定 (renpy gamedir)
- L3: try/except/else: else return 终止 → handler 变量确定 (jinja2 asyncsupport); 无 else → handler 变量正常路径不确定保留 (jinja2 debug 真问题)
- 新增 6 项回归测试; 78 passed; xiaoli 自测 E002 误报 27→0, Ren'Py E004 194→182
This commit is contained in:
shuiyu67 committed 2026-08-07 03:04:48 +08:00
1 parent 08f26cdcfd
commit 9badbf30b7
2 files changed
+84 -17

No files matched your search

+37 -17
View File
@@ -188,10 +188,17 @@ class _ModuleCollector(ast.NodeVisitor):
def visit_Assign(self, node: ast.Assign):
for t in node.targets:
if isinstance(t, ast.Name):
self.module_scope.setdefault(t.id, None)
self._collect_tuple_names(t, self.module_scope)
self._collect_uses(node.value)
def _collect_tuple_names(self, target, scope):
"""递归收集赋值目标名字 (含 a, b = ... 解包)"""
if isinstance(target, ast.Name):
scope.setdefault(target.id, None)
elif isinstance(target, (ast.Tuple, ast.List)):
for elt in target.elts:
self._collect_tuple_names(elt, scope)
def visit_AnnAssign(self, node: ast.AnnAssign):
if isinstance(node.target, ast.Name):
self.module_scope.setdefault(node.target.id, None)
@@ -359,12 +366,12 @@ class _ScopeAnalyzer(ast.NodeVisitor):
# ── 赋值/循环/上下文 ──
def visit_Assign(self, node: ast.Assign):
for t in node.targets:
if isinstance(t, ast.Name):
if self.scope_stack:
self.scope_stack[-1][t.id] = "变量"
if t.id in _BUILTINS:
if self.scope_stack:
self._collect_target(t, self.scope_stack[-1]) # 含 a, b = ... 解包
for n in ast.walk(t):
if isinstance(n, ast.Name) and n.id in _BUILTINS:
self._add("W008", node.lineno, node.col_offset,
extra=f"覆盖内置名: {t.id}", name=t.id)
extra=f"覆盖内置名: {n.id}", name=n.id)
self.generic_visit(node)
def visit_AnnAssign(self, node: ast.AnnAssign):
@@ -597,16 +604,19 @@ def _flow_statements(stmts, locals_set, defined, analyzer):
d_inner = set(defined)
_flow_collect_stores(stmt.target, d_inner) # 循环体内 target 确定有值
_flow_statements(stmt.body, locals_set, d_inner, analyzer)
_flow_statements(stmt.orelse, locals_set, set(d0), analyzer) # orelse 在 0 次循环也执行
# 循环体/orelse 的赋值可能 0 次执行 → 不贡献确定
defined.clear(); defined.update(d0)
d_orelse = set(d0)
_flow_statements(stmt.orelse, locals_set, d_orelse, analyzer)
# for-else: orelse 在循环无 break 时必执行(含 0 次) → 其赋值确定;
# 循环体赋值可能 0 次 → 不贡献
defined.clear(); defined.update(d_orelse)
elif isinstance(stmt, ast.While):
_flow_check_loads(stmt.test, locals_set, defined, analyzer)
d0 = set(defined)
d_inner = set(defined)
_flow_statements(stmt.body, locals_set, d_inner, analyzer)
_flow_statements(stmt.orelse, locals_set, d_inner, analyzer)
defined.clear(); defined.update(d0)
d_orelse = set(d0)
_flow_statements(stmt.orelse, locals_set, d_orelse, analyzer)
defined.clear(); defined.update(d_orelse)
elif isinstance(stmt, ast.If):
_flow_check_loads(stmt.test, locals_set, defined, analyzer)
d1 = set(defined)
@@ -625,14 +635,24 @@ def _flow_statements(stmts, locals_set, defined, analyzer):
defined.clear(); defined.update(d1 & d2) # 都流出 → 交集
elif isinstance(stmt, ast.Try):
d_main = set(defined)
_flow_statements(stmt.body, locals_set, d_main, analyzer)
_flow_statements(stmt.body, locals_set, d_main, analyzer) # 正常完成 → body 赋值确定
d_else = set(d_main)
_flow_statements(stmt.orelse, locals_set, d_else, analyzer)
else_terminates = bool(stmt.orelse) and _flow_terminates(stmt.orelse)
# 异常路径 handler: 仅当正常路径(else)终止时, 异常路径才是唯一流出 → 变量确定
d_handlers = set()
for h in stmt.handlers:
d_h = set(defined)
d_h = set(d_main) # 异常可能发生在 body 任意点之后 → 复用 body 确定集
if h.name:
d_h.add(h.name) # except ... as e: e 在 handler 内确定
d_h.add(h.name)
_flow_statements(h.body, locals_set, d_h, analyzer)
_flow_statements(stmt.orelse, locals_set, d_main, analyzer)
_flow_statements(stmt.finalbody, locals_set, d_main, analyzer)
if not _flow_terminates(h.body):
d_handlers.update(d_h)
if else_terminates:
d_main = d_main | d_handlers # 正常路径终止 → 仅异常路径流出
else:
d_main = d_else # 正常路径流出 → handler 变量在正常路径不确定
_flow_statements(stmt.finalbody, locals_set, d_main, analyzer) # finally 总是执行
defined.clear(); defined.update(d_main)
elif isinstance(stmt, ast.With):
for item in stmt.items:
+47
View File
@@ -425,6 +425,53 @@ class TestDataFlow:
" return 2\n")
assert "E004" not in {d["code"] for d in pd.analyze(src, "t.py")}
# ── 真实项目扫描发现的回归 (元组解包 / for-else) ──
def test_tuple_unpack_no_undefined(self, pd):
"""a, b = ... 解包赋值不得误报 E002 (xiaoli_chat.py RoPE 场景)"""
src = ("def f(x):\n"
" r, i = x.float().unbind(-1)\n"
" return r * i\n")
assert "E002" not in {d["code"] for d in pd.analyze(src, "t.py")}
def test_for_else_fallback(self, pd):
"""for-else: else 在无 break 时必执行(含 0 次) → 变量确定 (renpy gamedir 场景)"""
src = ("def f(cands):\n"
" for g in cands:\n"
" if g:\n"
" break\n"
" else:\n"
" g = 'default'\n"
" return g\n")
assert "E004" not in {d["code"] for d in pd.analyze(src, "t.py")}
def test_for_else_break_keeps_checking(self, pd):
"""循环体内赋值 + 无 orelse → 仍报 (0 次循环风险保留)"""
src = "def f(items):\n for x in items:\n total = x\n return total\n"
assert 4 in code_lines(pd.analyze(src, "t.py"), "E004")
def test_try_else_return_handler_ok(self, pd):
"""except 赋值 + else return 终止 → 仅异常路径流出, 变量确定 (jinja2 asyncsupport)"""
src = ("def g():\n"
" try:\n"
" work()\n"
" except ValueError:\n"
" exc = sys.exc_info()\n"
" else:\n"
" return\n"
" return handle(exc)\n")
assert "E004" not in {d["code"] for d in pd.analyze(src, "t.py")}
def test_try_handler_real_undefined_kept(self, pd):
"""无 else: handler 变量在正常路径不确定 → 保留 E004 (jinja2 debug 真问题)"""
src = ("def d(code, gl, lo):\n"
" try:\n"
" exec(code, gl, lo)\n"
" except ValueError:\n"
" exc = sys.exc_info()\n"
" tb = exc[2].tb_next\n"
" return exc[:2] + (tb,)\n")
assert "E004" in {d["code"] for d in pd.analyze(src, "t.py")}
# ── code_editor 写文件后自动检测集成 ──
class TestAutoDetectIntegration: