- 开发工具
- 代码质量
- CLI
【免费下载链接】vale
:pencil: A markup-aware linter for prose built with speed and extensibility in mind.
导读
本文围绕仓库 testdata/fixtures/filters/test.md 这份 Markdown 测试样例,深入讲解 Vale 的--filter标志(规则过滤器)如何以表达式(expr)形式对检查规则进行二次筛选。你将掌握:过滤器表达式的三种写法(文件路径、StylesPath 资源、内联字符串)、可用的规则字段(.Name、.Level、.Scope、.Extends等)与优先级语义,并学会像 e2e 测试那样组合表达式实现"只保留 error 级告警""只看 heading 范围""排除指定规则"等实战场景。全文以测试样例为事实基线,辅以 filter.go、definition.go 等源码佐证,确保每一条结论都可在仓库中复现验证。
从test.md看这份样例文件的定位
文件内容全景
test.md 全文仅 13 行,是一份典型的"过滤器靶场"文档,它刻意混排了多种会触发不同检查规则的文本形态:
# intro Nginx is a web server which can also be used as a reverse proxy, load balancer, mail proxy and HTTP cache. HTTPie is is a command line HTTP client. Its goal is to make CLI interaction with web services as human-friendly as possible. That doesn’t work. That doesn't work. That doesn‘t work. abdominocentesis is a word.这份文件被 testdata/e2e/cli-filter.yaml 引用为dir: fixtures/filters下的唯一测试输入,其设计意图是让不同过滤器表达式作用于同一份文档时,产生互不相同但完全可预期的告警集合。各行的"探测目标"如下:
| 行 | 内容 | 会命中的检查 | 说明 |
|---|---|---|---|
| 第 1 行 | # intro | demo.Cap(标题大小写)、demo.HeadingStartsWithCapital(标题首字母大写) | demo.Cap是level: error,demo.HeadingStartsWithCapital是level: suggestion,恰好覆盖两个告警级别 |
| 第 5 行 | HTTPie is is a ... | Vale.Repetition | 内置重复词规则,level: error,message为'%s' is repeated! |
| 第 7/9/11 行 | 三种doesn't的引号变体(直引号、弯引号 U+2019、其他弯引号) | 视配置而定(拼写/术语类规则) | 用于验证拼写类过滤(filters字段,见 demo/Filters.yml) |
| 第 13 行 | abdominocentesis is a word. | 词汇表(vocab)相关 | 用于验证accept.txt/reject.txt的取舍效果 |
从源码结构看,这份文件的编排显然是为了让--filter测试用例能以最少的文本覆盖最多的规则维度:级别(error/suggestion)、范围(heading/text)、扩展类型(capitalization/existence/repetition)三大过滤维度各有一个明确的探针。
样例对应的配置环境
测试样例通过 testdata/fixtures/styles/demo/_vale 提供运行配置,其中关键设置为:
StylesPath = ../../../styles MinAlertLevel = suggestion [*] BasedOnStyles = demo [*.md] demo.Reading = YES demo.Spellcheck = YES demo.Filters = YES demo.Smart = YES demo.ScopedHeading = YES注意demo.Cap = NO出现在[*]段中——意味着demo.Cap规则默认不运行,而 demo/Cap.yml 声明它的level: error。这就是 cli-filter.yaml 中--filter=min.expr用例依然能输出demo.Cap告警的原因:过滤器表达式中出现了.Level字段,filter.go 会据此下调全局最低告警级别并显式启用相关规则。这一点我们将在后文源码分析中展开。
--filter的三种表达式形态
命令行定义
--filter标志在 cmd/vale/flag.go 中注册:
pflag.StringVar(&Flags.Filter, "filter", "", "An expression to filter rules by.")其值会通过 internal/check/filter.go 中的filter(mgr *Manager)函数解析,解析顺序(stringOrPath判定)分为三种形态:
- 表达式文件路径:若该字符串是一个真实存在的文件路径,则直接读取文件内容作为表达式。例如
--filter=min.expr .。 - StylesPath 下的资源:若该字符串能通过
core.FindAsset在StylesPath(或 Vale 样式目录)中找到对应文件,则读取该文件。例如--filter=min.expr会命中 testdata/styles/config/filters/min.expr。 - 内联表达式字符串:若以上都不满足,则把该字符串本身当作表达式。例如
--filter=.Extends in ['existence']。
if system.FileExists(stringOrPath) { // Case 1: 表达式文件 } else if found := core.FindAsset(mgr.Config, stringOrPath); found != "" { // Case 2: StylesPath 下的 .expr 资源 } else { // Case 3: 内联表达式 }三种形态可以互换,cli-filter.yaml 里就同时演示了文件形态(min.expr、scope.expr、levels.expr、extends.expr)与内联形态(--filter=.Extends in ['existence']、--filter=.Level in ['error', 'suggestion'] and .Name != 'demo.Cap'),且二者输出完全一致。
表达式引擎与求值方式
filter()的核心逻辑是把用户表达式包装进一个名为Rules的集合过滤调用中(internal/check/filter.go):
env := FilterEnv{} for _, rule := range mgr.rules { env.Rules = append(env.Rules, rule.Fields()) } code := fmt.Sprintf(`filter(Rules, {%s})`, filter) program, err := expr.Compile(code, expr.Env(env))也就是说,用户只需要写一个谓词闭包体(例如.Level in ["error"]),Vale 会自动把它放进filter(Rules, { ... })的上下文里,由github.com/expr-lang/expr编译执行。FilterEnv的定义见 internal/check/definition.go,它只有一个字段:
type FilterEnv struct { Rules []Definition }Rules中每个元素是一个Definition,其可用字段(internal/check/definition.go)就是过滤器表达式能引用的全部属性:
| 字段 | 类型 | 含义 | 来源/示例 |
|---|---|---|---|
.Name | string | 规则全名,如demo.Cap、Vale.Repetition | 规则 YAML 的文件名 |
.Level | string | 告警级别:suggestion/warning/error | 规则 YAML 的level |
.Scope | []string | 规则作用范围,如["heading"]、["heading.h1"] | 规则 YAML 的scope |
.Extends | string | 扩展类型:capitalization、existence、repetition等 | 规则 YAML 的extends |
.Message | string | 告警消息模板 | 规则 YAML 的message |
.Description | string | 规则描述 | 规则 YAML 的description |
.Link | string | 规则关联文档链接 | 规则 YAML 的link |
.Action | core.Action | 可执行动作(如替换建议) | action字段 |
.Extends的合法取值集合在 internal/check/definition.go 的extensionPoints中列出,包括capitalization、conditional、consistency、existence、occurrence、repetition、substitution、readability、spelling、sequence、metric、script共 12 种。这与 testdata/styles/demo 目录下各规则 YAML 的extends字段一一对应。
四个表达式文件逐个拆解
四个.expr文件位于 testdata/styles/config/filters,它们是官方 e2e 测试挑选的代表性过滤谓词,恰好覆盖了.Level、.Scope、.Name、.Extends四类字段的用法。
min.expr:按告警级别收紧
.Level in ["error"]作用于 test.md 的期望输出(cli-filter.yaml)为:
test.md:1:3:demo.Cap:'intro' should be in title case ('Intro'). test.md:5:8:Vale.Repetition:'is' is repeated!即只保留error级告警:demo.Cap(标题大小写错误)与Vale.Repetition(is重复)。suggestion级的demo.HeadingStartsWithCapital被过滤掉。exit: 1表示存在告警。
scope.expr:按作用范围收窄
"heading" in .Scope期望输出只剩demo.Cap一条:
test.md:1:3:demo.Cap:'intro' should be in title case ('Intro').因为只有demo.Cap(demo/Cap.yml 中scope: heading)与demo.HeadingStartsWithCapital(scope: heading.h1)作用于标题,但后者是suggestion级;而Vale.Repetition的scope: text不含heading,故被排除。注意"heading" in .Scope是子串包含语义——heading.h1也满足"heading" in,这从 selection.go 中"每个标题被包装进带in.<id>标识的 scope"的实现可以印证。
levels.expr:多级别 + 排除特定规则
.Level in ["error", "suggestion"] and .Name != "demo.Cap"期望输出为:
test.md:1:3:demo.HeadingStartsWithCapital:'intro' should be capitalized test.md:5:8:Vale.Repetition:'is' is repeated!这里用布尔逻辑演示了组合谓词:先取error与suggestion两个级别,再通过.Name != "demo.Cap"剔除大小写规则,最终留下标题首字母建议与重复词错误。
extends.expr:按扩展类型筛选
.Extends in ["existence"]期望输出为:
test.md:1:3:demo.HeadingStartsWithCapital:'intro' should be capitalized只保留extends: existence的规则(demo/HeadingStartsWithCapital.yml),capitalization与repetition类规则全部被过滤。exit: 0表示"命令成功执行",即便仍有 suggestion 级输出。
内联形态等价演示
cli-filter.yaml 还验证了两个内联写法与文件写法结果等价:
--filter=.Extends in ['existence'] --filter=.Level in ['error', 'suggestion'] and .Name != 'demo.Cap'注意内联时使用单引号(['existence'])即可,因为整个表达式作为 CLI 参数传递;而.expr文件里用双引号(["error"])。expr 语言两者均可,实际风格取决于 Shell 转义需求。
源码层面的过滤语义:为什么结果永远是"子集"
过滤发生在规则装载阶段
filter(mgr)在规则加载(AddRuleFromFile之后)时执行,返回的filteredmap 会替换mgr.rules,从而决定后续shouldRun对哪些规则放行(internal/lint/lint.go)。它的注释明确点出两条关键语义(internal/check/filter.go):
过滤结果永远只能是"完整结果的一个子集"——因为规则集合本身就是基于
.vale.ini加载的;过滤器只是在这一集合上再做一次筛选。
也就是说,--filter不能启用ini 中未加载的规则,只能在已装载规则中做减法。例如demo.Cap虽然在[*]段被= NO关闭,但它仍被加载进mgr.rules(关闭只是运行时开关),所以过滤器可以"找回"它;而一个BasedOnStyles之外、完全没有被引入的样式规则,则无法通过--filter凭空启用。
.Level的特殊副作用:动态下调最低告警级别
过滤器命中.Level字段时有一个隐藏行为(internal/check/filter.go):
if strings.Contains(code, ".Level") { lvl := core.LevelToInt[rule.Level] if lvl < mgr.Config.MinAlertLevel { mgr.Config.MinAlertLevel = lvl } }级别映射定义在 internal/core/alert.go:
var LevelToInt = map[string]int{ "suggestion": 0, "warning": 1, "error": 2, }MinAlertLevel的默认装载逻辑见 internal/core/ini.go(接受suggestion/warning/error)。这正是min.expr用例中demo.Cap(error)与Vale.Repetition(error)能输出、而suggestion级规则被过滤掉的底层原因:过滤器基于MinAlertLevel做二次裁剪,.Level谓词会把阈值动态压到表达式允许的最低级别。这也解释了为何extends.expr(不含.Level)的用例exit: 0——它不触碰级别阈值,默认MinAlertLevel = suggestion下demo.HeadingStartsWithCapital本就在输出范围内。
RuleForAlert:过滤后规则名的回退解析
过滤结果 map 的键需要经过mgr.RuleForAlert(rule.Name)规范化(filter.go)。该函数(internal/check/manager.go)按"最长已知前缀"把告警名映射回其定义规则——这对consistency这类把匹配词拼进告警名的规则尤为重要,其单测见 internal/check/manager_test.go。
从测试用例反推实战场景
以 cli-filter.yaml 的用例为蓝本,可归纳出四类可直接复用的实战组合:
| 需求 | 表达式 | 效果 |
|---|---|---|
| 只关注硬错误 | --filter=.Level in ['error'] | 屏蔽所有 suggestion/warning,CI 友好 |
| 只看标题问题 | --filter='heading' in .Scope | 输出收敛到标题相关检查 |
| 多级别 + 豁免个别规则 | --filter=.Level in ['error','suggestion'] and .Name != 'demo.Cap' | 组合布尔逻辑精细控制 |
| 只跑某类扩展点 | --filter=.Extends in ['existence','substitution'] | 按规则类型批量筛选 |
与--minAlertLevel的区别
--filter与--minAlertLevel(cmd/vale/flag.go)不要混淆:前者是规则维度的取舍(决定"哪些规则参与"),后者是告警维度的阈值(决定"哪些级别展示")。二者叠加时,--filter中的.Level会下调MinAlertLevel(见上文副作用),而--minAlertLevel则在整个过滤链路末端再做一次展示裁剪(internal/core/source.go 中 CLI 传入的AlertLevel会覆盖 ini 配置)。
如何在本仓库中复现验证
推荐按以下步骤在仓库根目录复现 e2e 行为(对应 internal/e2e/e2e_test.go 的测试框架):
# 1) 使用表达式文件(等价于 cli-filter.yaml 的 min-alert-level 用例) vale --config=testdata/fixtures/styles/demo/_vale \ --filter=min.expr testdata/fixtures/filters # 2) 使用内联表达式 vale --config=testdata/fixtures/styles/demo/_vale \ --filter=".Extends in ['existence']" testdata/fixtures/filters # 3) 运行 e2e 测试套件中的 cli/filter 用例 go test ./internal/e2e/ -run TestCLI -v也可以把表达式文件临时放置到StylesPath下的config/filters/目录(如 testdata/styles/config/filters),再用--filter=文件名直接引用。四种.expr文件均可作为自定义过滤器的起点模板。
小结
test.md虽是一份仅 13 行的测试样例,却是理解 Vale--filter语义的最佳窗口:它用精心挑选的文本把capitalization、existence、repetition三类规则、error/suggestion 两种级别、heading/text 两个 scope 全部覆盖,而 cli-filter.yaml 的五个用例则为每个过滤维度提供了可复现的期望输出。结合 filter.go 的实现可以确认:过滤发生在规则装载阶段、结果恒为子集、.Level谓词会动态压低告警阈值,这三条语义决定了任何--filter表达式设计的边界。掌握这些后,你就能用一行表达式把 Vale 的输出精确裁剪到团队真正关心的检查维度,为 CI 流水线或编辑器插件提供稳定、可预期的 lint 信号。
- 开发工具
- 代码质量
- CLI
【免费下载链接】vale
:pencil: A markup-aware linter for prose built with speed and extensibility in mind.
相关推荐
Vale Markdown 解析与 Lint 实战:以 testdata/fixtures/formats/test.md 为基准的 markup-aware 散文检查剖析
Vale Markdown 解析与 Lint 实战:以 testdata/fixtures/formats/test.md 为基准的 markup aware
开发工具代码质量CLIFluvio regex-filter SmartModule 实战:用正则表达式过滤流式数据
Fluvio regex filter SmartModule 实战:用正则表达式过滤流式数据 导读 本文围绕 Fluvio 仓库中 smartmodule/r
流处理消息队列后端Apache RocketMQ 消息过滤实战:从 Tag 过滤到 SQL92 表达式过滤(Filter Example 详解)
Apache RocketMQ 消息过滤实战:从 Tag 过滤到 SQL92 表达式过滤(Filter Example 详解) 本指南基于 docs/en/Ex
消息队列后端微服务流处理
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考