mukundzha/scrut

GitHub: mukundzha/scrut

Scrut 是一个零依赖的 Python 命令行工具,仅基于 Git diff 审查本次提交中变更的 .py 文件,通过 AST 分析报告函数长度、参数数量、嵌套深度等结构性问题。

Stars: 1 | Forks: 0

# Scrut 审查你即将提交的 Python 文件 —— 仅此而已。 Scrut 是一个零依赖的 CLI 工具,它会查看你的 Git 工作区,分离出 自上次提交以来每个被修改的 `.py` 文件,使用 Python 自带的 `ast` 模块解析每一个文件,并根据你可 配置的限制报告结构性问题。它离线运行,在几毫秒内完成,并很好地回答了一个问题: *我刚写的代码是否仍然符合该项目所认可的规范?* ## 为什么会有 Scrut 代码审查可以发现作者遗漏的问题。但是大多数审查工具要么扫描 整个代码库(缓慢、嘈杂、充满预先存在的技术债务),要么需要 配置、服务器和插件生态系统才能输出有用的信息。 Scrut 从不同的前提出发:风险在于改变了什么。如果 某个函数在这次提交中增长到了 400 行,那就是新的技术债务。如果 它上周就已经是 400 行了,那是一个预先存在的问题 —— 这不是 pre-push 检查应该大喊大叫的东西。 这就是为什么 Scrut 从不扫描。它询问 Git 改变了什么,过滤出 Python 文件,并且只审查这些文件。结果是一个适合在 `git push` 之前的安静时刻使用的工具:足够快,可以每次都运行;足够简单, 可以完全理解;且带有坚定的主张,足以发挥其实用性。 基于 Regex 的 linting 无法可靠地完成这项工作。多行签名、嵌套 块,以及看起来像函数调用的定义,都会让模式匹配束手无策。 `ast` 模块将源代码解析为真正的语法树,这使得“这个 函数有多少个参数”从一场猜谜游戏变成了算术问题。 ## 理念 该项目基于几个刻意的选择: - **小巧。** 整个 pipeline 只是一个模块。你可以在十分钟内读完它。 - **快速。** 无缓存,无 daemon,无网络。每次运行都从头开始, 并在几毫秒内结束。 - **可预测。** 相同的输入总是产生相同的报告。没有 状态,没有持久化,没有任何隐藏的东西。 - **Git 优先。** Git 是审查内容的唯一事实来源。没有基于 flag 的 文件选择,没有列出路径的配置文件。 - **诚实地失败。** 一个不可读或语法损坏的文件会记录 错误并继续执行。一个损坏的文件永远不会让其他文件的报告静默。 - **没有不必要的抽象。** 五个规则,三种报告形态,一个 pipeline。当添加一个规则时,AST 遍历只需几行代码,而不是一个新 框架。 ## 架构 ``` ┌────────────┐ ┌─────────────┐ ┌───────────────┐ ┌──────────┐ │ Git │────▶│ Changed │────▶│ .py filter │────▶│ Loader │ │ work tree │ │ file list │ │ + exists │ │ scrut.toml│ └────────────┘ └─────────────┘ └───────────────┘ └────┬─────┘ │ limits ▼ ┌──────────┐ ┌──────────────┐ ┌───────────────┐ ┌──────────┐ │ Report │◀────│ Rule checks │◀────│ AST walk │◀────│ ast.parse│ │ generator│ │ 5 rules │ │ functions/ │ │ + read │ └──────────┘ └──────────────┘ │ classes │ └──────────┘ └───────────────┘ ``` 这个 pipeline 是一条直线。每个阶段都会缩小或转换其输入: | 阶段 | 组件 | 职责 | |---|---|---| | 发现 | `get_changed_files()` | `git diff HEAD --name-only` —— 自上次提交以来修改的每个文件 | | 过滤 | `get_reviewable_files()` | 仅保留现有的 `.py` 文件 | | 限制 | `load_config()` | 读取 `scrut.toml`,在 `DEFAULT_LIMITS` 上合并 | | 分析 | `analyze_file()` | 读取 → `ast.parse` → 遍历 → 应用这五个规则 | | 指标 | `get_depth()` | 递归嵌套计数器(仅限 `For`、`If`、`While`) | | 报告 | `generate_report()` | 打印汇总报告和摘要 | 失败处理位于文件边界:读取和解析错误会变成 报告中的 `ERROR` 条目,而不是停止运行的异常。 ## 仓库结构 ``` scrut/ ├── pyproject.toml # Packaging, entry point, pytest config ├── scrut.toml # Limits used by this repository itself ├── README.md ├── src/ │ └── scrut/ │ ├── cli.py # The entire pipeline: Git, analysis, report │ └── config/ │ ├── default.py # DEFAULT_LIMITS — single source of fallback truth │ ├── loader.py # scrut.toml reading + merging │ └── validator.py # Reserved for future validation └── tests/ └── test_git.py # 12 unit tests ``` 该布局遵循整洁打包所需的 src-layout 约定。 `cli.py` 是应用程序;`config` 包隔离了关于 限制来源的所有内容,因此 pipeline 永远不需要知道 TOML 或 文件系统路径。`validator.py` 故意留空 —— 这是一个占位符,以备将来 配置值需要进行类型和范围检查时使用。 ## Scrut 的工作原理 当你运行 `scrut` 时,会按顺序发生以下事情: 1. **加载限制。** `load_config()` 在当前 目录中查找 `scrut.toml`。如果存在,其 `[limits]` 表将合并到默认值之上。 如果不存在,则按原样使用 `DEFAULT_LIMITS`。 2. **检查 Git 上下文。** `is_gitrepo()` 运行 `git rev-parse --is-inside-work-tree`。在仓库之外,Scrut 会打印 `Not inside a Git repository.` 并停止。 3. **收集更改的文件。** `get_changed_files()` 运行 `git diff HEAD --name-only`。这涵盖了暂存和未暂存的更改 与上一次提交的对比 —— 正好是构成下一次 push 的文件集合。 4. **过滤。** `get_reviewable_files()` 仅保留以 `.py` 结尾且 仍然存在于磁盘上的文件。如果没有符合条件的文件,Scrut 会打印 `No Python files to review.` 并停止。 5. **分析每个文件。** 对于每个候选文件,`analyze_file()` 会读取 源代码,使用 `ast.parse` 进行解析,并遍历该树。每个 `FunctionDef` 都会测量长度、参数和嵌套深度;每个 `ClassDef` 测量 长度;文件本身测量行数。违规项被收集为 `WARNING` 条目。 6. **报告。** `generate_report()` 打印 FILE、FUNCTIONS 和 CLASSES 部分,随后是汇总的 SUMMARY。 该工具总是以状态 0 退出 —— 它报告发现的问题;它不监督 它们。 ## 安装 要求:你的 `PATH` 中有 **Python 3.10+** 和 **Git 2.0+**。不存在或不会安装其他 依赖项。 **从 PyPI** ``` pip install scrut ``` **从源码** ``` git clone https://github.com/mukundzha/scrut.git cd scrut pip install -e . ``` 这两种方式都会注册 `scrut` 控制台脚本,该脚本指向 `pyproject.toml` 中的 `scrut.cli:main`。 ## 快速开始 ``` cd your-repo # 进行更改 scrut ``` 没有参数也没有 flag。如果你在上次提交后更改了 Python 文件,你 就会得到一份报告。这就是整个接口。 ## 配置 在你运行 `scrut` 的地方旁边创建一个 `scrut.toml`: ``` [limits] max_parameters = 5 max_nesting = 4 max_function_lines = 50 max_class_lines = 200 max_file_lines = 400 ``` 配置是可选的,且在设计上是部分的。`[limits]` 表 合并到 `DEFAULT_LIMITS` 之上(`loader.py` 中的 `merge_limits()`),因此省略一个 key 意味着“使用默认值” —— 一个只包含 `max_parameters = 3` 的文件就是 一个完整、有效的配置。 | Key | 默认值 | 含义 | |---|---|---| | `max_parameters` | 5 | 每个函数的最大位置参数 | | `max_nesting` | 4 | 函数内最大的 `for`/`if`/`while` 嵌套深度 | | `max_function_lines` | 50 | 最大函数长度,包括签名 | | `max_class_lines` | 200 | 最大类长度,包括方法 | | `max_file_lines` | 400 | 最大文件长度 | 一个警告:该文件仅从当前工作目录解析。 从子目录运行 `scrut` 意味着找不到 `scrut.toml`,并且 将应用默认值。 ## 规则引擎 每个规则在超过其阈值时都会发出一个 `WARNING` 条目。消息 总是带有测量值和限制值,因此报告读起来就像是一个 独立的解释:`Function too long (87/50)` 无需上下文。 ### 函数长度 跨越数十行的函数很难阅读、测试,并且 容易累积职责。此规则会标记那些行跨度 —— 包含签名 —— 超过 `max_function_lines` 的函数。 ``` def handle_request(payload): # line 1 ... # 87 lines of logic ``` ``` [WARNING] Function too long (87/50) ``` ### 参数数量 每增加一个参数,调用者必须考虑的组合就会成倍增加。该规则根据 `max_parameters` 计数 `node.args.args` —— 位置和关键字参数 —— 。 ``` def register_user(name, email, password, role, newsletter, locale, timezone): ``` ``` [WARNING] Too many parameters (7/5) ``` ### 嵌套深度 深度嵌套是构建不可读代码最廉价的方式。`get_depth()` 遍历整棵树,并且仅在 `For`、`If` 和 `While` 节点上递增。`Try`、`With` 及其 async 变体被故意排除在外 —— 该指标针对的是条件和循环复杂性,而不是一般的块结构。 ``` def process(items): for item in items: if item.valid: while retry(item): ... ``` ``` [WARNING] Nesting too deep (3/4) ``` ### 类长度 一个超过了 `max_class_lines` 的类通常已经变成了状态和行为的混合袋。 该指标计算完整的类跨度,包括方法。 ``` [WARNING] Class too large (240/200) ``` ### 文件长度 一个 2,000 行的模块不利于导航。此规则将解析的 行数与 `max_file_lines` 进行比较。 ``` [WARNING] File too large (1200/400) ``` ## 示例项目 考虑一个包含三个更改文件的小型库: ``` # utils.py def normalize(text): return text.strip().lower() ``` ``` # api.py class UserAPI: def create(self, name, email, password, role, notify, retries): if not name: raise ValueError("name required") ... ``` ``` # handlers.py def main(): try: api = UserAPI() except Exception: api = UserAPI(retries=5) ``` 运行 `scrut`: - `utils.py` —— 整洁。`normalize` 有一个参数,无嵌套,五行。 - `api.py` —— 被标记。`create` 有六个参数;`UserAPI` 跨越 48 行, 这没问题,但参数数量超过了默认限制五。 - `handlers.py` —— 整洁。 一个警告,一个文件,`utils.py` 或任何未触及的模块没有产生零噪音。 ## 示例输出 ``` $ scrut SCRUT REPORT ================================================== ================================================== FILE -------------------------------------------------- Name : src/scrut/api.py Lines : 320 Issues: [WARNING] File too large (320/400) FUNCTIONS -------------------------------------------------- Function 1: create Lines : 41 Parameters : 6 Nesting Depth : 3 Issues: [WARNING] Too many parameters (6/5) CLASSES -------------------------------------------------- Class: UserAPI Lines: 48 Issues: None ================================================== SUMMARY ================================================== Functions Reviewed : 3 Classes Reviewed : 1 Files Reviewed : 3 Issues Found : 2 ================================================== ``` ## 设计决策 **为什么使用 AST 而不是 regex?** Regex 无法跨行计算括号、 测量嵌套,或区分定义和调用。AST 为 每个有效的 Python 文件提供准确的答案,包括包含关键字注释和 多行签名等边缘情况。 **为什么使用 TOML?** 它是 Python 工具配置的事实标准,在 标准库(`tomllib`)中有一个零依赖的解析器,并且在 diff 中阅读清晰。配置是一个包含五个数字的扁平表 —— 任何更重的东西 都是多此一举。 **为什么用 `git diff HEAD` 而不是 `git diff`?** 单纯的 `git diff` 只涵盖 未暂存的更改。与 `HEAD` 进行比较可以同时捕获暂存和未暂存的 工作,这就是将在下一次 push 中提交的完整文件集。 **为什么只针对更改的文件?** 预先存在的问题是噪音。一个在 10 个文件的提交中报告 500 个警告的工具会淹没那些真正重要的 少数问题:作者刚刚引入的那些问题。 **为什么 CLI 优先?** 终端是进行审查的地方 —— 就在提交和 push 之前。没有 daemon,没有 watch 模式,没有 IDE 插件。一个命令,一份 报告,搞定。 ## 性能 运行时间受限于你触及的内容,而不是你拥有的内容。每个更改的文件 都会被读取、解析一次,并遍历一次。解析在文件大小方面是线性的;AST 遍历和 `get_depth()` 递归在节点数量方面是线性的。这里 没有缓存,因为没有东西需要缓存 —— 总工作量只是少数 几个小文件。 两次 Git 子进程调用(`rev-parse`、`diff`)是每次运行唯一的外部 依赖。在典型的提交中,该工具在远低于 100 毫秒的时间内完成。 ## 路线图 - 在分析之前验证 `scrut.toml` 的值(类型和范围) - 包含未追踪的文件,并支持没有提交的仓库 - 添加 `--json` 输出和可配置的 CI 退出代码 - 计算 `*args`、`**kwargs` 和仅关键字参数 - 从子目录向上搜索 `scrut.toml` - 支持 Pre-commit hook ## 许可证 MIT。请参阅 `LICENSE`。
标签:Git工具, Python, Python安全, SOC Prime, 云安全监控, 代码审查, 开发工具, 文档结构分析, 无后门, 网络安全研究, 自动化payload嵌入, 逆向工具, 静态分析