Standard Ruby(规范工具)
我们知道Ruby创始人松本行弘(Matz, まつもとゆきひろ)设计Ruby的核心理念是: "让程序员快乐" 。他认为语言应该顺应人的思维,而不是让人去屈服于机器的规则。因此,Matz 特意让 Ruby 具备了极大的灵活性,甚至故意遵循 “条条大路通罗马”(TIMTOWTDI - There's more than one way to do it) 的原则。在 Ruby 中,为了让代码读起来像散文一样通顺,同一个逻辑真的可以用完全不同的语法来写。
这种极少的约束,赋予了开发者极大的自由度。
然而, 极致的自由在团队协作和大型工程中往往会带来灾难 。如果没有任何约束,同一个项目里可能会出现五花八门的写法,导致代码可读性变差。
于是,在长达二十多年的社区实践中,Ruby 社区经历了一个“始于自由,成于约定”的自发演进过程:
大浪淘沙 :无数天才开发者在写了大量开源库(如 Rails、Sinatra、Sidekiq)后,逐渐摸索出哪些写法能让代码最清晰、最不易出错且性能最好。
形成共识 : 这些写法被总结为“社区最佳实践”(Best Practices)或“惯用写法”(Idiomatic Ruby)
工具化固化 :
最早大家把这些约定写成著名的 Ruby Style Guide
随后,社区开发了
RuboCop静态代码分析工具,把这些文字指南变成了可以自动扫描代码的“警报器”现在 GitHub: standardrb/standard 采用了另一个极端: 把社区最公认、最现代、最简洁的那一套最佳实践彻底“锁死”,不允许任何人修改规则,用代码强行统一风格。
备注
这是一种工程化的ruby规范工具,对于开发大型软件以及团队协作至关重要
Ruby 在底层把自由交给了你,但社区在工程层面上用“约定”和类似 standardrb 的工具把你引向了最高效、最优雅的道路。
这是一种非常奇妙的平衡:它既保留了你独自写脚本时的灵动与快乐,又保障了你在团队开发大项目时的严谨与工程规范。
standardrb结合nvim
备注
我在 Debian镜像(tini进程管理器) 也补充了这段 standard 集成配置
编辑器调用
standard之前,系统需要先安装standard可执行程序:
standardgem install standard
如果是项目中使用(例如Rails项目),则修订 Gemfile 的 development 组添加 gem 'standard' ,然后执行 bundle install
针对 lazy.nvim 不需要手工下载插件,只需要 要给现有的格式化和语法检查插件增加对 standard 的支持 : 在
~/.config/vim/lua/plugins/目录下创建standard.lua配置
~/.config/vim/lua/plugins/standard.luareturn {
-- 1. 配置代码格式化(Conform.nvim)
{
"stevearc/conform.nvim",
opts = {
formatters_by_ft = {
ruby = { "standard" },
},
},
},
-- 2. 配置代码静态检查(nvim-lint)
{
"mfussenegger/nvim-lint",
opts = {
linters_by_ft = {
ruby = { "standardrb" },
},
},
},
-- 3. 让 Mason 自动追踪管理 standardrb
{
"mason-org/mason.nvim",
opts = function(_, opts)
if type(opts.ensure_installed) == "table" then
table.insert(opts.ensure_installed, "standardrb")
end
end,
},
}
重新打开 neovim ,此时 LazyVim 会自动读取新配置并在后台通过 Mason 安装
standardrb相关支持,通过:Mason可以查看standardrb是否安装成功编写一个格式混乱的ruby代码:
def calculate_score( name,score )
puts "Processing user: " + name
if score > 90
return 'Excellent'
else
'Good'
end
end
user_name = "John"
final_score = 95
# 这里故意留了很多空行和混乱的缩进
puts calculate_score(user_name,final_score)
保存一次,就会自动修订成:
def calculate_score(name, score)
puts 'Processing user: ' + name
return 'Excellent' if score > 90
'Good'
end
user_name = 'John'
final_score = 95
# 这里故意留了很多空行和混乱的缩进
puts calculate_score(user_name, final_score)
注意还会一些提示需要手工修改 Style/StringConcatenation: Prefer string interpolation to string concatenation. 原因是风格更期望采用内嵌表达式( #{} )而不是通过 + 连接字符串和变量。这里没有自动修复是因为我在 macOS工作室 和 Debian镜像(tini进程管理器) 中使用了微软和Shopify联合开发的 ruby-lsp 语言服务器,特点是极快、轻量级,但是目前自带的Code Actions(代码重构动作)比老牌的 solargraph 克制和保守得多,所以没有包含"字符串拼接转内嵌" 的自动重构。最佳实践应该是:
puts "Processing user: #{name}"
另外,现代ruby应该在开头加上注释 # frozen_string_literal: true 可以在处理大量重复的字符串(例如循环)指向同一个内存地址,极大节省了内存并减轻GC的负担
standard.lua改进过程
警告
我这段折腾最后发现没有什么改进效果,gemini太兜圈子了(幻觉?),最终放弃,还是回到最简单的配置
我最初采用gemini提供的 standard.lua
~/.config/vim/lua/plugins/standard.luareturn {
-- 1. 配置代码格式化(Conform.nvim)
{
"stevearc/conform.nvim",
opts = {
formatters_by_ft = {
ruby = { "standard" },
},
},
},
-- 2. 配置代码静态检查(nvim-lint)- 彻底修复版
{
"mfussenegger/nvim-lint",
opts = function(_, opts)
-- A. 安全地注入你的语言映射
opts.linters_by_ft = opts.linters_by_ft or {}
opts.linters_by_ft.ruby = { "standard" }
-- B. 核心安全防御:利用 LazyVim 的加载时机动态修改参数,绝不碰它的默认 parser
opts.linters = opts.linters or {}
opts.linters.standard = vim.tbl_deep_extend("force", opts.linters.standard or {}, {
cmd = "standardrb",
stdin = true,
args = { "--format", "json", "--stdin", "%:p" },
ignore_exit_code = true,
})
end,
},
-- 3. 让 Mason 自动追踪管理 standardrb
{
"mason-org/mason.nvim",
opts = function(_, opts)
if type(opts.ensure_installed) == "table" then
table.insert(opts.ensure_installed, "standardrb")
end
end,
},
}
这个配置中有一点小问题,每次编辑ruby文件,会一闪而过 /Users/admin/.local/share/nvim/lazy/nvim-lint/lua/lint.lua:278: attempt to index local 'parse… ,但是实际上没有进行 ruby_lsp 索引。gemini解释是因为当 nvim-lint 尝试合并传入的自定义属性是,因为某些更深层的加载顺序问题,它自己内部的完整对象还没有构造完。 这里通过声明式 opts 轻微改动依然会触发内部源码的断层。
改进方法: 重构平替方案:彻底把 nvim-lint 里的 standard 拆解出来,作为一个全新的、完全由你一手掌控的独立自定义 Linter。也就是 不覆写它官方内置的 standard 槽位,而是定义一个全新的 mystandard 名字,这样就绕过了原生插件底层的初始化冲突。
mystandard 名字避免 standard.lua 影响官方内置 standardreturn {
-- 1. 配置代码格式化(Conform.nvim)
{
"stevearc/conform.nvim",
opts = {
formatters_by_ft = {
ruby = { "standard" },
},
},
},
-- 2. 配置代码静态检查(nvim-lint)- 彻底修复版
{
"mfussenegger/nvim-lint",
opts = function(_, opts)
-- A. 安全地注入你的语言映射
opts.linters_by_ft = opts.linters_by_ft or {}
-- 👈 关键点 A:我们将映射名改为自定义的 "mystandard"
opts.linters_by_ft.ruby = { "mystandard" }
-- 👈 关键点 B:直接借用 rubocop 现成的 parser 逻辑,纯声明式定义你的专属检查器
-- B. 核心安全防御:利用 LazyVim 的加载时机动态修改参数,绝不碰它的默认 parser
opts.linters.mystandard = {
cmd = "standardrb",
stdin = true,
args = { "--format", "json", "--stdin", "%:p" },
ignore_exit_code = true,
parser = function(output, bufnr)
-- 健壮安全检查:如果什么都没吐出来或者不是标准的 json 格式,直接安全返回空数组,绝不引发弹窗崩溃
if not output or output == "" or not output:match("^%s*[{[]") then
return {}
end
-- 动态回退到标准的 rubocop 解析机制,平滑展示所有的黄色/红色波浪线
local success, lint = pcall(require, "nvim-lint")
if success and lint.linters.rubocop then
return lint.linters.rubocop.parser(output, bufnr)
end
return {}
end,
}
end,
},
-- 3. 让 Mason 自动追踪管理 standardrb
{
"mason-org/mason.nvim",
opts = function(_, opts)
if type(opts.ensure_installed) == "table" then
table.insert(opts.ensure_installed, "standardrb")
end
end,
},
}
果然,这样修改之后nvim编辑ruby文件就会看到右下角有索引的行为 "% completed (72%) ⠙ Ruby LSP: indexing files ruby_lsp"
不过,以前检查完成后,右上角有一个一闪而过的 Linter command standardrb exited with code: 1 ,gemini解释是因为把静态检查的名字修改成了完全自定义的 mystandard ,这对于 nvim-lint 内部处理中,只有原生的Linter才会自动读取配置 ignore_exit_code = true 属性,导致忽略了传入的防崩参数
再次改回使用内置的 standard 利用一个空对象将推出状态安全吞掉
standard.lua 回内置 standard 但增加一个空表防止初始化未完成return {
-- 1. 配置代码格式化(Conform.nvim)
{
"stevearc/conform.nvim",
opts = {
formatters_by_ft = {
ruby = { "standard" },
},
},
},
-- 2. 配置代码静态检查(nvim-lint)- 彻底修复版
{
"mfussenegger/nvim-lint",
opts = function(_, opts)
-- A. 安全地注入你的语言映射
opts.linters_by_ft = opts.linters_by_ft or {}
-- 重新用回内置的 "standard" 槽位,但是采用 LazyVim 的非破坏性局部赋值,保证既不丢原生的 parser,又能继承安全参数
opts.linters_by_ft.ruby = { "standard" }
-- 如果原生的 standard 骨架未初始化,我们先给它一个空表
opts.linters.standard = opts.linters.standard or {}
-- 局部精准修正参数,绝不整块重写,确保原厂的 parser 函数完好无损
opts.linters.standard.cmd = "standardrb"
opts.linters.standard.stdin = true
opts.linters.standard.args = { "--format", "json", "--stdin", "%:p" }
opts.linters.standard.ignore_exit_code = true
end,
},
-- 3. 让 Mason 自动追踪管理 standardrb
{
"mason-org/mason.nvim",
opts = function(_, opts)
if type(opts.ensure_installed) == "table" then
table.insert(opts.ensure_installed, "standardrb")
end
end,
},
}
这次修改以后,虽然右下角显示Ruby 语言服务(LSP)索引是正常进行的,但是在索引之前,可以看到系统似乎停顿了一下,右上角一闪而过显示 /Users/admin/.local/share/nvim/lazy/nvim-lint/lua/lint.lua:278: attempt to index local 'parse…
gemini解释是打开Ruby问见时会触发一次静态检查,此时nvim-lint 的内部原生 standard 工具对象还只是一个“空壳”或者还没被框架完全实例化,它的 parser 属性此时处于 nil 状态。虽然在 opts 函数里通过 opts.linters.standard.cmd = "standardrb" 进行了局部赋值,但在赋值的瞬间,原厂的 parser 还没有准备好,随后的检查逻辑直接在底层代码的第 278 行因为触碰了 nil 的 parser 而引发了这一瞬间的弹窗。
解决的方法是延迟替换: 当 Neovim 启动时, standard.lua 保持绝对纯净,零干扰;只有当 ruby-lsp 彻底就绪、右下角的 indexing files 开始滚动、整个 nvim-lint 已经 100% 物理实例化完毕之后,才动态地把 standard 的底层执行命令替换为 standardrb 。
standard.lua 等parser就绪之后再修改standard为standardrbreturn {
-- 1. 配置代码格式化(Conform.nvim)
{
"stevearc/conform.nvim",
opts = {
formatters_by_ft = {
ruby = { "standard" },
},
},
},
-- 2. 配置代码静态检查(nvim-lint)- 极简配置,绝不提前触碰属性
{
"mfussenegger/nvim-lint",
opts = {
linters_by_ft = {
ruby = { "standard" },
},
},
},
-- 3. 让 Mason 自动追踪管理 standardrb
{
"mason-org/mason.nvim",
opts = function(_, opts)
if type(opts.ensure_installed) == "table" then
table.insert(opts.ensure_installed, "standardrb")
end
end,
},
-- 4. 🚀 终结技:利用 Neovim 的事件机制,在全环境完全就绪后,再安全修正标准命令
{
"neovim/nvim-lspconfig",
opts = function()
-- 创建一个针对 Ruby 文件的自动命令
vim.api.nvim_create_autocmd("FileType", {
pattern = "ruby",
callback = function()
-- 延迟 100 毫秒执行,死死确保 nvim-lint 的内部对象已经完全由原厂安全初始化完毕
vim.defer_fn(function()
local success, lint = pcall(require, "nvim-lint")
if success and lint.linters and lint.linters.standard then
-- 此时原厂的 parser 已经 100% 就绪,我们只安全地修正它的路径和参数
lint.linters.standard.cmd = "standardrb"
lint.linters.standard.stdin = true
lint.linters.standard.args = { "--format", "json", "--stdin", "%:p" }
lint.linters.standard.ignore_exit_code = true
end
end, 100)
end,
})
end,
},
}
现在编辑 ruby 文件时候,确实能够正确索引,并且也不再出现 /Users/admin/.local/share/nvim/lazy/nvim-lint/lua/lint.lua:278: attempt to index local 'parse… ,但是一闪而过改成了 Linter not found: standard ,虽然看起来没有影响,但是还是有点逼死强迫症。原因是打开Ruby文件的瞬间,LazyVim 极其高效的核心框架会瞬间扫描插件声明。此时 ruby = { "standard" } 会使得 nvim-lint 在系统中寻找名为 standard 的可执行文件。但是由于历史原因,这个执行名字是 standardrb 。这要等待后面的自动命令(Autocmd)触发订正为 standardrb 才会解决。
太折腾了,既然无法解决初始化一开始时候找不到standard命令(虽然可以通过后续补丁来修正,但是无法消除开始时候一闪而过的找不到standard的报错),那不如回退到最简单的方式,为 standardrb 建立一个软链接:
standardrb 软链接ln -s $(which standardrb) $(dirname $(which standardrb))/standard
这就从根本上解决了自动找到 standard 快捷方式指向 standardrb
那么我们前面复杂的hack脚本已经不再需要,改回最初最简单的模式:
~/.config/nvim/lua/plugins/standard.luareturn {
-- 1. 配置代码格式化(Conform.nvim)
{
"stevearc/conform.nvim",
opts = {
formatters_by_ft = {
ruby = { "standard" },
},
},
},
-- 2. 配置代码静态检查(nvim-lint)
{
"mfussenegger/nvim-lint",
opts = {
linters_by_ft = {
ruby = { "standardrb" },
},
},
},
-- 3. 让 Mason 自动追踪管理 standardrb
{
"mason-org/mason.nvim",
opts = function(_, opts)
if type(opts.ensure_installed) == "table" then
table.insert(opts.ensure_installed, "standardrb")
end
end,
},
}
我发现这个简单配置其实就足够了,这里要注意的是 nvim-lint 使用的是命令行工具,所以要使用实际的安装命令 standardrb ,否则就会报错 Linter not found: standard 。现在已经实现了目标,唯一的小遗憾是有不规范的语法时 standardrb 返回的值是 1 会导致屏幕上一闪而过 Linter command standardrb exited with code: 1 ,不过不影响使用,我尝试了多次也改变不了,就只能这样了。