BayyinahEnterprise/furqan-lint
GitHub: BayyinahEnterprise/furqan-lint
基于 Furqan AST 的多语言结构化完整性检查工具,能在 Python、Rust、Go 代码中捕获遗漏返回、Optional 折叠和 API 破坏性变更等类型系统难以发现的问题。
Stars: 0 | Forks: 0
# furqan-lint
[](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注入, 代码审查, 代码检查工具, 代码规范, 代码质量控制, 可视化界面, 多语言支持, 威胁情报, 安全测试框架, 开发者工具, 开源, 抽象语法树, 无后门, 日志审计, 类型安全, 网络流量审计, 软件开发, 逆向工具, 错误基检测, 静态代码分析, 静态检测