fix: 顶级库压测 (pydantic/fastapi/click) 修复 posonlyargs/常量迭代源/for-else 终止
- posonlyargs (/ 位置专用参数) 漏收集 → pydantic 45 个 E002 归零 (config.py class_、networks.py input_value)
- 常量非空迭代源 for x in ('a','b','c')/range(n>0): 循环体必执行 → 变量确定 (pydantic v1 host)
- for-else else 终止 → 仅 break 路径流出 → 循环变量确定 (click/utils.py i)
- 效果: pydantic E002 45→0 E004 18→7 (剩闭包盲区); Web 栈 E004 27→22; 新增 3 回归测试; 302 passed
This commit is contained in:
1 parent
082efd62cc
commit
e16234aba9
2 files changed
+70
-6
No files matched your search
+48
-6
@@ -320,7 +320,7 @@ class _ScopeAnalyzer(ast.NodeVisitor):
|
|||||||
self._add("W007", node.lineno, node.col_offset,
|
self._add("W007", node.lineno, node.col_offset,
|
||||||
extra=f"方法 {node.name} 缺 self/cls", func=node.name)
|
extra=f"方法 {node.name} 缺 self/cls", func=node.name)
|
||||||
scope: Dict[str, str] = {}
|
scope: Dict[str, str] = {}
|
||||||
for arg in node.args.args + node.args.kwonlyargs:
|
for arg in node.args.posonlyargs + node.args.args + node.args.kwonlyargs:
|
||||||
scope[arg.arg] = "参数"
|
scope[arg.arg] = "参数"
|
||||||
if node.args.vararg:
|
if node.args.vararg:
|
||||||
scope[node.args.vararg.arg] = "参数"
|
scope[node.args.vararg.arg] = "参数"
|
||||||
@@ -626,6 +626,40 @@ def _match_bound_names(pattern) -> set:
|
|||||||
return names
|
return names
|
||||||
|
|
||||||
|
|
||||||
|
def _target_name_list(target) -> list:
|
||||||
|
"""提取赋值目标的全部名字 (含解包/星号)"""
|
||||||
|
names = []
|
||||||
|
if isinstance(target, ast.Name):
|
||||||
|
names.append(target.id)
|
||||||
|
elif isinstance(target, ast.Starred):
|
||||||
|
names.extend(_target_name_list(target.value))
|
||||||
|
elif isinstance(target, (ast.Tuple, ast.List)):
|
||||||
|
for elt in target.elts:
|
||||||
|
names.extend(_target_name_list(elt))
|
||||||
|
return names
|
||||||
|
|
||||||
|
|
||||||
|
def _flow_iter_always_runs(iter_node) -> bool:
|
||||||
|
"""迭代源是否为非空常量 → 循环体必执行至少一次"""
|
||||||
|
if isinstance(iter_node, (ast.Tuple, ast.List, ast.Set)):
|
||||||
|
return len(iter_node.elts) > 0
|
||||||
|
if isinstance(iter_node, ast.Constant):
|
||||||
|
v = iter_node.value
|
||||||
|
return isinstance(v, (str, bytes, list, tuple, dict, set)) and len(v) > 0
|
||||||
|
if isinstance(iter_node, ast.Call) and isinstance(iter_node.func, ast.Name) \
|
||||||
|
and iter_node.func.id == "range" and len(iter_node.args) >= 1:
|
||||||
|
try:
|
||||||
|
stop = iter_node.args[0].value
|
||||||
|
if len(iter_node.args) == 1:
|
||||||
|
return isinstance(stop, int) and stop > 0
|
||||||
|
if len(iter_node.args) >= 2 and isinstance(stop, int):
|
||||||
|
start = iter_node.args[1].value
|
||||||
|
return isinstance(start, int) and stop > start
|
||||||
|
except AttributeError:
|
||||||
|
return False
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _flow_terminates(stmts) -> bool:
|
def _flow_terminates(stmts) -> bool:
|
||||||
"""语句列表是否必然终止/不流出 (return/raise/continue/break, 或 if 两分支都终止)"""
|
"""语句列表是否必然终止/不流出 (return/raise/continue/break, 或 if 两分支都终止)"""
|
||||||
if not stmts:
|
if not stmts:
|
||||||
@@ -665,9 +699,17 @@ 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)
|
||||||
# for-else: orelse 在循环无 break 时必执行(含 0 次) → 其赋值确定;
|
if _flow_iter_always_runs(stmt.iter):
|
||||||
# 循环体赋值可能 0 次 → 不贡献
|
# 常量非空迭代源 → 循环体必执行至少一次 → 循环体赋值确定
|
||||||
defined.clear(); defined.update(d_orelse)
|
defined.clear(); defined.update(d_inner)
|
||||||
|
else:
|
||||||
|
if stmt.orelse and _flow_terminates(stmt.orelse):
|
||||||
|
# for-else: else 终止(return/raise) → 仅 break 路径流出 → 循环变量确定
|
||||||
|
for n in _target_name_list(stmt.target):
|
||||||
|
d_orelse.add(n)
|
||||||
|
# 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)
|
||||||
@@ -758,7 +800,7 @@ def _dataflow_check(tree, analyzer):
|
|||||||
for sub in ast.walk(fn):
|
for sub in ast.walk(fn):
|
||||||
if isinstance(sub, ast.Name) and isinstance(sub.ctx, (ast.Store, ast.Del)):
|
if isinstance(sub, ast.Name) and isinstance(sub.ctx, (ast.Store, ast.Del)):
|
||||||
locals_set.add(sub.id)
|
locals_set.add(sub.id)
|
||||||
for a in fn.args.args + fn.args.kwonlyargs:
|
for a in fn.args.posonlyargs + fn.args.args + fn.args.kwonlyargs:
|
||||||
locals_set.add(a.arg)
|
locals_set.add(a.arg)
|
||||||
if fn.args.vararg:
|
if fn.args.vararg:
|
||||||
locals_set.add(fn.args.vararg.arg)
|
locals_set.add(fn.args.vararg.arg)
|
||||||
@@ -769,7 +811,7 @@ def _dataflow_check(tree, analyzer):
|
|||||||
for n in sub.names:
|
for n in sub.names:
|
||||||
locals_set.discard(n)
|
locals_set.discard(n)
|
||||||
|
|
||||||
defined = {a.arg for a in fn.args.args + fn.args.kwonlyargs}
|
defined = {a.arg for a in fn.args.posonlyargs + fn.args.args + fn.args.kwonlyargs}
|
||||||
if fn.args.vararg:
|
if fn.args.vararg:
|
||||||
defined.add(fn.args.vararg.arg)
|
defined.add(fn.args.vararg.arg)
|
||||||
if fn.args.kwarg:
|
if fn.args.kwarg:
|
||||||
|
|||||||
@@ -572,6 +572,28 @@ class TestDataFlow:
|
|||||||
assert "E002" not in {d["code"] for d in pd.analyze(src, "t.py")}
|
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")}
|
assert "E004" not in {d["code"] for d in pd.analyze(src, "t.py")}
|
||||||
|
|
||||||
|
# ── 顶级库压测回归 (posonlyargs / 常量迭代源) ──
|
||||||
|
def test_posonly_args_collected(self, pd):
|
||||||
|
"""def f(a, /, b): a 是位置专用参数, 不得误报 (pydantic Self | str 场景)"""
|
||||||
|
src = ("def inner(class_, /):\n"
|
||||||
|
" return class_.__name__\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_const_iter_always_runs(self, pd):
|
||||||
|
"""for f in ('a','b','c'): 常量非空 → 循环变量确定 (pydantic v1 host 场景)"""
|
||||||
|
src = ("def f(parts):\n"
|
||||||
|
" for k in ('a', 'b', 'c'):\n"
|
||||||
|
" host = parts[k]\n"
|
||||||
|
" if host:\n"
|
||||||
|
" break\n"
|
||||||
|
" return host\n")
|
||||||
|
assert "E004" not in {d["code"] for d in pd.analyze(src, "t.py")}
|
||||||
|
|
||||||
|
def test_range_const_iter(self, pd):
|
||||||
|
src = "def f():\n for i in range(3):\n x = i\n return x\n"
|
||||||
|
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