BayyinahEnterprise/furqan-lint

GitHub: BayyinahEnterprise/furqan-lint

基于 Furqan AST 的多语言结构化完整性检查工具,能在 Python、Rust、Go 代码中捕获遗漏返回、Optional 折叠和 API 破坏性变更等类型系统难以发现的问题。

Stars: 0 | Forks: 0

# furqan-lint [![CI](https://static.pigsec.cn/wp-content/uploads/repos/2026/05/26e91f63ac133833.svg)](https://github.com/BayyinahEnterprise/furqan-lint/actions/workflows/ci.yml) 针对 Python 的结构化完整性检查,由 [Furqan](https://tryfurqan.com) 提供支持。 `furqan-lint` 将 Python 源代码转换为 Furqan AST,并运行 Furqan 检查器中的一个子集,这些检查器的语义能够跨越语言边界转换为符合惯用法的 Python。目前已发布四项检查: - **D24 (all-paths-return)** 类型化函数的每一条控制流路径最终都会到达一个 return 语句。 - **D11 (status-coverage)** 当函数返回 `Optional[X]` 时,每一个调用者要么传播该可选性,要么显式处理 `None`。如果调用者静默地将 `Optional[X]` 折叠为非可选的返回类型,这在结构上等同于丢弃了 Furqan 的 `Integrity | Incomplete` 联合类型中的 `Incomplete` 分支。 - **return_none_mismatch** 如果一个声明为 `-> str`(或任何非 Optional 类型)的函数在某条路径上返回了 `None`,将被标记为类型不匹配。这填补了 D24 中关于 return-None 的盲区。 - **additive_only** 通过 `furqan-lint diff old.py new.py` 调用,它会比较模块公共接口的两个版本,一旦移除任何名称就会触发警告。而添加公共名称则是静默允许的。 ## 安装 ``` pip install furqan-lint ``` 这会从 PyPI 安装最新版本。需要 Python 3.10+ 和 `furqan>=0.11.0`。 ### 可选适配器 ``` pip install "furqan-lint[rust]" # tree-sitter Rust adapter pip install "furqan-lint[go]" # Go adapter (requires Go 1.22+ toolchain at install time) pip install "furqan-lint[rust,go]" # both adapters ``` ### 从特定 commit 或 tag 安装 ``` pip install "git+https://github.com/BayyinahEnterprise/furqan-lint.git@v0.8.4" ``` 将 `v0.8.4` 替换为 [发布历史](https://github.com/BayyinahEnterprise/furqan-lint/releases) 中的任何标签,或使用 `main` 获取最新开发版本。 ### Furqan 依赖 furqan-lint 需要 `furqan>=0.11.0`,即 Furqan 编程语言工具链。截至 2026-05-03,PyPI 上的 `furqan` 发布版本为 v0.10.1;请直接从 GitHub 安装 v0.11.1: ``` pip install "git+https://github.com/BayyinahEnterprise/furqan-programming-language.git@v0.11.1" ``` 一旦 `furqan` v0.11.1 发布到 PyPI,此 GitHub 固定版本的步骤将不再需要。 ### Rust 支持(可选) 从 v0.7.0 起,furqan-lint 能够对 `.rs` 文件进行 lint。Rust 支持位于可选的额外安装包中,因此纯 Python 的安装路径保持不变: ``` pip install "furqan-lint[rust]" ``` 这将引入 `tree-sitter` 和 `tree-sitter-rust`(PyPI 提供了适用于 ARM64 和 x86_64 的预编译 wheel 包;无需从源码构建)。 从 v0.7.2 起,`.rs` 文件会运行三个检查器:R3(ring-close,针对带有注解的函数的零返回,通过上游的 `furqan.checker.check_ring_close` 实现)、D24 (all-paths-return) 和 D11 (status-coverage)。D11 的生产者谓词能识别 `Option` 和 `Result` 返回值;如果调用者调用了一个可能失败的辅助函数但未传播该联合类型,则会被标记。 根据 v0.7.2 的提示词约束自检,计划中与 `return_none_mismatch` 对应的类似功能已被弃用。实证表明,该触发条件在任何可编译的 Rust 源码上都是不可达的(`rustc` 在 furqan-lint 看到文件之前,就会在编译时拒绝 `fn f() -> i32 { None }`)。Trait 对象、生命周期、宏展开、闭包返回类型检查以及 Cargo 工作区遍历目前仍不在作用域内。 Rust edition 是从最近的祖先 `Cargo.toml` 文件中的 `[package].edition` 字段(“2018”、“2021”或“2024”之一)读取的;如果未找到 Cargo.toml 或该字段格式不正确,则 edition 默认为“2021”。当前实现并未针对 edition 进行分支处理。 ### Go 支持(可选) 从 v0.8.1 起,furqan-lint 能够对 `.go` 文件进行 lint(Go diff 功能在 v0.8.1 引入;从 v0.8.2 起,goast 开始输出限定方法名)。Go 支持位于可选的额外安装包中,因此纯 Python 的安装路径保持不变: ``` pip install "furqan-lint[go]" ``` 在安装时需要 Go 工具链(1.21+),以便 PEP 517 构建钩子可以编译内含的 `goast` 二进制文件;但在运行时不需要。 从 v0.8.1 起,`.go` 文件会运行两个检查器:D24 (all-paths-return) 和 D11 (status-coverage,使用 `(T, error)` 触发形式)。跨语言的 `_is_may_fail_producer` 谓词(Shape B)可以识别 `(T, error)` 返回约定;如果调用者调用了一个可能失败的辅助函数但未传播该联合类型,则会被标记。R3 (zero-return) 已被记录为不适用于 Go:Go 编译器会在编译时拒绝所有触发该规则的代码形式。 `.go` 文件支持 Additive-only diff:`furqan-lint diff old.go new.go` 会通过 `goast` 从每个文件中提取首字母大写的公共名称,并报告在 `old` 中存在但在 `new` 中不存在的名称。诊断提示文本是感知语言的:Go 用户将看到 `var Name = ` 的重新导出提示,而不是 Python 的别名语法。跨语言的 diff(例如 `foo.py` 与 `bar.go`)将返回退出代码 2 并提示“Cross-language diff not supported”。 ## 使用方法 ``` furqan-lint check path/to/file.py furqan-lint check path/to/directory/ furqan-lint diff old_version.py new_version.py furqan-lint version ``` ## CI 集成 有两种方式可以将 furqan-lint 集成到您的工作流中。 ### GitHub Action 只需在您的工作流文件中添加三行代码,即可在每次 push 或 pull request 时运行结构化检查: ``` # .github/workflows/furqan-lint.yml name: Furqan Lint on: [push, pull_request] jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: BayyinahEnterprise/furqan-lint@v0.4.0 with: path: src/ ``` 输入参数(均为可选): - `path` -- 要检查的文件或目录。默认值:`.` - `python-version` -- 要使用的 Python 版本。默认值:`3.12` - `furqan-lint-version` -- 指定要安装的版本。默认值:安装 `main` 分支的最新版本。 ### Pre-Commit 钩子 在每次执行 `git commit` 时,对暂存的 Python 文件在本地运行相同的检查: ``` # .pre-commit-config.yaml repos: - repo: https://github.com/BayyinahEnterprise/furqan-lint rev: v0.4.0 hooks: - id: furqan-lint ``` 然后执行 `pre-commit install`。检查失败将阻止提交。 ## 与其他工具配合使用 furqan-lint 是 ruff 和 mypy 的互补工具。它们各自捕获不同类别的问题: | 工具 | 捕获内容 | 与 furqan-lint 的重叠部分 | |------|---------|--------------------------| | **ruff** | 代码风格、未使用的导入、复杂度、常见错误模式、格式化(替代 black + isort + flake8 + pyupgrade) | 无 | | **mypy** | 类型错误、部分缺失的返回值 | 在 D24 和 return-none 上有部分重叠。mypy 不能捕获 Optional 折叠 (D11) 或破坏 API 的更改 (additive-only)。 | | **furqan-lint** | 缺失的返回路径、Optional 折叠、return-None 不匹配、破坏 API 的更改、零返回函数 | 参见 mypy 列 | ### 为您的项目推荐的 `.pre-commit-config.yaml` ``` repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.8.0 hooks: - id: ruff args: [--fix] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.13.0 hooks: - id: mypy - repo: https://github.com/BayyinahEnterprise/furqan-lint rev: v0.5.0 hooks: - id: furqan-lint ``` 然后执行 `pre-commit install`。运行顺序:ruff (lint + format) -> mypy (类型) -> furqan-lint (结构化完整性)。每一层都会捕获前面几层未能发现的问题。 ### 贡献给 furqan-lint ``` git clone https://github.com/BayyinahEnterprise/furqan-lint.git cd furqan-lint pip install -e ".[dev]" pre-commit install # 手动运行所有工具 ruff check . ruff format --check . mypy pytest -q # 按测试类别运行 pytest -m unit # fast, in-process, no subprocess pytest -m integration # CLI and pipeline tests pytest -m "not slow" # skip slow tests pytest -m "not network" # skip network-dependent tests ``` `furqan-lint check src/` 自检将作为 pre-commit 的一部分运行;用于检测他人代码中漂移的工具,其自身绝不能出现漂移。 ## 示例 ``` # example.py from typing import Optional def find_record(id: int) -> Optional[dict]: if id <= 0: return None return {"id": id} def get_name(id: int) -> str: record = find_record(id) if record is not None: return record["name"] # Missing else: falls through with no return ``` ``` $ furqan-lint check example.py MARAD example.py 3 violation(s): [all_paths_return] Function 'get_name' at line 8 declares -> str but not every control-flow path reaches a return statement. [status_coverage] Function 'get_name' at line 8 calls 'find_record' (returns Optional[dict]) but declares -> str. The Optional is silently collapsed. [return_none_mismatch] Function 'get_name' at line 8 declares -> str but returns None on at least one path. ``` ## 在 v0.4.1 中关闭 - **D11 monkey-patch 已弃用。** 生产者谓词的 hack 经历了三个生命周期阶段:v0.1.0 中针对 `status_coverage._is_integrity_incomplete_union` 的临时 monkey-patch,v0.3.0 中的作用域上下文管理器,以及 v0.3.0 中用于保证线程安全的 `threading.Lock`。v0.4.1 通过上游自 `furqan>=0.11.0` 起提供的 `producer_predicate=` 关键字将 Python-Optional 谓词传入,从而彻底弃用了该补丁。关闭了第一轮审计发现的完整生命周期。 - **Pre-commit 钩子的可安装性。** 该钩子现在通过 git URL 将 `furqan` 声明为 `additional_dependency`,因此 `pre-commit install` 能够解析 PyPI 尚未托管的依赖项。 ## 在 v0.3.5 中关闭 两项将已记录的已知限制升级为修复的纠正性更新: - **异常驱动的穿透。** `try`/`except` 体现在被建模为可能运行的代码块(成功路径 = `try.body + orelse` 成为合成 `IfStmt` 的主体;处理程序链入 `else_body`)。D24 现在能够正确标记出那种假阴性情况,即函数唯一的返回路径位于 `try` 块内,但其 `except` 处理程序发生了穿透(经典的 mypy "Missing return statement" 形式)。该问题自 v0.3.1 起作为已知限制被记录。 - **PEP 604 `None | None`。** 现在会转换为裸 `TypePath("None")`,与 `Optional[None]`(v0.3.4)和 `Union[None]`(v0.3.3)生成的形状相同。在所有全为 None 的情况下,这三种 Optional 的拼写路径目前在结构上是完全一致的。在 v0.3.4 中被记录为 v0.4.0 的候选修复。 ## 在 v0.3.2 中关闭 针对 v0.3.1 的第五轮审查中发现的三个问题,均已通过实证复现并修复: - **`Union[X, None]` 识别。** `Union[X, None]`、`Union[None, X]`、`Union[X, Y, None]` 以及 `typing.Union` / `t.Union` 别名形式现在都被视为 Optional。较老的代码库(PEP 604 之前)经常使用 `Union[X, None]`,之前这会产生假阳性的 `return_none_mismatch` 诊断信息。 - **字符串前向引用注解。** 类似 `-> "Optional[User]"` 的 PEP 484 字符串注解(用于打破循环导入的典型 `TYPE_CHECKING` 惯用法)现在会被解析并向下递归。在 v0.3.2 之前,字面字符串会被视为裸类型名称。 - **嵌套类方法。** `Outer.Inner.method`、`Outer.Mid.Inner.method` 等方法现在通过递归下降遍历嵌套的 `ClassDef` 主体来收集。在 v0.3.2 之前,下降仅在一层停止,内部类的方法被静默丢弃,导致在常见的 Python 惯用法中出现 D24 和 `return_none_mismatch` 的假阴性。 ## 在 v0.3.0 中关闭 对 v0.2.0 进行三轮审查后发现的六个问题,均已通过实证复现并修复: - **复合语句盲区。** `for`、`while`、`with`、`try` 和 `match` 体现在会被正确转换,因此它们内部的 `return None` 会被 `return_none_mismatch` 捕获。循环和 `except` 主体被包装为可能运行的 if 语句,因此 D24 不会过度声明覆盖率。 - **Additive 表面缺口。** `MAX_RETRIES: int = 5` 和 `A, B = 1, 2` 现在对 additive 检查器可见。带有注解的 `__all__` 声明也会被读取。 - **动态 `__all__` 级联。** 非静态的 `__all__` 现在会引发 `DynamicAllError`,并且 CLI 会以 `INDETERMINATE` 结果退出并返回代码 2,而不是静默地将表面视为空。 - **D11 线程安全。** 使用 `threading.Lock` 来序列化对 monkey-patched 谓词上下文管理器的并发进入。 - **`Optional` 过度匹配。** `weird.lib.Optional[X]` 不再被视为 `typing.Optional[X]`。 - **`int | str` 渲染。** 非可选联合类型的诊断信息不再显示 `Optional[Unknown]`。 在 v0.2.0 中关闭 - **D24 return-None 盲区。** 声明了非 Optional 返回类型但返回了 `None` 的函数,现在会被 `return_none_mismatch` 检查器捕获。 - **嵌套函数调用归属。** 闭包、内部函数和内部类方法内部的调用不再归属于外围函数。 - **装饰器调用归属。** 装饰器不再被作为被装饰函数主体内部的调用进行收集。 ## 当前限制 此处的每项限制都在 `tests/fixtures/documented_limits/` 中提供了一个固定测试用例,并在 `tests/test_documented_limits.py` 中设有一项测试来锚定当前行为,因此任何方向的更改都将是有意为之,而非悄然发生。 - **`self.method()` 调用。** 适配器将 `self.foo()` 解析为裸方法名 `foo`,与普通的 `foo()` 调用相同。这在目前不算是 bug,但如果适配器将来存储限定的调用路径,则需要重新审视。 - **检查器覆盖率。** Furqan 的十个检查器中只有四个能在 Python 上运行。其余的依赖于 Python 特有的约定(作用域声明、层级注解、校准边界、依赖映射),而标准 Python 并不提供这些。 - **返回类型表达式推断。** `return_none_mismatch` 只捕获 `None` 字面量。一个声明为 `-> int` 却返回了 `"hello"` 的函数不会被捕获。 - **穷尽式 `match` 识别。** 每个 case 主体都被包装为可能运行的 `IfStmt`,因此 D24 无法将结构上穷尽的 `match`(带有 `case _:` 分支)识别为具有保证的覆盖率。未来的工作可能会将 catch-all 分支拼接到前一个 `IfStmt` 的 `else_body` 中。 - **别名的 `Optional` / `Union` 导入。** `from typing import Optional as MyOpt; -> MyOpt[X]` 会被视为非可选的返回类型。同样的缺陷也适用于 `Union`:`from typing import Union as U; -> U[X, None]` 和 `from somelib import Union; -> Union[X, None]` 都绕过了裸名以及 `typing.` / `t.` 匹配器。匹配器仅识别裸 `Optional` / `Union` 名称以及限定的 `typing.` / `t.` 形式;任意别名和来自非 `typing` 模块的同名导入需要符号表跟踪(解析导入,构建别名映射,在匹配前进行解析),这已被推迟到未来的阶段。权宜之计:使用裸名或限定形式,或将导入重命名为 `import typing as t`。 - **任何函数或方法主体内的局部类。** 在函数主体或方法主体内定义的类,其方法将被静默丢弃。v0.3.2 的嵌套类修复添加了对顶层 `ClassDef` -> `ClassDef` 的递归下降(因此 `Outer.Inner.method` 会被收集);但无论该 `FunctionDef` 是在模块作用域内还是在另一个 `ClassDef` 内,它都不会穿透 `FunctionDef` -> `ClassDef`。这种不对称性的理由是:局部类是一种私有实现细节(通常是类似闭包的返回值),不属于 D24 和 `return_none_mismatch` 存在所要维护的模块公共契约。如果真实世界的回归测试证明了相反的情况,应扩展函数遍历器以深入局部 `ClassDef` 主体并调用 `_collect_class_methods`。 ### Rust 适配器(截至 v0.8.3 的最新情况) 每个 Rust 限制在 `tests/fixtures/rust/documented_limits/` 中都有一个固定测试用例,并在 `tests/test_rust_correctness.py` 中有对应的锚定测试。 - **Trait 对象返回类型。** 返回 `Box` 的函数会被转换为一个忽略 trait 对象有效负载的 `TypePath`。Trait 对象多态性超出了当前作用域;未来的检查器将是重新审视此问题的合适场所。已固定在 `tests/fixtures/rust/documented_limits/trait_object_return.rs`。 - **受生命周期影响的返回类型。** 带有显式生命周期参数的函数(`fn f<'a>(...) -> &'a str`)在转换期间会剥离其生命周期;返回类型会被按字面值视为 `-> &'a str`(无生命周期语义)。D24 的路径覆盖逻辑不受影响;未来的借用模式检查器将需要保留生命周期。已固定在 `tests/fixtures/rust/documented_limits/lifetime_param_return.rs`。 - **带有注解返回类型的闭包。** 在 v0.7.1 中,`closure_expression` 节点对于 D24、D11 和 R3 被跳过。外围函数会被正常检查;闭包主体是不透明的。当出现具体的用户报告的假阴性时,未来可能会重新审视此问题。已固定在 `tests/fixtures/rust/documented_limits/closure_with_annotated_return.rs`。 - **`extract_public_names` 遗漏了 impl 块方法。** Rust additive-only diff 路径的名称提取器仅遍历顶层 CST 根子节点;在 v0.8.3 中,有意不收集在 `impl Type { ... }` 块内定义的方法。这与 goast 不对称(从 v0.8.2 起,goast 会输出类似 `Counter.increment` 的限定方法名)。已固定在 `tests/fixtures/rust/documented_limits/impl_methods_omitted.rs`(在 v0.8.3 中添加)。解决路径:已注册为 v0.8.4 候选项。 - **`panic!()`(或任何发散宏)用作无 `;` 的尾部表达式。** 根据 v0.7.0 的 R1 规则,转换器会为任何尾部表达式合成一个 `ReturnStmt(opaque)`,因此 R3 (zero-return) 不会在 `fn f() -> i32 { panic!() }` 上触发。添加修复需要硬编码的发散宏允许列表(脆弱)或跨文件的宏展开类型推断(超出作用域)。如果 Rust 生态系统标准化了 `#[diverging]` 属性,未来可能会重新审视。已固定在 `tests/fixtures/rust/documented_limits/r3_panic_as_tail_expression.rs`。 ### Go 适配器(截至 v0.8.2 的最新情况) 每个 Go 限制在 `tests/fixtures/go/documented_limits/` 中都有一个固定测试用例,并在 `tests/test_go_documented_limits.py`(或者,对于较早的转换器层面的限制,位于 `tests/test_go_translator.py`)中有对应的锚定测试。 - **包含 3 个或更多元素的返回签名。** 转换为不透明的 `TypePath("")`。D24 和 D11 可以看到该函数,但无法推断各个分支的情况。已固定在 `tests/fixtures/go/documented_limits/multi_return_three_or_more.go`。 - **2 元素的非 error 元组返回。** 转换为不透明的 `TypePath("(T, U)")`。D11 的 may-fail 谓词不会对这些触发;根据锁定决策 4,只有 `(T, error)` 形式才会被识别为 may-fail。已固定在 `tests/fixtures/go/documented_limits/two_element_non_error_tuple.go`。 - **`for` 和 `for-range` 主体。** 包装为运行次数介于 0 到 N 次之间(may-runs-0-or-N)的不透明 IfStmt。D24 无法证明一个总是返回值的 `for` 主体能保证覆盖率。已固定在 `tests/fixtures/go/documented_limits/for_statement_opaque.go`。 - **`switch` 主体。** 包装为 may-runs-0-or-N 不透明 IfStmt;case-arm 的返回对 D24 是不可见的。已固定在 `tests/fixtures/go/documented_limits/switch_statement_opaque.go`。 - **`select` 主体。** 包装为 may-runs-0-or-N 不透明 IfStmt。已固定在 `tests/fixtures/go/documented_limits/select_statement_opaque.go`。 - **`defer` 语句。** 包装为不透明;延迟调用对控制流的影响(panic 恢复、资源清理)未被建模。已固定在 `tests/fixtures/go/documented_limits/defer_statement_opaque.go`。 - **接口方法分派。** 通过接口接收者进行的调用未进行特殊建模;接收者类型对适配器是不透明的。已固定在 `tests/fixtures/go/documented_limits/interface_method_dispatch.go`。 - **泛型类型参数。** 在语法上允许出现在签名中,但其约束被忽略。已固定在 `tests/fixtures/go/documented_limits/generic_type_parameters.go`。 - **R3 不适用。** Go 编译器在编译时拒绝所有的 R3 触发形式(带有注解的函数的零返回);唯一可编译的最近边缘情况是带有裸 `return` 的命名返回值,转换器将其视为具有 return 语句。已固定在 `tests/fixtures/go/documented_limits/r3_compile_rejected.go`(在 v0.8.1 中添加)。 ## 许可证 Apache-2.0。
标签:Apache-2.0, API稳定性检查, Go, Linter, LNA, odt, pptx, Python, Ruby工具, Rust, XML注入, 代码审查, 代码检查工具, 代码规范, 代码质量控制, 可视化界面, 多语言支持, 威胁情报, 安全测试框架, 开发者工具, 开源, 抽象语法树, 无后门, 日志审计, 类型安全, 网络流量审计, 软件开发, 逆向工具, 错误基检测, 静态代码分析, 静态检测