A place to accumulate what goes wrong when authoring Vale rules, so the recipe can prevent it rather than each author rediscovering it.
The organizing principle: everything below passes verify and test. That is what makes these worth documenting. A malformed Vale rule is caught immediately; a wrong one is silent, ships green, and is discovered when someone notices the rule has never reported anything. The recipe already documents the %s interpolation trap on exactly this basis — these are the same class.
Each entry is measured against the pinned binary, not reasoned from the docs.
1. A token made of punctuation needs nonword: true
Vale wraps every token in word boundaries. An em dash is non-word on both sides, so \b—\b can never match.
extends: existence
level: error
tokens: ['—', '–'] # parses, verifies, and never fires
Adding nonword: true fixes it. Measured both ways: without it the fail/ fixture does not fire and test reports the rule as passing its own fixtures only because nothing fires anywhere.
This one bit us for real — the em-dash rule specified in #104 carries exactly this shape, so it would have shipped as a no-op.
Rule of thumb for the recipe: if a token is not made of word characters, it needs nonword: true.
2. The scope table, measured
scope |
prose |
inline code |
fenced block |
text |
✓ |
|
|
code |
|
✓ |
|
[code, text] |
✓ |
✓ |
|
raw |
✓ |
✓ |
✓ |
raw subsumes the other two, so a bare raw and [raw, code, text] behave identically.
3. scope is per-rule, and rules do not interfere
Vale assembles one config for the whole run, which makes it reasonable to assume scopes interact. They do not. Same document, same run: a default-scoped rule stayed out of a fenced block and honored a suppression comment, while a raw-scoped rule read through both.
Worth stating in the recipe explicitly, because the assumption otherwise discourages using raw at all.
4. raw and Vale's suppression comments are mutually exclusive
scope |
<!-- vale Style.Rule = NO --> honored |
[raw, code, text] |
no |
[code, text] |
yes |
Under raw Vale reads the unparsed text, so a markup-level directive is invisible. A rule that reaches into fenced blocks cannot be annotated away case by case — which matters because a rule about a command needs raw (see #167). Detail in the comment on that issue.
5. Prefer collocations to bare words
A token that is a whole word will find the sense you did not mean, and the near-miss is rarely the one you predicted. #104 anticipated this for land/landed and proposed landed on as a safe narrowing. Measured, landed on fires on "the plane landed on time" — the literal sense the issue was protecting. we landed already covered the decision sense, so the broader token added only false positives.
For the recipe: when narrowing a banned word to a collocation, write the pass/ fixture from the literal sense first, then check the collocation against it.
6. Fixture design should follow the rule's subject
The recipe says a pass/ fixture must contain the phrase outside the scope being narrowed. The converse is missing and is what catches #167: when the rule's subject normally appears in code, the fail/ fixture must contain it inline, fenced, and in prose. A fixture written only as prose passes while the rule is inert against every real document.
7. How to exclude files from a rule
The recipe explains how to scope a rule in, and never how to scope it out. A second matcher with = NO works:
[**/README.md]
tskl) rule = my-rule
BasedOnStyles =
my-rule.my-rule = YES
# Test fixtures are inputs to a test suite, not documentation.
[**/test/fixtures/**/README.md]
tskl) rule = my-rule
BasedOnStyles =
my-rule.my-rule = NO
Measured working, and needed immediately: without it the first house-style rules fired on the CLI's own test fixtures, which hold deliberately wrong prose. #104 anticipates the need and does not say how.
Refs #167
Refs #104
A place to accumulate what goes wrong when authoring Vale rules, so the recipe can prevent it rather than each author rediscovering it.
The organizing principle: everything below passes
verifyandtest. That is what makes these worth documenting. A malformed Vale rule is caught immediately; a wrong one is silent, ships green, and is discovered when someone notices the rule has never reported anything. The recipe already documents the%sinterpolation trap on exactly this basis — these are the same class.Each entry is measured against the pinned binary, not reasoned from the docs.
1. A token made of punctuation needs
nonword: trueVale wraps every token in word boundaries. An em dash is non-word on both sides, so
\b—\bcan never match.Adding
nonword: truefixes it. Measured both ways: without it thefail/fixture does not fire andtestreports the rule as passing its own fixtures only because nothing fires anywhere.This one bit us for real — the em-dash rule specified in #104 carries exactly this shape, so it would have shipped as a no-op.
Rule of thumb for the recipe: if a token is not made of word characters, it needs
nonword: true.2. The
scopetable, measuredscopetextcode[code, text]rawrawsubsumes the other two, so a barerawand[raw, code, text]behave identically.3.
scopeis per-rule, and rules do not interfereVale assembles one config for the whole run, which makes it reasonable to assume scopes interact. They do not. Same document, same run: a default-scoped rule stayed out of a fenced block and honored a suppression comment, while a
raw-scoped rule read through both.Worth stating in the recipe explicitly, because the assumption otherwise discourages using
rawat all.4.
rawand Vale's suppression comments are mutually exclusivescope<!-- vale Style.Rule = NO -->honored[raw, code, text][code, text]Under
rawVale reads the unparsed text, so a markup-level directive is invisible. A rule that reaches into fenced blocks cannot be annotated away case by case — which matters because a rule about a command needsraw(see #167). Detail in the comment on that issue.5. Prefer collocations to bare words
A token that is a whole word will find the sense you did not mean, and the near-miss is rarely the one you predicted. #104 anticipated this for
land/landedand proposedlanded onas a safe narrowing. Measured,landed onfires on "the plane landed on time" — the literal sense the issue was protecting.we landedalready covered the decision sense, so the broader token added only false positives.For the recipe: when narrowing a banned word to a collocation, write the
pass/fixture from the literal sense first, then check the collocation against it.6. Fixture design should follow the rule's subject
The recipe says a
pass/fixture must contain the phrase outside the scope being narrowed. The converse is missing and is what catches #167: when the rule's subject normally appears in code, thefail/fixture must contain it inline, fenced, and in prose. A fixture written only as prose passes while the rule is inert against every real document.7. How to exclude files from a rule
The recipe explains how to scope a rule in, and never how to scope it out. A second matcher with
= NOworks:Measured working, and needed immediately: without it the first house-style rules fired on the CLI's own test fixtures, which hold deliberately wrong prose. #104 anticipates the need and does not say how.
Refs #167
Refs #104