尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Zed 仓库自定义 dylint lint 开发指南:为 tooling/lints 新增、注册与测试私有 lint

Zed 仓库自定义 dylint lint 开发指南:为 tooling/lints 新增、注册与测试私有 lint Zed 仓库自定义 dylint lint 开发指南为 tooling/lints 新增、注册与测试私有 lint【免费下载链接】zedCode at the speed of thought – Zed is a high-performance, multiplayer code editor from the creators of Atom and Tree-sitter.项目地址: https://gitcode.com/GitHub_Trending/ze/zed本文聚焦 Zed 代码仓库中位于tooling/lints的私有 dylint 库完整讲解从零新增一个自定义 lint的工程流程模块布局与注册、基于notify_in_render.rs的模板骨架、诊断纪律、正/负向 UI 测试约定、gpui 测试桩、.stderr更新方法以及用single-lint对真实代码库做冒烟验证。读完本文你可以照着 Zed 内部规则为 tooling/lints 写出一个符合仓库规范的、可直接cargo test验收的自定义 lint并理解其底层的 rustc late-pass 机制。本文面向tooling/lints的维护者与贡献者基线资料为技能文档 .agents/skills/lint-creator/SKILL.md 及仓库源码。一、背景为什么 Zed 需要一套私有的 dylint 库Zed 的代码量庞大且大量代码运行在 GUI 主线程上渲染render、gpui 的Context/Entity状态更新、前台阻塞 IO 等这些模式与通用规则存在差异clippy 的默认规则集难以精确表达在 render 期间调用notify()这类仓库专属反模式。为此 Zed 把自研检查集中放在tooling/lints—— 按 README.md 的定义它是一个dylint 库A dylint library that flags various bad patterns in our codebase用于识别 Zed 代码库中的各类不良模式。dylintDynamic Lint Library允许把 lint 编译成动态库由cargo-dylint子命令在cargo check时动态加载并注入 rustc 驱动。与该机制配套的工程约束都直接写在了 rust-toolchain.toml 与技能文档中该 crate刻意不加入 zed workspace而是一个独立 crate避免与主仓库的编译配置耦合它固定使用自己的 nightly toolchain用于访问rustc_private等私有 API。当前锁定版本为nightly-2026-03-21并声明组件llvm-tools-preview、rustc-dev、rust-src这正是编译 dylint 库所需的最小组件集合。安装与首次准备来自 README.mdcargo install cargo-dylint dylint-link cd tooling/lints rustup toolchain install # 读取 rust-toolchain.toml自动安装并配备对应组件cargo-dylint负责构建并运行 lint 库dylint-link是构建 lint 库时使用的链接器。仓库根目录的Cargo.toml通过[workspace.metadata.dylint]注册了该库因此运行 dylint 时无需显式传--pathDylint 会自动发现它首次运行需要以固定的 nightly 编译该库速度较慢后续会命中缓存。整个仓库当前登记的 lint即 README 中 Current lints 列表共七个lint 标识snake_case检查内容shared_string_from_str_literal用复制/分配路径从字符串字面量构造SharedString应改用new_staticasync_block_without_awaitasync { … }块体内不含.awaitentity_update_in_render在Render::render内用.update()修改实体map_lookup_then_insert查表后再对同一 key 分支插入应使用entryAPIHashSet/BTreeSet同理notify_in_render在Render::render内同步调用Context::notify()owned_string_into_shared先构造String再.into()为SharedString等类型blocking_io_on_foreground在主线程非闭包/后台线程调用阻塞 IO运行方式README 也推荐优先通过 Zed 的clippy脚本统一执行对整个仓库或单个 crate 跑全部 lintcargo dylint --all -- --workspace cargo dylint --all -- -p project_panel新增 lint 前的前置检查在动笔之前lint-creator SKILL.md 要求先确认一件事检查 clippy 是否已经覆盖该模式官方文档 https://rust-lang.github.io/rust-clippy/master/ 。如果 clippy 已提供等价检查应停止并向用户说明而不是重复造一个冗余 lint。这一步避免了与上游规则库的重复劳动也保证本库只存放 clippy 管不到或管不准的 Zed 专属规则。二、布局与注册一个 lint 对应一个模块新增 lint 的文件布局遵循严格的一 lint 一模块约定新建模块文件src/lint_name.rs在 src/lib.rs 中声明该模块mod lint_name;在register_lints函数中完成两项注册把 lint 加入lint_store.register_lints([…])的切片同时追加一个register_late_pass调用把 lint pass 实例交给编译器。实际注册代码位于 tooling/lints/src/lib.rs#L52-L70#[allow(clippy::no_mangle_with_rust_abi)] #[unsafe(no_mangle)] pub fn register_lints(sess: rustc_session::Session, lint_store: mut rustc_lint::LintStore) { dylint_linting::init_config(sess); lint_store.register_lints([ SHARED_STRING_FROM_STR_LITERAL, ASYNC_BLOCK_WITHOUT_AWAIT, BLOCKING_IO_ON_FOREGROUND, ENTITY_UPDATE_IN_RENDER, MAP_LOOKUP_THEN_INSERT, NOTIFY_IN_RENDER, OWNED_STRING_INTO_SHARED, ]); lint_store.register_late_pass(|_| Box::new(SharedStringFromStrLiteral)); lint_store.register_late_pass(|_| Box::new(AsyncBlockWithoutAwait)); lint_store.register_late_pass(|_| Box::new(blocking_io_on_foreground::BlockingIoOnForeground)); lint_store.register_late_pass(|_| Box::new(entity_update_in_render::EntityUpdateInRender)); lint_store.register_late_pass(|_| Box::new(map_lookup_then_insert::MapLookupThenInsert)); lint_store.register_late_pass(|_| Box::new(notify_in_render::NotifyInRender)); lint_store.register_late_pass(|_| Box::new(owned_string_into_shared::OwnedStringIntoShared)); }文件顶部还有两个约定值得注意模块文件与 lint 常量通过use引入见 lib.rs#L29-L40库必须导出 dylint ABI 版本符号dylint_linting::dylint_library!();lib.rs#L45这是 dylint 库能被cargo-dylint识别加载的胶水。需要了解一个历史例外当前直接写在lib.rs里的两个 lint ——SHARED_STRING_FROM_STR_LITERAL与ASYNC_BLOCK_WITHOUT_AWAIT—— 是先于一 lint 一模块规则存在的老 lint仍留在lib.rs中。技能文档明确提示不要照抄shared_string_from_str_literal的诊断风格详见第五节也不要因此推翻模块化约定新 lint 一律走独立模块。三、lint 模块模板以notify_in_render.rs为骨架技能文档指定 src/notify_in_render.rs 作为新 lint 的参考模板。一个规范模块由四部分组成1.rustc_session::declare_lint!宏用宏声明 lint 常量并要求在文档注释里写清### What it does与### Why is this bad?两节说明做什么与为什么不好。以notify_in_render为例notify_in_render.rs#L7-L22rustc_session::declare_lint! { /// ### What it does /// /// Flags calls to Context::notify() that execute synchronously inside a /// Render::render method. /// /// ### Why is this bad? /// /// notify() tells the framework that the entitys state has changed and /// it should be re-rendered. Calling it during render means every render /// pass schedules another render pass — either an infinite loop or wasted /// work. pub NOTIFY_IN_RENDER, Warn, calling cx.notify() during render schedules a redundant re-render }2. 定义 pass 类型并生成 pass 实现pub(crate) struct NotifyInRender; rustc_session::impl_lint_pass!(NotifyInRender [NOTIFY_IN_RENDER]);3. 实现LateLintPass尽早短路核心检查逻辑放在check_expr中采用每个条件不满足就提前 return的短路风格配合let … else解构而非层层嵌套。完整骨架如下对齐真实实现 notify_in_render.rs#L28-L57impltcx LateLintPasstcx for NotifyInRender { fn check_expr(mut self, cx: LateContexttcx, expr: tcx Exprtcx) { // ① 跳过宏展开产生的代码用户无法编辑该 span if expr.span.from_expansion() { return; } // ② 只关心方法调用 let ExprKind::MethodCall(segment, receiver, _args, _span) expr.kind else { return; }; // ③ 方法名必须是 notify if segment.ident.name.as_str() ! notify { return; } // ④ 接收者类型必须是 gpui 的 Context let receiver_ty cx.typeck_results().expr_ty(receiver); if !is_gpui_context(cx, receiver_ty) { return; } // ⑤ 必须直接位于 render 方法内中间无闭包 if !is_directly_in_render_method(cx, expr.hir_id) { return; } // ⑥ 触发单句消息 span_lint( cx, NOTIFY_IN_RENDER, expr.span, cx.notify() called during render schedules a re-render every render pass, ); } }这段代码演示了全部关键技巧用from_expansion()拦截宏代码用let … else把非目标表达式挡在门外借助类型检查结果cx.typeck_results().expr_ty(receiver)判断接收者是否为 gpui 类型用公共助手判定调用点是否真正处于 render 方法内。判定的顺序也从廉价方法名比较到昂贵HIR 祖先遍历先粗筛后精筛。4. 边界语义只有直接位于 render 中才报is_directly_in_render_method特意要求hir_id与render方法之间不能隔着闭包详见 render_helpers.rs#L10-L23。这是因为闭包通常延迟执行例如注册到事件处理器后执行此时调用notify()是合法的只有 render 当帧同步执行的调用才是问题。参考entity_update_in_render的做法entity_update_in_render.rs一个更复杂的 lint 会在短路链上叠加更多约束例如要求.update()闭包返回()表示变更而非读取、接收者为gpui::Entity/WeakEntity等 —— 这些判定均复用第六节的公共助手。四、诊断纪律只提示、不修复技能文档用专门一节规定了 lint 触发时的诊断输出风格所有新 lint 都必须遵守只标记flag only报告问题即可绝不附带修复建议绝不使用Applicability::MachineApplicable。原因在于 Zed 代码量大、自动修改风险高且 render/主线程相关反模式的修复往往需要人工判断上下文保持检测与报告尽可能简单优先使用span_lint加一句话消息而不是span_lint_and_then附带多条 notes 的复杂诊断notify_in_render、entity_update_in_render均遵循这一风格直接use clippy_utils::diagnostics::span_lint跳过宏展开代码入口处即if expr.span.from_expansion() { return; }。当前lib.rs里的shared_string_from_str_literallib.rs#L278-L325正是反例它通过span_lint_and_then输出机器可应用的建议Applicability::MachineApplicable把SharedString::from(…)改写为SharedString::new_static(…)。技能文档明确说明该 lint早于这些规则存在不要模仿其诊断风格同理直接内联在lib.rs的两个历史 lint 早于模块化规则不作为新 lint 参照。五、公共助手render_helpers.rs中的渲染/GPUI 判定针对 Zed 特有反模式绝大多数 lint 都要回答这段代码是否在 gpui 的 render 上下文里接收者是不是 gpui 类型之类的问题。这些判定被集中抽到 src/render_helpers.rs 中新 lint 应当复用而不是重新实现is_directly_in_render_method(cx, hir_id)判断hir_id是否直接位于某个impl gpui::Render或RenderOnce的fn render内、且之间无闭包。实现上沿cx.tcx.hir_parent_iter向上遍历父节点遇到ExprKind::Closure立即返回false遇到名为render的ImplItem再校验其所属impl块的 trait 是否为gpui::Render/RenderOnceis_gpui_render_trait/is_gpui_context/is_gpui_entity_or_weak通过ty_adt_def()拿到 ADT 定义后用cx.tcx.crate_name(def_id.krate) gpui校验 crate再比对类型名从而可靠区分 gpui 类型与同名第三方类型is_unit_or_result_unit判断表达式类型是()来自Entity::update还是Result(), _来自WeakEntity::update用于识别仅作变更、不返回值的 update 调用。这些助手通过DefId而非字符串匹配类型路径crate_nameitem_name能正确处理类型经pub use重导出后DefId不变的事实与lib.rs中is_shared_string判定SharedString的思路一致见 lib.rs#L266-L276保证稳定性与正确性。六、UI 测试正/负用例是硬性要求技能文档规定每个 lint 都必须配套 UI 测试文件成对出现ui/lint_name.rs—— 用例源码ui/lint_name.stderr—— rustc 编译该源码时的预期诊断输出快照。查看仓库 tooling/lints/ui 目录可以看到现有成对文件async_block_without_await.{rs,stderr}、blocking_io_on_foreground.{rs,stderr}、entity_update_in_render.{rs,stderr}、map_lookup_then_insert.{rs,stderr}、owned_string_into_shared.{rs,stderr}。负向用例缺失 断言语义.rs文件必须包含负向用例negative cases即看起来像坏模式、但实际不该触发诊断的代码。负向用例不需要出现在.stderr中——它们在.stderr中的缺席本身就是断言如果负向用例被误报生成的诊断会出现在 stderr 里测试即失败。这是 UI 快照测试天然的误报防线。以 ui/blocking_io_on_foreground.rs 为范例文件前半部分放应当触发的一批用例带 gpuimut App/Context参数的std::fs::read_to_string、std::fs::File::open、std::thread::sleep、std::net::TcpStream::connect、Mutex::lock、Render::render中的阻塞 IO 等后半部分放不应当触发的一批用例无 gpui 参数的普通函数load_config_plain、把阻塞 IO 包进闭包、独立线程休眠standalone_sleep等。分节标题格式技能文档要求所有 lint 专用ui/*.rs用例文件必须把期望触发的用例放在前面不期望触发的放在后面并使用如下精确标题注释// SHOULD FIRE // SHOULD NOT FIRE 仓库中的 ui/map_lookup_then_insert.rs 已采用SHOULD FIRE/SHOULD NOT FIRE分节风格相对早期的blocking_io_on_foreground.rs、entity_update_in_render.rs则使用SHOULD WARN/SHOULD NOT WARN表述语义一致触发/不触发。新增用例请按技能文档规范统一采用SHOULD FIRE/SHOULD NOT FIRE。七、GPUI 类型依赖用test_fixture/假 crate而不是真 gpui许多 Zed 专属 lint如 render/context 相关的用例需要 gpui 类型才能编译。技能文档给出硬性规则UI 测试使用test_fixture/中的假gpuicrate绝不把真实的 gpui 作为测试依赖。理由很现实真实的 gpui 依赖庞大GPU、窗口系统等会让每个 UI 测试都背上沉重的编译负担且与独立 crate 的轻量测试定位冲突。test_fixture/是一个独立的迷你 workspace内含若干 stub crate详见 tooling/lints/test_fixture 目录gpui/—— 提供App、Window、Context、Entity、Render、RenderOnce等骨架类型与方法供 UI 用例extern crate gpui; use gpui::*;gpui_shared_string/—— 提供字符串相关测试类型render_consumer/、consumer/—— 消费测试桩。搭建机制在 lib.rs#L528-L563 的测试模块中gpui_fixture_rustc_flags()用cargo build --package gpui先构建 fixture 目录里的假 gpui再拼出 rustc flagsfn gpui_fixture_rustc_flags() - VecString { // 在 test_fixture 下 cargo build --package gpui // 找到 libgpui.rlib 与 deps 目录返回 // --edition2021 // --externgpuipath-to-libgpui.rlib // -Ldependencypath-to-deps } #[test] fn ui() { let flags gpui_fixture_rustc_flags(); dylint_testing::ui::Test::src_base(env!(CARGO_PKG_NAME), ui) .rustc_flags(flags) .run(); }UI 用例经dylint_testing::ui::Test驱动其职责是用给定 rustc flags 编译ui/目录下每个.rs将实际 stderr 与对应.stderr比对。若测试中需要某个 gpui 类型或方法但 fixture 里缺失应当扩展 fixture往test_fixture/gpui的 lib.rs 中补上 stub而不是把真实 gpui 加进依赖。八、运行测试与更新.stderr由于该 crate 不在 zed workspace 中必须在 crate 内部运行测试以使其命中自身固定的 nightly 与 rustc-dev 组件cd tooling/lints cargo test测试失败时dylint_testing会报告实际输出与期望快照的差异并在失败信息中给出Actual stderr saved to PATH一行指向保存了真实输出的临时文件路径。修改.stderr的唯一正规流程是运行测试在失败报告中找到Actual stderr saved to PATH一行把该真实输出文件复制覆盖到仓库中对应的ui/lint_name.stderr。技能文档特别强调这个仓库没有 bless 环境变量不像部分工具链提供BLESS1一键刷新因此只能手动复制覆盖避免误刷新掩盖真实行为变化。另一个容易踩坑的细节一个正确的.stderr文件通常以空行结尾复制时不要顺手删掉结尾空行否则快照比对仍会失败。并且只有在有意的修改lint 逻辑或用例变更导致输出确实变化之后才允许更新快照若是回归性误报/漏报应当修 lint 而不是改.stderr。九、真实验收single-lint冒烟验证单元与 UI 测试通过只代表用例满足不代表 lint 在真实代码上不误报。因此技能文档要求 lint 可工作后对真实代码库做冒烟测试tooling/lints/single-lint lint_name -p crate第一个参数是 lint 名README Current lints 列表中的 snake_case 标识之后所有参数透传给cargo check若不指定包默认对--workspace全仓运行single-lint脚本封装了两个在 README 中明确记载的坑详见 single-lint 脚本注释与 README 说明。single-lint的核心机制dylint 总是把整库所有 lint 一起加载因此脚本用环境变量把所有 lint 静音、只强制打开目标 lintDYLINT_RUSTFLAGS-A warnings --force-warn lint cargo dylint --all -- args两个坑的具体原理必须使用--force-warn-A warnings已把整组警告允许allow掉之后对 driver 注册的 lint 单纯加-W lint无法可靠地重新启用--force-warn才是对该场景有效的强制手段必须清理目标包缓存DYLINT_RUSTFLAGS不属于 Cargo 的 fingerprint指纹组成部分改变它不会让 Cargo 失效已检查过的 crate——若不清理Cargo 会重放陈旧缓存导致过滤看起来没生效。脚本因此先对指定的-p pkg做cargo clean -p若为--workspace全仓运行则直接丢弃整个 dylint 检查缓存目录target/dylint/target/toolchain-hosttoolchain 取自rust-toolchain.toml的 channel。也可以配合更细粒度验证对整个仓库执行cargo dylint --all -- --workspace或对单个 crate 执行cargo dylint --all -- -p crate此时所有 lint 都会启用。冒烟时请重点确认目标 crate 中新 lint 的命中点是否合理、是否出现不应有的误报可对照该 lint 的负向用例心智模型。十、收尾清单从功能到入库当 lint 逻辑与用例全部就绪后按以下顺序收尾对齐技能文档 After the lint works 一节登记到文档把新 lint 加入 tooling/lints/README.md 的 Current lints 列表附带一句话说明检查对象与理由全仓冒烟运行tooling/lints/single-lint lint_name默认--workspace确认在真实代码上的命中符合预期回归完整测试回到 crate 内执行cd tooling/lints cargo test确认 UI 快照全部通过、.stderr与用例一一对应且以空行结尾自检新 lint 是否符合文档全部纪律一模块一文件、注册到register_lints与register_late_pass、以notify_in_render.rs为模板、只 flag 不自动修复、不用MachineApplicable、跳过宏展开、用例带负向用例并遵循SHOULD FIRE/SHOULD NOT FIRE分节。附开发流程速查阶段操作依据/产物前置检查查 clippy 是否已覆盖命中则放弃避免冗余布局新建src/lint_name.rs在src/lib.rs声明 mod一 lint 一模块历史 lint 例外注册加入register_lints切片 追加register_late_passlib.rs#L52-L70实现复制notify_in_render.rs骨架declare_lint!impl_lint_pass!LateLintPass短路链notify_in_render.rs诊断span_lint一句话消息跳过宏展开禁MachineApplicable技能文档 Diagnostics rules复用render/gpui 判定优先用render_helpers.rs助手render_helpers.rsUI 用例写ui/lint_name.rs.stderr正/负向分节、触发用例在前ui/map_lookup_then_insert.rsgpui 依赖扩展test_fixture/gpuistub不引入真 gpuitest_fixture测试cd tooling/lints cargo test按Actual stderr saved to PATH手动更新.stderrcrate 内固定 nightly冒烟tooling/lints/single-lint lint_name [-p crate]single-lint收尾更新 README Current lints 列表README.md按上述流程产出的 lint 将同时满足 Zed 的代码库纪律render 主线程、gpui 类型语义与 dylint 工程约束固定 nightly、独立 workspace、UI 快照测试既能作为 Zed 持续集成的一部分运行也可在本地用single-lint随时对指定 crate 做定向验证。【免费下载链接】zedCode at the speed of thought – Zed is a high-performance, multiplayer code editor from the creators of Atom and Tree-sitter.项目地址: https://gitcode.com/GitHub_Trending/ze/zed创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表