Skip to content

Oxlint 类型感知预览

本文章宣布类型感知检查的技术预览。 对于最新版本的 Alpha 版本(具有更好的稳定性、可配置性和规则覆盖范围),请参阅 类型感知检查 Alpha 版本公告


我们非常激动地宣布 oxlint 中引入了类型感知检查!

备受期待的 no-floating-promises 及相关规则现已上线。

此预览版本旨在通过记录我们的决策过程和技术细节,与社区展开协作与讨论。

快速开始

如果 oxlint 已经配置好,请安装 oxlint-tsgolint 并使用 --type-aware 标志运行 oxlint

bash
pnpm add -D oxlint-tsgolint@latest
pnpm dlx oxlint --type-aware

如果 oxlint 尚未配置,但你想立即体验 no-floating-promises 规则:

bash
pnpm add -D oxlint-tsgolint@latest
pnpm dlx oxlint@latest --type-aware -A all -D typescript/no-floating-promises

我们期望看到类似以下输出:

js
 × typescript-eslint(no-floating-promises): Promises 必须被等待,以 .catch 调用结尾,以带有拒绝处理程序的 .then 调用结尾,或显式使用 `void` 操作符标记为忽略。
   ╭─[packages/rolldown/src/api/watch/watcher.ts:30:7]
29await this.close();
30originClose();
   ·       ──────────────
31 │     };
   ╰────

更多配置选项,请访问我们的 使用指南

性能

我们的测试表明,此前使用 typescript-eslint 运行需耗时一分钟的仓库,现在可在 10 秒内完成。

这得益于我们利用了 typescript-go,即用 Go 编写的 10 倍更快的 TypeScript

使用来自 oxc-ecosystem-ci 项目中的示例:

项目文件数时间
napi-rs1441.0s
preact2452.7s
rolldown3141.5s
bluesky11527.0s

类型感知检查

请参考 基于 Rust 的 JavaScript 检查器:快速,但目前尚无类型感知检查 了解当前生态系统中类型感知检查的现状。

技术细节

这一新功能的核心是 oxc-project/tsgolint

tsgolint 项目最初作为原型在 typescript-eslint/tsgolint 中实现。 然而,typescript-eslint 团队决定不为此原型投入开发资源,因为他们计划继续在 typescript-eslint 上推进基于 ESLint 的类型感知检查工作。

@boshen@auvred 提出请求,希望获得一个专为 oxlint 定制的精简版分支。 该版本将仅包含类型感知规则,而不包含完整检查器所需的复杂配置解析机制。

@auvred 毅然同意在 Oxc 组织下继续该项目的开发。

架构设计

oxlint(用 Rust 编写)和 tsgolint(用 Go 编写)分别编译成独立的二进制文件。

oxlint 作为 tsgolint 的“前端”,负责处理 CLI、路径遍历、忽略逻辑和诊断信息的打印。

tsgolint 作为 oxlint 的后端,接收路径和配置作为输入,并输出结构化的诊断结果。

这形成了一条简洁的处理流程:

oxlint CLI(返回路径 + 规则 + 配置)
  -> tsgolint(返回诊断信息)
  -> oxlint CLI(打印诊断信息)

tsgolint

tsgolint 并不通过公共 API 与 typescript-go 通信。

相反,它通过 打补丁 的方式,将 typescript-go 的内部接口暴露为公开接口。

所有类型感知规则都直接基于这些打补丁后的接口编写。

虽然这不是访问内部实现的推荐方式,但目前效果良好!

决策过程

自行编写类型检查器

过去尝试实现类型检查器的多个努力均告失败,包括:

此外,正在开发中的 Biome 2.0 也拥有自己的类型推断实现。

我们判断,自行开发类型推断器或类型检查器不可行,因为需要持续跟进像 TypeScript 这样快速演进的目标,难度极高。

与 TypeScript 编译器的通信

typescript-go 之前,项目通过以下方式向 TypeScript 公共 API 添加插件接口:将其语法树映射到 estree,或直接遍历 TypeScript 语法树。例如:

我们也曾探索过 oxlint 的进程间通信,但最终放弃。

随着 typescript-go 的发展,TypeScript 团队正倾向于通过 进程间通信 将 TypeScript 语法树编码并解码到 JavaScript 端。

尽管这些方法有效,但仍存在以下问题:

  • 性能问题程度不一,无法满足 oxlint 的性能要求。
  • 维护从 TypeScript 语法树到其他表示形式的映射成本高昂。

考虑事项

尽管 tsgolint 解决了性能问题,仍存在其他技术挑战亟待解决。

对不同 TypeScript 版本的需求

我们计划发布 typescript-go 的快照版本,并使其版本号与 TypeScript 对齐。 届时,你将能够安装正确版本的 oxlint-typescript

该方法的缺点是,若 oxlint-tsgolint 需要更新,你可能需要升级 TypeScript。

tsgolint 的维护成本

打补丁方式会带来一定风险。不过,实际证明 TypeScript 语法树及其访问者结构非常稳定。 我们接受这种风险,并会在升级 typescript-go 时修复破坏性变更。

我们的 typescript-go 版本每天同步一次。

性能问题

目前 tsgolint 在包含数百个项目的大型多包仓库中表现不佳,或在大量项目引用场景下容易出现死锁,甚至导致内存溢出(OOM)。

我们正在积极解决这些问题,进行性能剖析并向 typescript-go 提交改进,惠及所有 typescript-go 用户。

核心团队成员 @camc314 已提交 多项 PR,显著提升了多个代码路径的性能。

v1.0 版本发布

对于 tsgolint v1.0,我们将解决以下问题:

  • 大型多包仓库的性能问题
  • 支持单个规则的配置
  • 每个规则的正确性
  • IDE 支持
  • 整体稳定性

致谢

我们衷心感谢:

  • TypeScript 团队创造了 typescript-go
  • typescript-eslint 团队给予的热情支持。
  • @auvred 创建了 tsgolint
  • @camchenry 实现了 oxlinttsgolint 的集成。
  • @camc314 在性能问题上的贡献。

加入社区

我们非常期待听到你对 oxlint 以及类型感知检查的反馈,并热切期待看到它如何提升你的开发流程。

与我们连接:

下一步

安装 oxlint

bash
pnpm add -D oxlint@latest oxlint-tsgolint@latest
pnpm dlx oxlint --init # 生成 .oxlintrc.json

或按照 安装指南 进行操作。

使用 --type-aware CLI 标志。

bash
pnpm dlx oxlint --type-aware

并在 .oxlintrc.json 中尝试任意一个类型感知规则:

json
{
  "$schema": "./node_modules/oxlint/configuration_schema.json",
  "rules": {
    "typescript/await-thenable": "error",
    "typescript/no-array-delete": "error",
    "typescript/no-base-to-string": "error",
    "typescript/no-confusing-void-expression": "error",
    "typescript/no-duplicate-type-constituents": "error",
    "typescript/no-floating-promises": "error",
    "typescript/no-for-in-array": "error",
    "typescript/no-implied-eval": "error",
    "typescript/no-meaningless-void-operator": "error",
    "typescript/no-misused-promises": "error",
    "typescript/no-misused-spread": "error",
    "typescript/no-mixed-enums": "error",
    "typescript/no-redundant-type-constituents": "error",
    "typescript/no-unnecessary-boolean-literal-compare": "error",
    "typescript/no-unnecessary-template-expression": "error",
    "typescript/no-unnecessary-type-arguments": "error",
    "typescript/no-unnecessary-type-assertion": "error",
    "typescript/no-unsafe-argument": "error",
    "typescript/no-unsafe-assignment": "error",
    "typescript/no-unsafe-call": "error",
    "typescript/no-unsafe-enum-comparison": "error",
    "typescript/no-unsafe-member-access": "error",
    "typescript/no-unsafe-return": "error",
    "typescript/no-unsafe-type-assertion": "error",
    "typescript/no-unsafe-unary-minus": "error",
    "typescript/non-nullable-type-assertion-style": "error",
    "typescript/only-throw-error": "error",
    "typescript/prefer-promise-reject-errors": "error",
    "typescript/prefer-reduce-type-parameter": "error",
    "typescript/prefer-return-this-type": "error",
    "typescript/promise-function-async": "error",
    "typescript/related-getter-setter-pairs": "error",
    "typescript/require-array-sort-compare": "error",
    "typescript/require-await": "error",
    "typescript/restrict-plus-operands": "error",
    "typescript/restrict-template-expressions": "error",
    "typescript/return-await": "error",
    "typescript/switch-exhaustiveness-check": "error",
    "typescript/unbound-method": "error",
    "typescript/use-unknown-in-catch-callback-variable": "error"
  }
}