Skip to content

Commit a165f8d

Browse files
committed
fix: match sequence patterns against whole tokens
Signed-off-by: Joseph Kato <joseph@jdkato.io>
1 parent be96fa9 commit a165f8d

2 files changed

Lines changed: 80 additions & 1 deletion

File tree

internal/check/sequence.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,16 @@ type NLPToken struct {
3535
// words".
3636
Target bool
3737

38+
// re finds candidate positions by scanning the whole sentence, so it must
39+
// not be anchored.
3840
re *rx.Regexp
3941

42+
// tokenRe tests one token, so it must be. A `pattern` names the word a
43+
// position accepts: without anchoring, `self` also accepts the single
44+
// token `self-worth`, and a rule for `your self` fires on `your
45+
// self-worth`.
46+
tokenRe *rx.Regexp
47+
4048
// wordRe narrows a universal tag to the words it can apply to.
4149
//
4250
// Kept apart from `re` because that one doubles as the anchor and is run
@@ -123,6 +131,17 @@ func NewSequence(cfg *core.Config, generic baseCheck, path string) (Sequence, er
123131
return rule, core.NewE201FromPosition(errc.Error(), path, 1)
124132
}
125133
rule.Tokens[i].re = re
134+
135+
anchored := fmt.Sprintf(tokenTemplate, token.Pattern)
136+
if rule.Ignorecase {
137+
anchored = ignoreCase + anchored
138+
}
139+
140+
tre, terr := rx.Compile(anchored)
141+
if terr != nil {
142+
return rule, core.NewE201FromPosition(terr.Error(), path, 1)
143+
}
144+
rule.Tokens[i].tokenRe = tre
126145
}
127146
}
128147

@@ -175,7 +194,8 @@ func tokensMatch(token NLPToken, word tag.Token) bool {
175194
}
176195

177196
failedTag = failedTag == token.Negate
178-
failedTok := token.re != nil && token.re.MatchStringStd(word.Text) == token.Negate
197+
failedTok := token.tokenRe != nil &&
198+
token.tokenRe.MatchStringStd(word.Text) == token.Negate
179199

180200
// A universal tag that Penn cannot express also restricts which words
181201
// qualify -- `upos: AUX` is "a verb, and one of these words".

internal/check/sequence_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package check
2+
3+
import (
4+
"testing"
5+
6+
"github.com/errata-ai/vale/v3/internal/core"
7+
"github.com/errata-ai/vale/v3/internal/nlp"
8+
)
9+
10+
// A sequence position names the word it accepts, so it has to match the whole
11+
// token.
12+
//
13+
// The regex that finds candidate positions is deliberately unanchored -- it is
14+
// run against the whole sentence -- and reusing it to test a token made
15+
// `pattern: self` accept the single token `self-worth`, so a rule for
16+
// `your self` fired on `your self-worth`.
17+
func TestSequenceMatchesWholeTokens(t *testing.T) {
18+
rule, err := NewSequence(testConfig(), baseCheck{
19+
"extends": "sequence",
20+
"name": "Test.Yourself",
21+
"level": "error",
22+
"message": "Did you mean 'yourself'?",
23+
"tokens": []interface{}{
24+
map[string]interface{}{"pattern": "your"},
25+
map[string]interface{}{"pattern": "self"},
26+
},
27+
}, "Test.Yourself")
28+
if err != nil {
29+
t.Fatalf("building rule: %v", err)
30+
}
31+
32+
cases := []struct {
33+
name string
34+
text string
35+
want int
36+
}{
37+
{"exact words", "Ask your self what matters.", 1},
38+
{"hyphenated compound", "Question your self-worth sometimes.", 0},
39+
{"longer word", "Consider your selfishness here.", 0},
40+
}
41+
42+
for _, c := range cases {
43+
t.Run(c.name, func(t *testing.T) {
44+
f := &core.File{NLP: nlp.Info{}}
45+
46+
alerts, rerr := rule.Run(nlp.NewBlock(c.text, c.text, "text"), f, testConfig())
47+
if rerr != nil {
48+
t.Fatalf("running rule: %v", rerr)
49+
}
50+
if len(alerts) != c.want {
51+
t.Errorf("%q produced %d alerts, want %d", c.text, len(alerts), c.want)
52+
}
53+
})
54+
}
55+
}
56+
57+
func testConfig() *core.Config {
58+
return &core.Config{WordTemplate: wordTemplate}
59+
}

0 commit comments

Comments
 (0)