Repository navigation
Description
Check for existing issues
- Completed
Environment
- OS: macOS (Apple Silicon)
- Install method: Homebrew. I also got the same result with the official
vale_3.22.0_macOS_arm64.tar.gzrelease. - Vale version: 3.22.0
Describe the bug / provide steps to reproduce it
Running Vale repeatedly on the same file with the same config sometimes gives one alert fewer. Nothing changes between runs. Every version I tried from v3.18.0 to v3.22.0 does this. Every version I tried up to v3.17.1 gives the same output every time.
The smallest repro I could make (five tiny files):
.vale.ini
StylesPath = styles
[*.txt]
BasedOnStyles = Teststyles/Test/Quotes.yml
extends: consistency
message: "Inconsistent quote style '%s'"
level: error
nonword: true
either:
'"': '”'
"'": '’'styles/Test/Para.yml (it matches nothing in this input; it only has to be enabled)
extends: existence
message: "never fires"
level: error
scope: paragraph
tokens:
- 'zzzqqq'input.txt is a ", a ' and a ”, then a blank line, then ”’. It needs to be exactly 13 bytes with no trailing newline:
printf '\042\047\342\200\235\n\n\342\200\235\342\200\231' > input.txtrepro.sh runs Vale N times and counts how many different outputs it sees:
#!/bin/sh
n=${1:-200}
i=0
while [ "$i" -lt "$n" ]; do
vale --output=line input.txt | sort | tr '\n' '|'
echo
i=$((i + 1))
done | sort | uniq -c | sort -rnFrom the folder that holds .vale.ini, run sh repro.sh 300. I expected one line of output. On 3.22.0 I get two:
243 input.txt:1:3:Test.Quotes:Inconsistent quote style '”'|input.txt:3:1:Test.Quotes:Inconsistent quote style '”'|input.txt:3:2:Test.Quotes:Inconsistent quote style '’'|
57 input.txt:1:3:Test.Quotes:Inconsistent quote style '”'|input.txt:3:2:Test.Quotes:Inconsistent quote style '’'|
The second line is the same as the first but without the 3:1 alert. In three batches of 300 runs, 57, 66 and 82 runs came out short, so roughly one run in four or five. I can't tell whether 2 or 3 alerts is the "right" answer here. The problem is only that it changes from run to run.
What I found
Same repro, official release binaries:
| Version | Result |
|---|---|
| v2.29.0, v3.0.1, v3.2.2, v3.7.1 | always 2 alerts (a different but stable answer; 100–200 runs each) |
| v3.11.0, v3.15.1, v3.15.2, v3.16.0, v3.17.0, v3.17.1 | always 3 alerts (100–300 runs each) |
| v3.18.0 | 242 × 3 alerts, 58 × 2 alerts (300 runs) |
| v3.19.0 | 158 × 3, 42 × 2 (200 runs) |
| v3.20.0 | 147 × 3, 53 × 2 (200 runs) |
| v3.21.0 | 156 × 3, 44 × 2 (200 runs) |
| v3.22.0 | 152 × 3, 48 × 2 (200 runs) |
In this example it needs both of these at the same time: a consistency rule with two pairs under either, and an enabled rule with scope: paragraph. In each of these variants the output was identical on every run (at least 150 runs each):
- delete
Para.yml - keep only the
"pair inQuotes.yml - split
Quotes.ymlinto two rules with one pair each - use
input.mdwith[*.md]in.vale.iniinstead of.txt
GOMAXPROCS=1 sh repro.sh 300 still varies (57 short runs), so I don't think it is a race between threads, though I haven't ruled out everything.
My guess at the cause. I suspect the order of the two pairs matters. NewConsistency builds its steps with for v1, v2 := range rule.Either, a Go map, and Go doesn't fix the order of a map, so the pairs can end up in either order from one run to the next. Both steps report under the same check name, and the missing alert is always the second ”, which is the only text reported twice under that name. That map loop is also in v3.17.1, so on its own it isn't new. What is new in v3.18.0 is the per-check bookkeeping in File.AddAlert (chkMasked in internal/core/file.go, which I couldn't find in the v3.17.1 tag). I haven't built Vale or bisected, so I don't know if that is the cause.
Related
#476 was a similar problem for substitution, where the advice was to split the patterns into separate files. Splitting also makes this case stable, as listed above. But the two pairs in a consistency rule don't overlap each other, so I'd expect their order not to matter, and this only started in v3.18.0.
I used Claude Code to help investigate this and to write it up. The repro is self-contained.
Activity
fix: locate a match past a multi-byte mask (#1184) (#1193)