fix: 标准库压测修复 (walrus 登记 + match/case 绑定 + import* 提示)

- _flow_check_loads: NamedExpr(walrus) 先登记 target → ast.py end_lineno / doctest.py loader 误报消除
- _match_bound_names: match/case 模式绑定变量收集 (MatchAs/MatchStar/MatchMapping/MatchSequence/MatchClass), L1+L3 双收集 → dataclasses iterable 误报消除
- import * 文件: E002 补提示「可能来自星号导入」 (ast.py Expression 等)
- 剩余 E004 为固有盲区: 数值条件关联(difflib best_ratio<cutoff 恒真) / 闭包 / locals() 防御, 需符号执行级别, 接受
- 新增 3 项回归测试; 291 passed
This commit is contained in:
shuiyu67 committed 2026-08-07 03:16:43 +08:00
1 parent 9badbf30b7
commit 7494d57aec
2 files changed
+88

No files matched your search

+60
View File
@@ -219,6 +219,12 @@ class _ModuleCollector(ast.NodeVisitor):
self.module_scope.setdefault(node.name, None)
self.generic_visit(node)
def visit_Match(self, node: ast.Match):
for case in node.cases:
for n in _match_bound_names(case.pattern):
self.module_scope.setdefault(n, None)
self.generic_visit(node)
def _first_name(self, target) -> Optional[str]:
if isinstance(target, ast.Name):
return target.id
@@ -429,6 +435,12 @@ class _ScopeAnalyzer(ast.NodeVisitor):
self._add("W003", node.lineno, 0)
self.generic_visit(node)
def visit_Match(self, node: ast.Match):
for case in node.cases:
for n in _match_bound_names(case.pattern):
self._declare(n)
self.generic_visit(node)
# ── 表达式检测 ──
def visit_Name(self, node: ast.Name):
if isinstance(node.ctx, ast.Load):
@@ -546,6 +558,12 @@ def _flow_check_loads(node, locals_set, defined, analyzer):
if isinstance(node, ast.Name):
_flow_maybe_report(node, locals_set, defined, analyzer)
return
if isinstance(node, ast.NamedExpr):
# walrus: (x := expr) 先赋值再使用 → 登记 target
if isinstance(node.target, ast.Name):
defined.add(node.target.id)
_flow_check_loads(node.value, locals_set, defined, analyzer)
return
for child in ast.iter_child_nodes(node):
_flow_check_loads(child, locals_set, defined, analyzer)
@@ -567,6 +585,33 @@ def _flow_check_comp(node, locals_set, defined, analyzer):
_flow_check_loads(cond, locals_set, d, analyzer)
def _match_bound_names(pattern) -> set:
"""收集 match/case 模式里绑定的名字 (case 1 | 2 as x / case [a, b] / case {**rest} 等)"""
names = set()
def walk(p):
if p is None:
return
if isinstance(p, ast.MatchAs):
if p.name:
names.add(p.name)
walk(p.pattern)
elif isinstance(p, ast.MatchStar):
if p.name:
names.add(p.name)
elif isinstance(p, ast.MatchMapping):
if p.rest:
names.add(p.rest)
for v in p.patterns:
walk(v)
elif isinstance(p, (ast.MatchSequence, ast.MatchClass)):
for sub in p.patterns:
walk(sub)
walk(pattern)
return names
def _flow_terminates(stmts) -> bool:
"""语句列表是否必然终止 (末尾 return/raise, 或 if 两分支都终止)"""
if not stmts:
@@ -670,6 +715,15 @@ def _flow_statements(stmts, locals_set, defined, analyzer):
for t in stmt.targets:
if isinstance(t, ast.Name):
defined.discard(t.id)
elif isinstance(stmt, ast.Match):
_flow_check_loads(stmt.subject, locals_set, defined, analyzer)
for case in stmt.cases:
d_case = set(defined)
for n in _match_bound_names(case.pattern):
d_case.add(n)
_flow_check_loads(case.guard, locals_set, d_case, analyzer)
_flow_statements(case.body, locals_set, d_case, analyzer)
# match 后绑定变量不确定 (哪个 case 匹配未知) → defined 不变
elif isinstance(stmt, ast.Expr):
_flow_check_loads(stmt.value, locals_set, defined, analyzer)
@@ -769,6 +823,12 @@ def analyze(source: str, filename: str = "<string>") -> List[Dict]:
_dataflow_check(tree, analyzer) # L3: 数据流 (部分路径未定义)
issues.extend(analyzer.issues) # L1+L3 只 extend 一次, 避免重复
issues = _jedi_validate(source, filename, issues) # L2: 语义二次确认 (可选)
# import * 的文件: E002 可能是星号导入的名字 (静态无法展开) → 补提示
if collector.import_star_lines:
hint = "(本文件有 from x import *——该名字若来自星号导入,请显式导入以消除此提示)"
for d in issues:
if d["code"] == "E002":
d["fix"] = (d.get("fix", "").rstrip() + hint)
return issues
+28
View File
@@ -472,6 +472,34 @@ class TestDataFlow:
" return exc[:2] + (tb,)\n")
assert "E004" in {d["code"] for d in pd.analyze(src, "t.py")}
# ── 标准库压测发现的回归 (walrus / match-case) ──
def test_walrus_in_condition_ok(self, pd):
"""(x := ...) 在 if 条件里 → x 在 if 体内确定 (ast.py end_lineno 场景)"""
src = ("def f(node):\n"
" if 'end' in node._attributes and (end := getattr(node, 'end', 0)) is not None:\n"
" node.end = end + 1\n"
" return node\n")
assert "E004" not in {d["code"] for d in pd.analyze(src, "t.py")}
def test_match_case_bind_ok(self, pd):
"""match/case 模式绑定变量在其 body 内确定 (dataclasses iterable 场景)"""
src = ("def f(v):\n"
" match v:\n"
" case iterable if not hasattr(iterable, '__next__'):\n"
" return iterable\n"
" case _:\n"
" return None\n")
assert "E004" not in {d["code"] for d in pd.analyze(src, "t.py")}
def test_match_after_use_ok(self, pd):
src = ("def h(v):\n"
" match v:\n"
" case [a, b]:\n"
" return a + b\n"
" case _:\n"
" return 0\n")
assert "E004" not in {d["code"] for d in pd.analyze(src, "t.py")}
# ── code_editor 写文件后自动检测集成 ──
class TestAutoDetectIntegration: