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:
1 parent
08f26cdcfd
commit
9badbf30b7
2 files changed
+84
-17
No files matched your search
+37
-17
@@ -188,10 +188,17 @@ class _ModuleCollector(ast.NodeVisitor):
|
|||||||
|
|
||||||
def visit_Assign(self, node: ast.Assign):
|
def visit_Assign(self, node: ast.Assign):
|
||||||
for t in node.targets:
|
for t in node.targets:
|
||||||
if isinstance(t, ast.Name):
|
self._collect_tuple_names(t, self.module_scope)
|
||||||
self.module_scope.setdefault(t.id, None)
|
|
||||||
self._collect_uses(node.value)
|
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):
|
def visit_AnnAssign(self, node: ast.AnnAssign):
|
||||||
if isinstance(node.target, ast.Name):
|
if isinstance(node.target, ast.Name):
|
||||||
self.module_scope.setdefault(node.target.id, None)
|
self.module_scope.setdefault(node.target.id, None)
|
||||||
@@ -359,12 +366,12 @@ class _ScopeAnalyzer(ast.NodeVisitor):
|
|||||||
# ── 赋值/循环/上下文 ──
|
# ── 赋值/循环/上下文 ──
|
||||||
def visit_Assign(self, node: ast.Assign):
|
def visit_Assign(self, node: ast.Assign):
|
||||||
for t in node.targets:
|
for t in node.targets:
|
||||||
if isinstance(t, ast.Name):
|
if self.scope_stack:
|
||||||
if self.scope_stack:
|
self._collect_target(t, self.scope_stack[-1]) # 含 a, b = ... 解包
|
||||||
self.scope_stack[-1][t.id] = "变量"
|
for n in ast.walk(t):
|
||||||
if t.id in _BUILTINS:
|
if isinstance(n, ast.Name) and n.id in _BUILTINS:
|
||||||
self._add("W008", node.lineno, node.col_offset,
|
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)
|
self.generic_visit(node)
|
||||||
|
|
||||||
def visit_AnnAssign(self, node: ast.AnnAssign):
|
def visit_AnnAssign(self, node: ast.AnnAssign):
|
||||||
@@ -597,16 +604,19 @@ def _flow_statements(stmts, locals_set, defined, analyzer):
|
|||||||
d_inner = set(defined)
|
d_inner = set(defined)
|
||||||
_flow_collect_stores(stmt.target, d_inner) # 循环体内 target 确定有值
|
_flow_collect_stores(stmt.target, d_inner) # 循环体内 target 确定有值
|
||||||
_flow_statements(stmt.body, locals_set, d_inner, analyzer)
|
_flow_statements(stmt.body, locals_set, d_inner, analyzer)
|
||||||
_flow_statements(stmt.orelse, locals_set, set(d0), analyzer) # orelse 在 0 次循环也执行
|
d_orelse = set(d0)
|
||||||
# 循环体/orelse 的赋值可能 0 次执行 → 不贡献确定
|
_flow_statements(stmt.orelse, locals_set, d_orelse, analyzer)
|
||||||
defined.clear(); defined.update(d0)
|
# for-else: orelse 在循环无 break 时必执行(含 0 次) → 其赋值确定;
|
||||||
|
# 循环体赋值可能 0 次 → 不贡献
|
||||||
|
defined.clear(); defined.update(d_orelse)
|
||||||
elif isinstance(stmt, ast.While):
|
elif isinstance(stmt, ast.While):
|
||||||
_flow_check_loads(stmt.test, locals_set, defined, analyzer)
|
_flow_check_loads(stmt.test, locals_set, defined, analyzer)
|
||||||
d0 = set(defined)
|
d0 = set(defined)
|
||||||
d_inner = set(defined)
|
d_inner = set(defined)
|
||||||
_flow_statements(stmt.body, locals_set, d_inner, analyzer)
|
_flow_statements(stmt.body, locals_set, d_inner, analyzer)
|
||||||
_flow_statements(stmt.orelse, locals_set, d_inner, analyzer)
|
d_orelse = set(d0)
|
||||||
defined.clear(); defined.update(d0)
|
_flow_statements(stmt.orelse, locals_set, d_orelse, analyzer)
|
||||||
|
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)
|
||||||
@@ -625,14 +635,24 @@ def _flow_statements(stmts, locals_set, defined, analyzer):
|
|||||||
defined.clear(); defined.update(d1 & d2) # 都流出 → 交集
|
defined.clear(); defined.update(d1 & d2) # 都流出 → 交集
|
||||||
elif isinstance(stmt, ast.Try):
|
elif isinstance(stmt, ast.Try):
|
||||||
d_main = set(defined)
|
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:
|
for h in stmt.handlers:
|
||||||
d_h = set(defined)
|
d_h = set(d_main) # 异常可能发生在 body 任意点之后 → 复用 body 确定集
|
||||||
if h.name:
|
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(h.body, locals_set, d_h, analyzer)
|
||||||
_flow_statements(stmt.orelse, locals_set, d_main, analyzer)
|
if not _flow_terminates(h.body):
|
||||||
_flow_statements(stmt.finalbody, locals_set, d_main, analyzer)
|
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)
|
defined.clear(); defined.update(d_main)
|
||||||
elif isinstance(stmt, ast.With):
|
elif isinstance(stmt, ast.With):
|
||||||
for item in stmt.items:
|
for item in stmt.items:
|
||||||
|
|||||||
@@ -425,6 +425,53 @@ class TestDataFlow:
|
|||||||
" return 2\n")
|
" return 2\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")}
|
||||||
|
|
||||||
|
# ── 真实项目扫描发现的回归 (元组解包 / 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 写文件后自动检测集成 ──
|
# ── code_editor 写文件后自动检测集成 ──
|
||||||
class TestAutoDetectIntegration:
|
class TestAutoDetectIntegration:
|
||||||
|
|||||||
Reference in new issue
Block a user