资讯详情

postgres_lsp 自定义 lint 规则开发指南:从命名、实现、配置到测试的完整流程

📅 2026/9/18 0:11:51 | 华诺云谱 👁 阅读
postgres_lsp 自定义 lint 规则开发指南:从命名、实现、配置到测试的完整流程
postgres_lsp 自定义 lint 规则开发指南从命名、实现、配置到测试的完整流程【免费下载链接】postgres_lspA Language Server for Postgres项目地址: https://gitcode.com/GitHub_Trending/po/postgres_lsp导读本文以 postgres_lsp 项目的pgls_analysercrate 为核心系统讲解如何在其基于 libpg_query 抽象语法树AST的 lint 基础设施上创建一条全新的安全类safety规则。你将掌握规则命名约定、declare_lint_rule!宏的声明语法、LinterRuletrait 的run实现方式、serde驱动的规则选项配置、文档编写规范、快照测试流程以及借助just工具链与代码生成脚本自动化完成全流程的实战方法。读完本文你可以独立为该项目贡献一条经过完整测试、文档齐备且可被postgres-language-server.jsonc配置的 lint 规则。背景pgls_analyser在项目中的定位postgres_lsp 是一个面向 Postgres 的语言服务器其代码仓库由多个 crate 组成。其中crates/pgls_analyser是分析器核心 crate负责对 SQL 语句进行 lint 检查crates/pgls_analyse则提供宏与基础类型如declare_lint_rule!、RuleMeta、MetadataRegistry、AnalysisFilter等。本文所指的“Analyser”即 crates/pgls_analyser 目录其 Cargo.toml 中依赖了pgls_query基于 libpg_query 的 AST 表示、pgls_diagnostics诊断类型与渲染、pgls_consolemarkup 输出等 crate。从源码结构看pgls_analyser的规则按“组group”组织目前规则全部位于src/lint/safety/目录下对应“safety”组已有banDropColumn、addingFieldWithDefault、requireConcurrentIndexCreation等 50 余条规则每条规则对应一个.rs文件。核心入口Analyser定义在 src/lib.rs它接收AnalyserParams拆分后的 SQL 语句列表、可选的schema_cache与AnalyserConfigLinterOptions与AnalysisFilter内部通过LinterRuleRegistry::builder(filter)构建规则注册表然后对每条语句依次执行所有已启用规则的run函数汇总返回VecLinterDiagnostic。这也是“规则针对每条语句运行”这一模型的最直接源码证据。规则命名约定ban与use前缀在动手写代码前必须先确定规则名称。项目遵循一套与规则语义强绑定的命名约定前缀语义适用场景示例ban禁止某个单一概念规则的唯一意图是禁止某种写法banDropColumn、banTruncate、banUpdateWithoutWhereuse强制/要求某个单一概念规则的唯一意图是强制使用某种写法useMyRuleName文档中的示例名规则名的格式为banConcept/useConcept即前缀加驼峰概念名。例如仓库中真实的 ban_drop_column.rs 声明了name: banDropColumn。需要说明的是当前仓库已实现的规则以ban为主use前缀是文档定义的约定方向新增规则时应遵循同样的原则让规则名本身就能说明意图。用just new-lintrule一键搭建规则骨架由于一条规则需要同时创建和更新多个文件规则实现、注册表、选项类型、文档、测试等手动维护非常繁琐项目提供了基于 Just 的命令来生成规则骨架。just不在 Rust 工具链中需先用系统包管理器单独安装。生成一条新 lint 规则的命令如下just new-lintrule safety useMyRuleName (severity)其中severity是可选的可选值为info、warn、error缺省为error。从 justfile 的源码可以看到该命令实际执行的是cargo run -p xtask_codegen -- new-lintrule --categorylint --nameuseMyRuleName --groupsafety --severityseverity just gen-lint即先调用xtask/codegen生成规则相关文件再运行gen-lint触发完整的代码生成流程。生成的骨架文件中规则实现位于pgls_analyser/src/lint/safety/use_my_new_rule_name.rs你需要在这个文件里完成规则逻辑。骨架会同时生成注册表、选项类型别名等配套代码src/options.rs 中即为自动生成的各规则Options类型别名列表。[!TIP] 你不必在一个 PR 里把规则做到完美。项目鼓励先规划、再分多个 PR 逐步完善如果对 API 还不熟悉可以在 issue 中先描述计划。实现规则的三支柱原则项目对规则的信息传达有明确要求规则应当对用户足够有信息量并给出尽可能多的解释。写规则时须遵守三条“支柱pillars”告诉用户错误是什么通常是诊断信息diagnostic message本身告诉用户为什么触发通常通过附加节点label/detail实现告诉用户应该怎么做通常用代码建议code action实现若代码建议不适用则用 note 告知用户修复方式。这三条支柱直接映射到诊断类型的构建 API。查看 src/linter_rule.rs 可知LinterDiagnostic提供了一系列链式方法LinterDiagnostic::new(category, span, title)创建诊断并设置标题对应支柱 1label(span, msg)/detail(span, msg)附加说明对应支柱 2note(msg)、footer_list(message, list)、warning(msg)等追加页脚信息对应支柱 3 的 note 路径。诊断内部通过RuleAdvice结构src/linter_rule.rs统一承载 details、notes 与 suggestion list并最终由Advices::record交给诊断渲染器输出。以真实的ban_drop_column为例ban_drop_column.rsimpl LinterRule for BanDropColumn { type Options (); fn run(ctx: LinterRuleContextSelf) - VecLinterDiagnostic { let mut diagnostics Vec::new(); if let pgls_query::NodeEnum::AlterTableStmt(stmt) ctx.stmt() { for cmd in stmt.cmds { if let Some(pgls_query::NodeEnum::AlterTableCmd(cmd)) cmd.node cmd.subtype() pgls_query::protobuf::AlterTableType::AtDropColumn { diagnostics.push(LinterDiagnostic::new( rule_category!(), None, markup! { Dropping a column may break existing clients. }, ).detail(None, You can leave the column as nullable or delete the column once queries no longer select or modify the column.)); } } } diagnostics } }可以看到规则实现的三个要点type Options ();Options关联类型不一定要用但必须定义没有自定义选项时写()即可。LinterRuletrait 定义在 src/linter_rule.rs其约束为type Options: Default Clone Debugrun接收LinterRuleContextSelf并返回VecLinterDiagnostic遍历 AST通过ctx.stmt()拿到当前语句的根节点用pgls_query::NodeEnum的变体如AlterTableStmt、AlterTableCmd做模式匹配再比对protobuf::AlterTableType::AtDropColumn这样的子类型枚举来精确定位目标节点。也就是说规则本质上是“在 AST 上做模式匹配”上下文携带数据库信息LinterRuleContext还能通过ctx.schema_cache()拿到数据库 schema 缓存仅当用户配置了数据库连接时才可用。例如adding_field_with_default规则会读取schema_cache.version.major_version来判断 Postgres 主版本从而决定非易变 DEFAULT 是否安全见 adding_field_with_default.rs。写完实现后记得用just f格式化、just l执行 lint。声明规则declare_lint_rule!宏规则类型本身通过declare_lint_rule!宏声明。宏定义在 crates/pgls_analyse/src/macros.rs它做了两件事调用declare_rule!生成一个空枚举类型并为它实现RuleMeta记录 version、name、文档、severity 等元数据同时在当前模块声明一个rule_category!宏用于在编译期静态注入该规则的诊断类别。基本用法use pgls_analyse::declare_lint_rule; declare_lint_rule! { /// Documentation pub(crate) ExampleRule { version: next, name: myRuleName, severity: Severity::Error, recommended: false, } }各字段含义字段说明version规则的引入版本新规则通常写next表示随下一个版本发布name规则在配置与诊断中使用的名称驼峰对应postgres-language-server.jsonc中的键severity默认严重级别可选Severity::Info/Severity::Warning/Severity::Errorrecommended是否属于推荐启用的规则集合sources可选灵感来源值为static [RuleSource]deprecated可选标记规则已废弃值为true标注规则来源sources如果新规则借鉴了其他生态的既有规则如 Squawk可以添加sources元数据每个来源用RuleSource的一个变体表示。例如实现与 Squawk 的ban-drop-column行为一致的规则use pgls_analyse::{declare_lint_rule, RuleSource}; declare_lint_rule! { /// Documentation pub(crate) ExampleRule { version: next, name: myRuleName, severity: Severity::Error, recommended: false, sources: [RuleSource::Squawk(ban-drop-column)], } }仓库中真实规则普遍带有此标注例如BanDropColumn声明了sources: [RuleSource::Squawk(ban-drop-column)]。项目根目录下的 agentic/port_squawk_rules.md 等文件记录了将 Squawk 规则移植到本项目的规则与过程可作参考。使用rule_category!宏declare_lint_rule!会在所在模块内声明rule_category!宏它展开为当前规则对应的诊断类别如lint/safety/banDropColumn。相比动态解析类别名字符串它的优势是在编译期静态注入类别并校验其已正确注册到pgls_diagnostics库。用法如下impl Rule for BanDropColumn { type Options Options; fn run(ctx: RuleContextSelf) - VecRuleDiagnostic { vec![RuleDiagnostic::new( rule_category!(), None, message, )] } }注意实际 crate 中 trait 名与类型名做了重新导出pgls_analyser中Rule是LinterRule的别名RuleContext是LinterRuleContext的别名RuleDiagnostic是LinterDiagnostic的别名见 src/lib.rs因此上面例子中的impl Rule for BanDropColumn即impl LinterRule for BanDropColumn。为规则添加可配置选项配置文件的形态规则支持通过postgres-language-server.jsonc配置文件定制。假设规则myRule支持以下选项behaviorA/B/C之一、threshold0 到 255 的整数、behaviorExceptions字符串数组配置写法如下{ linter: { rules: { safety: { myRule: { level: warn, options: { behavior: A, threshold: 20, behaviorExceptions: [one, two] } } } } } }项目根目录的 postgres-language-server.jsonc 就是该配置文件的真实示例pgls_configurationcrate 负责将其解析为配置结构。定义 Rust 选项类型第一步是创建选项的 Rust 数据表示。文档给出的示例#[derive(Clone, Debug, Default)] pub struct MyRuleOptions { behavior: Behavior, threshold: u8, behavior_exceptions: Box[Boxstr] } #[derive(Clone, Debug, Defaul)] pub enum Behavior { #[default] A, B, C, }这里有两个值得注意的实践用Box[Boxstr]而不是VecString盒装切片与盒装字符串只占两个字two words而Vec/String各占三个字three words能节省内存。这是文档明确给出的性能考虑u8承载 0–255 的整数threshold的上限 255 正好是u8的最大值用无符号小整数类型让取值范围在类型层面自解释。接着把选项类型接到规则上并实现serde的Serialize/Deserialize编译器会提示缺失这些 traitimpl Rule for MyRule { type Options MyRuleOptions; }用serde属性对齐 JSON 配置规则选项的 JSON 形态由以下serde属性控制rename_all camelCase把所有字段重命名为驼峰风格与postgres-language-server.jsonc的命名风格保持一致如 Rust 字段behavior_exceptions对应 JSON 键behaviorExceptionsdeny_unknown_fields当配置中出现多余字段时报错避免拼写错误被静默忽略default结构体级字段缺失时使用Default值使字段可选。同时可以配合#[cfg_attr(feature schemars, derive(JsonSchema))]在启用schemarsfeature 时生成 JSON Schema。完整示例#[derive(Debug, Default, Clone, Serialize, Deserialize)] #[cfg_attr(feature schemars, derive(JsonSchema))] #[serde(rename_all camelCase, deny_unknown_fields, default)] pub struct MyRuleOptions { #[serde(default, skip_serializing_if is_default)] main_behavior: Behavior, #[serde(default, skip_serializing_if is_default)] extra_behaviors: VecBehavior, } #[derive(Debug, Default, Clone)] #[cfg_attr(feature schemars, derive(JsonSchema))] pub enum Behavior { #[default] A, B, C, }注意这里文档示例将main_behavior字段命名为驼峰mainBehavior省略下划线与rename_all camelCase的作用相呼应。项目对“是否加选项”持保守态度选项要尽量少只在确实需要时引入加选项之前值得先讨论。运行时如何拿到选项规则在run中通过ctx.options()取回自己的选项。底层机制可以从 src/linter_options.rs 窥见LinterOptions内部是LinterRules即FxHashMapRuleKey, RuleOptions的包装RuleOptions用(TypeId, Boxdyn Any)保存类型擦除的选项值value::O()通过TypeId校验后向下转型取回具体类型。LinterOptions::rule_options::R()则按规则的RuleKey查表并克隆出R::Options。也就是说配置解析后按规则名存入类型擦除的容器运行时再按类型还原这正是“每条规则拿到的永远是自己的选项类型”的保证。编写规则文档格式与约束规则文档是代码生成与规则页面渲染的重要输入必须遵守以下硬性规则第一段必须是规则的一句话简介且必须写在一行内该段落会被用作规则列表页的表格内容换行会破坏表格布局后续段落可自由补充细节文档必须有## Examples标题其下按顺序包含### Invalid与### Valid两个小节且### Invalid在前先展示规则何时触发如果规则有选项必须写在## Options小节每个代码块必须声明语言为sql### Invalid中的每个片段必须使用expect_diagnostic代码块属性代码生成脚本会根据该属性为片段生成并附加一条诊断一个片段必须且只能产生一条诊断### Valid中的片段可以只有一个可以用ignore代码块属性告诉代码生成脚本“不要为某个 invalid 片段生成诊断”。一个完整的规则文档示例与真实banDropColumn的实现几乎一致declare_lint_rule! { /// Dropping a column may break existing clients. /// /// Update your application code to no longer read or write the column. /// /// You can leave the column as nullable or delete the column once queries no longer select or modify the column. /// /// ## Examples /// /// ### Invalid /// /// sql,expect_diagnostic /// alter table test drop column id; /// /// pub BanDropColumn { version: next, name: banDropColumn, recommended: true, severity: Severity::Error, sources: [RuleSource::Squawk(ban-drop-column)], } }文档生成器会据此确保规则对该 SQL 恰好产生一条诊断并把该诊断的快照纳入规则文档页面。测试规则快速测试与快照测试快速测试debug_test想快速验证规则行为可打开 src/lib.rs 中的debug_test函数源码中该测试带#[ignore]属性默认跳过移除#[ignore]宏把SQL这个str的内容改成你需要的语句在RuleFilter::Rule(..)中传入你的组与规则名例如RuleFilter::Rule(safety, banDropColumn)。运行后规则产生的所有诊断会打印到控制台。快照测试tests/specs规则实现并文档化后必须在tests/specs/group/ruleName/目录下创建快照测试。每个测试文件应满足以一个注释说明该测试检查什么首行包含-- expect_lint/group/ruleName或-- expect_no_diagnostics包含会触发或不会触发规则的合法 SQL。以addSerialColumn规则为例的目录结构对应仓库中真实存在的 tests/specs/safety/addSerialColumntests/specs/safety/addSerialColumn/ ├── basic.sql # 触发规则的基础用例 ├── basic.sql.snap # 自动生成的快照 ├── bigserial.sql # 测试 bigserial 类型 ├── bigserial.sql.snap ├── generated_stored.sql # 测试 GENERATED ... STORED ├── generated_stored.sql.snap ├── valid_regular_column.sql # 合法用例——不应触发 └── valid_regular_column.sql.snap触发诊断的用例示例-- expect_lint/safety/addSerialColumn -- Test adding serial column to existing table ALTER TABLE prices ADD COLUMN id serial;不应触发诊断的用例示例-- Test adding regular column (should be safe) -- expect_no_diagnostics ALTER TABLE prices ADD COLUMN name text;测试入口在 tests/rules_tests.rs它通过pgls_test_macros::gen_tests!自动扫描tests/specs/**/*.sql生成测试每个用例先用pgls_statement_splitter拆分 SQL再用pgls_query::parse解析出 AST交给只启用当前规则的Analyser运行最后做两件事一是用insta生成/比对.snap快照包含输入与诊断输出二是解析测试文件首行的expect_*注释断言实际产生的诊断类别与数量完全匹配见 rules_tests.rs 中的Expectation逻辑。正因如此测试文件必须声明expect_*注释否则会直接 panic。运行与更新测试# 运行测试并生成快照 cargo test -p pgls_analyser --test rules_tests # 审查并接受新增/变更的快照 cargo insta test --accept # 或交互式审查快照 cargo insta review.sql.snap快照文件是自动生成的应提交到仓库它们记录了每个用例的预期诊断输出。触发代码生成just gen-lint项目内大量代码是由xtask/codegen脚本自动生成的例如 src/options.rs 顶部就标注了 “Generated file, do not edit by hand”。CI 会确保生成代码与实际代码保持同步一旦不同步就会构建失败。因此完成规则实现后必须运行代码生成以刷新所有依赖它的文件just gen-lint该命令在 justfile 中定义会调用cargo run -p xtask_codegen -- gen-lint生成分析器相关代码包括注册表、选项类型别名、规则索引与文档等。新增规则后不运行它CI 会报错。提交与废弃规则提交变更规则实现、测试、文档与生成代码都完成后就可以提交并开启 Pull Requestgit add -A git commit -m feat(pgls_analyser): myRuleName废弃规则当规则需要被废弃避免破坏性变更时在宏中追加deprecated: true字段即可并在文档中说明废弃原因。一个完整的废弃示例use pgls_analyse::declare_lint_rule; declare_lint_rule! { /// Dropping a column may break existing clients. /// /// Update your application code to no longer read or write the column. /// /// You can leave the column as nullable or delete the column once queries no longer select or modify the column. /// /// ## Examples /// /// ### Invalid /// /// sql,expect_diagnostic /// alter table test drop column id; /// /// pub BanDropColumn { version: next, name: banDropColumn, recommended: true, severity: Severity::Error, deprecated: true, sources: [RuleSource::Squawk(ban-drop-column)], } }总结与进阶路径从命名、宏声明、run实现、serde选项、文档规范到快照测试与代码生成一条完整的 lint 规则在 postgres_lsp 中的生命周期可以被概括为按ban/use约定起名用just new-lintrule safety name (severity)生成骨架在src/lint/safety/rule_name.rs中实现LinterRule::run用rule_category!()与LinterDiagnostic的链式方法践行“三支柱”如有需要定义Options类型并用serde属性对齐postgres-language-server.jsonc配置编写符合格式约束的文档单行简介、## Examples、expect_diagnostic用debug_test快速验证再在tests/specs/safety/RuleName/下补快照测试并跑cargo insta运行just gen-lint刷新生成代码just f/just l保证格式与 lint 通过提交并开启 PR必要时用deprecated: true标记废弃规则。若想深入理解规则运行时的细节建议继续阅读 crates/pgls_analyse 中的宏与元数据实现、src/linter_context.rs 的上下文类型以及 crates/pgls_analyser/tests/specs/safety 下各规则的测试用例如banDropColumn、addingFieldWithDefault、requireConcurrentIndexCreation的快照这些都是学习既有规则写法的最佳范例。【免费下载链接】postgres_lspA Language Server for Postgres项目地址: https://gitcode.com/GitHub_Trending/po/postgres_lsp创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。