From 9badbf30b7c742d9eddf586c7f7274042649e9d6 Mon Sep 17 00:00:00 2001 From: shuiyu67 Date: Fri, 7 Aug 2026 03:04:48 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20L1=20=E5=85=83=E7=BB=84=E8=A7=A3?= =?UTF-8?q?=E5=8C=85=E6=BC=8F=E6=94=B6=E9=9B=86=20+=20L3=20for-else/try-el?= =?UTF-8?q?se=20=E8=B7=AF=E5=BE=84=E8=AF=AF=E6=8A=A5=20(=E5=A4=96=E9=83=A8?= =?UTF-8?q?=E7=9C=9F=E5=AE=9E=E9=A1=B9=E7=9B=AE=E6=89=AB=E6=8F=8F)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- plugins/py_detect.py | 54 ++++++++++++++++++++++++++++------------- tests/test_py_detect.py | 47 +++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 17 deletions(-) diff --git a/plugins/py_detect.py b/plugins/py_detect.py index 1df898b..70182a8 100644 --- a/plugins/py_detect.py +++ b/plugins/py_detect.py @@ -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: diff --git a/tests/test_py_detect.py b/tests/test_py_detect.py index 69b7d8a..a4aaecb 100644 --- a/tests/test_py_detect.py +++ b/tests/test_py_detect.py @@ -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: