czhao-dev/python-static-analyzer

GitHub: czhao-dev/python-static-analyzer

一个基于 AST 的轻量级 Python 静态分析器,通过九条内置规则在代码运行前发现质量、正确性和可维护性问题,支持作为本地检查工具或 CI 门禁使用。

Stars: 1 | Forks: 0

# Python 静态分析器 一个轻量级的 Python 静态分析器,用于在运行前发现常见的代码质量、正确性和可维护性问题。 它使用 `ast` 模块扫描 `.py` 文件(不会执行你的代码),报告带有稳定规则 ID 的文件及行级诊断信息,并在发现问题时以非零状态退出——因此它既可以作为本地检查工具,也可以作为 CI 门禁。 ## 已实现的检查 | 规则 | 描述 | |---------|--------------| | `SA001` | 可变的默认参数,例如 `def add_item(item, items=[])`。 | | `SA002` | 未使用的 import。 | | `SA003` | 过宽的异常捕获,例如 `except Exception` 或光秃秃的 `except:`。 | | `SA004` | 覆盖了内建名称,例如 `list`、`dict` 或 `id`。 | | `SA005` | 圈复杂度过高的函数。 | | `SA006` | 未使用的局部变量。 | | `SA007` | 深度嵌套的控制流。 | | `SA008` | 在看起来应该返回值的函数中缺少返回路径。 | | `SA009` | 在 `return`、`raise`、`break` 或 `continue` 之后的不可达代码。 | ## 示例 给定这个 Python 文件: ``` def collect(value, values=[]): try: values.append(value) return values except Exception: return [] ``` 分析器将报告: ``` example.py:1: SA001 Mutable default argument `values=[]` example.py:5: SA003 Broad exception handler `except Exception` ``` ## 安装说明 ``` python -m venv .venv source .venv/bin/activate pip install -e ".[dev]" ``` ## 使用说明 ``` static-analyzer scan path/to/project # 或者,不安装 console script: python -m static_analyzer scan path/to/project ``` 示例输出: ``` src/app.py:12: SA001 Mutable default argument `items=[]` src/app.py:34: SA002 Unused import `json` src/service.py:48: SA003 Broad exception handler `except Exception` ``` 当未发现问题时,命令以 `0` 状态退出;当报告诊断信息时以 `1` 状态退出;发生使用错误(例如路径不存在)时则以 `2` 状态退出——这使其适合作为 CI 门禁。 默认情况下,扫描器会跳过常见的非项目目录(`.venv`、`venv`、`.git`、`__pycache__`、`build`、`dist`、`*.egg-info`、`.tox`、caches、`node_modules`)。 ### CLI 选项 ``` static-analyzer scan [paths ...] --max-complexity N Cyclomatic complexity threshold (default: 10) --max-nesting N Control flow nesting depth threshold (default: 4) --select SA001,SA002 Only run these rule IDs (default: all rules) --exclude PATTERN Glob pattern to exclude; repeatable --no-config Ignore pyproject.toml configuration ``` ## 配置 可以在命令行或 `pyproject.toml` 中进行设置: ``` [tool.static-analyzer] exclude = ["tests/fixtures/*"] max_complexity = 10 max_nesting = 4 enabled_rules = ["SA001", "SA002", "SA004"] ``` `enabled_rules` 默认为空列表,这意味着所有规则都会被启用。CLI 参数会覆盖从 `pyproject.toml` 加载的值。 ## 开发说明 项目结构: ``` python-static-analyzer/ ├── README.md ├── pyproject.toml ├── examples/ │ └── sample_issues.py ├── src/ │ └── static_analyzer/ │ ├── __init__.py │ ├── __main__.py │ ├── cli.py │ ├── analyzer.py │ ├── config.py │ ├── diagnostics.py │ └── rules/ │ ├── __init__.py │ ├── mutable_defaults.py │ ├── unused_imports.py │ ├── unused_variables.py │ ├── broad_exceptions.py │ ├── shadowed_builtins.py │ ├── complexity.py │ ├── nesting.py │ ├── missing_return.py │ └── unreachable_code.py └── tests/ ├── test_analyzer.py ├── test_cli.py └── test_*.py (one file per rule) ``` 开发命令: ``` pip install -e ".[dev]" pytest static-analyzer scan examples/ ``` ## 设计 该分析器使用 Python 内置的 `ast` 模块: 1. 将每个 `.py` 文件解析为抽象语法树。 2. 在语法树上运行每个已启用规则的 `check(tree, path, config)` 函数。 3. 收集包含规则 ID、消息、文件路径和行号的诊断信息。 4. 以人类易读的 CLI 格式对结果进行排序和渲染。 5. 当发现问题时以非零状态退出,使该工具可在 CI 中使用。 每条规则都位于 `src/static_analyzer/rules/` 下各自的模块中,并公开一个 `RULE_ID` 和一个 `check()` 函数,因此添加一条新规则只需添加一个文件并在 `rules/__init__.py` 中注册即可。 ## 测试结果 该项目包含 43 个单元测试和端到端测试,涵盖了所有规则以及 CLI: ``` $ pytest -q ........................................... 43 passed in 0.03s ``` 在专门为触发每条规则而编写的文件 [examples/sample_issues.py](examples/sample_issues.py) 上运行分析器,确认了端到端的行为表现: ``` $ static-analyzer scan examples/sample_issues.py examples/sample_issues.py:1: SA002 Unused import `json` examples/sample_issues.py:2: SA002 Unused import `os` examples/sample_issues.py:5: SA001 Mutable default argument `values=[]` examples/sample_issues.py:9: SA003 Broad exception handler `except Exception` examples/sample_issues.py:13: SA004 Parameter `list` shadows a built-in name examples/sample_issues.py:14: SA006 Local variable `unused` is assigned but never used examples/sample_issues.py:18: SA008 Function `classify` may not return a value on all code paths examples/sample_issues.py:23: SA008 Function `first_even` may not return a value on all code paths examples/sample_issues.py:27: SA009 Unreachable code after `return` 9 issue(s) found. ``` 在该分析器自身的源码树上运行它,只发现了两处真实的复杂度问题,分别位于本质上包含大量分支的函数中(一个控制流分发器和一个 import 解析循环)——保持原样,而没有为了满足 linter 而人为地重构它们: ``` $ static-analyzer scan src src/static_analyzer/rules/missing_return.py:38: SA005 Function `_stmt_always_exits` has cyclomatic complexity 16 (threshold 10) src/static_analyzer/rules/unused_imports.py:48: SA005 Function `check` has cyclomatic complexity 12 (threshold 10) 2 issue(s) found. ``` ## Rust 移植版 Rust 移植版位于 [`rust/`](rust/) 中,它是上述 Python CLI 在行为上的直接替代品:相同的 9 条规则、相同的规则 ID 和诊断消息、相同的 CLI 标志、相同的 `pyproject.toml` 配置语义、相同的默认排除项,以及相同的排序、面向行的输出格式和退出码(`0`/`1`/`2`)。 它使用 [`rustpython-ruff_python_ast`/`rustpython-ruff_python_parser`](https://crates.io/crates/rustpython-ruff_python_ast)(ruff 内部 Python AST/解析器 crate 的一个积极维护的镜像),而不是无人维护的 `rustpython-parser`,从而提供带有官方 visitor 模块来遍历的、形状类似 CPython 的 AST。 ### 构建与运行 ``` cd rust cargo build --release ./target/release/static-analyzer scan path/to/project ``` CLI 层面(子命令、标志、退出码)与上面记录的 Python 版本完全相同。 ### 测试结果 59 个测试通过——包括 52 个单元测试(规则逻辑、配置加载、fnmatch、文件发现)以及 7 个集成测试(CLI 行为以及与 Python 实现的逐字节标准输出对比): ``` $ cargo test ... test result: ok. 52 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (tests/analyzer.rs) test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (tests/cli.rs) test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (tests/golden.rs) ``` ### 一致性验证 除了单元/集成测试之外,Rust 二进制文件的输出还在本仓库的每个目录(`src/`、`tests/` 和 `examples/`)的 stdout 和 stderr 上,与 Python CLI(`python -m static_analyzer scan --no-config`)的输出进行了直接 diff 对比。这三者的输出完全逐字节一致: ``` $ diff <(python -m static_analyzer scan src --no-config) <(./rust/target/release/static-analyzer scan src --no-config) (no output — identical) src/static_analyzer/rules/missing_return.py:38: SA005 Function `_stmt_always_exits` has cyclomatic complexity 16 (threshold 10) src/static_analyzer/rules/unused_imports.py:48: SA005 Function `check` has cyclomatic complexity 12 (threshold 10) 2 issue(s) found. ``` 移植过程中暴露了 CPython 的 `ast` 模块与 ruff 的 AST 结构之间的一些实际差异,这些差异需要专门处理(不仅仅是机械翻译): - **Elif 链。** CPython 将 `elif` 表示为 `orelse` 中嵌套的 `ast.If`;ruff 则将 `if`/`elif`/`else` 扁平化为一个单一的 `elif_else_clauses` 列表。这改变了 [`SA005`](rust/src/rules/sa005_complexity.rs) 计算判断点的方式(每个 `elif` 依然会被计算,但不再是一个独立的节点),也改变了 [`SA008`](rust/src/rules/sa008_missing_return.rs) 判断穷尽性的方式(一个没有后续 `else` 的 `if`/`elif` 链是不穷尽的,即使现有的每个分支都返回了——原来的 Python 测试套件没有覆盖这种情况,现在已作为回归测试添加进来)。 - **双重访问的怪癖。** Ruff 生成的语句访问器会访问每个 `elif` 子句的测试表达式两次(一次是显式访问,另一次是在其自身的子句遍历器内部)。[`SA005`](rust/src/rules/sa005_complexity.rs) 手动驱动 `elif` 遍历,以避免重复计算位于 `elif` 条件中的布尔运算符。 - **[`SA007`](rust/src/rules/sa007_nesting.rs) 的嵌套** 实际上变得更简单了:CPython 的 `ast` 无法在结构上区分真正的 `elif` 与 `else:` 后面紧跟位于同一源码列的嵌套 `if`(Python 实现使用列偏移量启发式方法来处理这个问题)。Ruff 的 `elif_else_clauses` 通过 `test: Option`(`elif` 为 `Some`,`else` 为 `None`)直接区分它们,因此 Rust 移植版根本不需要这种启发式方法。 ## 路线图 - [x] 使用 `pyproject.toml` 添加项目打包。 - [x] 实现 CLI 入口点。 - [x] 为 Python 文件实现 AST 解析。 - [x] 添加第一条规则:可变的默认参数。 - [x] 添加诊断格式化。 - [x] 为每条规则添加单元测试。 - [x] 添加配置支持。 - [x] 添加对 CI 友好的退出码。 - [ ] 添加 JSON 输出,以便用于编辑器和自动化集成。 - [x] 将分析器移植到 Rust,作为一个无依赖的单一二进制文件(参见 [Rust 移植版](#rust-port))。 - [ ] 待移植版本经过充分测试后,切换到 Rust 实现作为标准版本,并废弃 Python 源码。 ## 许可证 本项目根据 [LICENSE](LICENSE) 中的条款授权。
标签:Python, SOC Prime, 可视化界面, 开发工具, 无后门, 逆向工具, 通知系统, 错误基检测, 静态代码分析