11package check
22
33import (
4+ "slices"
5+ "sort"
6+ "strings"
47 "testing"
58
69 "github.com/errata-ai/vale/v3/internal/core"
@@ -164,12 +167,22 @@ func TestRecaseToTerm(t *testing.T) {
164167 cases := []struct {
165168 term , observed , want string
166169 }{
167- {"OAuth2?" , "oauth2" , "OAuth2" }, // optional char present
168- {"OpenAPI" , "openapi" , "OpenAPI" }, // plain literal
169- {`Wi\-?Fi` , "wi-fi" , "Wi-Fi" }, // escaped literal hyphen, aligned
170- {"Wi-?Fi" , "wifi" , "Wi-?Fi" }, // optional char absent -> fall back
171- {"[Pp]ython" , "python" , "[Pp]ython" }, // class -> fall back
172- {"(?:foo|bar)" , "foo" , "(?:foo|bar)" }, // group/alternation -> fall back
170+ {"OAuth2?" , "oauth2" , "OAuth2" }, // optional char present
171+ {"OAuth2?" , "Oauth" , "OAuth" }, // optional char absent -- see #997
172+ {"OpenAPI" , "openapi" , "OpenAPI" }, // plain literal
173+ {`Wi\-?Fi` , "wi-fi" , "Wi-Fi" }, // escaped literal hyphen
174+ {"Wi-?Fi" , "wifi" , "WiFi" }, // optional char absent
175+ {"Docker(file|ize)" , "dockerfile" , "Dockerfile" }, // alternation -- see #997
176+ {"Docker(file|ize)" , "DOCKERIZE" , "Dockerize" },
177+ {"(?:foo|bar)" , "foo" , "foo" }, // non-capturing group
178+
179+ // No spelling in the set matches, so the term stands.
180+ {"Docker(file|ize)" , "docker" , "Docker(file|ize)" },
181+
182+ // Not a finite set of spellings: nothing to name.
183+ {"[Pp]ython" , "python" , "[Pp]ython" },
184+ {`Py.*\b` , "pythonic" , `Py.*\b` },
185+ {"Go+gle" , "google" , "Go+gle" },
173186 }
174187 for _ , c := range cases {
175188 if got := recaseToTerm (c .term , c .observed ); got != c .want {
@@ -178,6 +191,67 @@ func TestRecaseToTerm(t *testing.T) {
178191 }
179192}
180193
194+ func TestExpandPattern (t * testing.T ) {
195+ cases := []struct {
196+ pattern string
197+ want []string
198+ }{
199+ {"OAuth" , []string {"OAuth" }},
200+ {"OAuth2?" , []string {"OAuth2" , "OAuth" }},
201+ {"Docker(file|ize)" , []string {"Dockerfile" , "Dockerize" }},
202+ {"Docker(?:file|ize)" , []string {"Dockerfile" , "Dockerize" }},
203+ {"foo|bar" , []string {"foo" , "bar" }},
204+ {`Wi\-Fi` , []string {"Wi-Fi" }},
205+ {"a(b|c)d?" , []string {"abd" , "ab" , "acd" , "ac" }},
206+
207+ // Optional group: the whole group drops out.
208+ {"Java(Script)?" , []string {"JavaScript" , "Java" }},
209+
210+ // Unbounded or class-based -- no finite set of spellings.
211+ {"[Pp]ython" , nil },
212+ {"Go+gle" , nil },
213+ {"Py.*" , nil },
214+ {`\d+` , nil },
215+ {"a{2,3}" , nil },
216+ {"(unclosed" , nil },
217+ {"unopened)" , nil },
218+ {"(?=lookahead)" , nil },
219+ }
220+
221+ for _ , c := range cases {
222+ got := expandPattern (c .pattern )
223+ if c .want == nil {
224+ if got != nil {
225+ t .Errorf ("expandPattern(%q) = %v, want nil" , c .pattern , got )
226+ }
227+ continue
228+ }
229+ if ! slices .Equal (sorted (got ), sorted (c .want )) {
230+ t .Errorf ("expandPattern(%q) = %v, want %v" , c .pattern , got , c .want )
231+ }
232+ }
233+ }
234+
235+ // A blown budget has to return nil rather than a truncated set: a partial list
236+ // would let recaseToTerm miss the spelling the writer actually used and report
237+ // the raw pattern, which is the bug this exists to avoid.
238+ func TestExpandPatternBudget (t * testing.T ) {
239+ // 2^7 = 128 spellings, past maxExpansions.
240+ if got := expandPattern (strings .Repeat ("(a|b)" , 7 )); got != nil {
241+ t .Errorf ("expected nil past the budget, got %d spellings" , len (got ))
242+ }
243+ // 2^5 = 32, inside it.
244+ if got := expandPattern (strings .Repeat ("(a|b)" , 5 )); len (got ) != 32 {
245+ t .Errorf ("expected 32 spellings, got %d" , len (got ))
246+ }
247+ }
248+
249+ func sorted (s []string ) []string {
250+ out := append ([]string {}, s ... )
251+ sort .Strings (out )
252+ return out
253+ }
254+
181255func TestOptions (t * testing.T ) {
182256 cases := map [string ][]string {
183257 "foo|bar" : {"foo" , "bar" },
0 commit comments