feat: py_detect L2 语义验证后端 (可选 jedi, 自动降级)

- _jedi_validate(): 检测到 jedi 时用语义推断二次确认 E002 未定义变量
  - 消除 from x import * / 跨文件符号 / 类型别名 的规则引擎误报
  - 真未定义保留; 无 jedi 时原样返回 (纯标准库模式行为完全不变)
- 关键坑: jedi 的 infer line/column 是 1-based (与 ast 一致, 与 LSP 0-based 不同), 之前按 0-based 传导致位置全错
- 版本 1.1.0 → 1.2.0; 新增 4 项测试 (import* 误报消除/真未定义保留/无 jedi 降级/跨文件无错)
- 相关套件 263 passed 零回归
This commit is contained in:
shuiyu67 committed 2026-08-07 02:25:36 +08:00
1 parent 917ba25902
commit 5008d431c9
2 files changed
+76 -3

No files matched your search

+33
View File
@@ -278,6 +278,39 @@ class TestSmartExplain:
assert "isinstance(x, int)" in fix, fix
# ── L2 jedi 语义验证后端 (可选, 无 jedi 自动降级) ──
class TestJediBackend:
def test_import_star_false_positive_removed(self, pd):
pytest.importorskip("jedi")
# 规则引擎: import * 不收集 sin → 误报 E002; jedi: 能解析 math.sin → 消除
src = "from math import *\ndef f():\n return sin(3.14)\n"
codes = {d["code"] for d in pd.analyze(src, "C:/tmp/x.py")}
assert "E002" not in codes
assert "W010" in codes # import * 警告保留
def test_real_undefined_kept_with_jedi(self, pd):
pytest.importorskip("jedi")
src = "def f():\n return totally_missing_xyz\n"
assert "E002" in {d["code"] for d in pd.analyze(src, "C:/tmp/x.py")}
def test_fallback_without_jedi(self, pd, monkeypatch):
import sys
monkeypatch.setitem(sys.modules, "jedi", None) # import jedi 将失败
src = "from math import *\ndef f():\n return sin(3.14)\n"
codes = {d["code"] for d in pd.analyze(src, "C:/tmp/x.py")}
assert "E002" in codes # 无 jedi → 保留规则引擎判定 (行为不变)
assert "W010" in codes
def test_cross_file_symbol_no_error(self, pd, tmp_path):
pytest.importorskip("jedi")
helper = tmp_path / "helper.py"
helper.write_text("def helper_func():\n return 1\n", encoding="utf-8")
main = tmp_path / "main.py"
src = "from helper import helper_func\ndef f():\n return helper_func()\n"
issues = pd.analyze(src, str(main))
assert "E002" not in {d["code"] for d in issues}
# ── code_editor 写文件后自动检测集成 ──
class TestAutoDetectIntegration:
"""AI 用 code_editor 改完 .py 文件后,系统自动跑 py_detect"""