Skip to content

Commit dd09d42

Browse files
authored
fix(core): resolve Lang and Transform in section order, not map order (#1143)
* fix(core): resolve Lang and Transform in section order, not map order * fix(core): rename variables to eliminate shadowing --------- Co-authored-by: VXNCXNX <VXNCXNX@users.noreply.github.com>
1 parent 85ee608 commit dd09d42

2 files changed

Lines changed: 128 additions & 15 deletions

File tree

‎internal/core/file.go‎

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313
"github.com/jdkato/prose/v3/summarize"
1414
"github.com/jdkato/prose/v3/tag"
1515

16-
"github.com/errata-ai/vale/v3/internal/glob"
1716
"github.com/errata-ai/vale/v3/internal/nlp"
1817
"github.com/errata-ai/vale/v3/internal/system"
1918
)
@@ -152,24 +151,30 @@ func NewFile(src string, config *Config) (*File, error) {
152151
}
153152

154153
lang := "en"
155-
for syntax, code := range config.FormatToLang {
156-
sec, err := glob.Compile(syntax)
157-
if err != nil {
158-
return &File{}, err
159-
} else if sec.Match(path) {
160-
lang = code
161-
break
154+
if code, found := config.FormatToLang["*"]; found && code != "" {
155+
lang = code
156+
}
157+
for _, sec := range config.RuleKeys {
158+
if pat, found := config.SecToPat[sec]; found && pat.Match(path) {
159+
// Sections are visited in the order they were written, so a
160+
// later one wins -- for this file, and no other. See #965.
161+
if code, ok := config.FormatToLang[sec]; ok {
162+
lang = code
163+
}
162164
}
163165
}
164166

165167
transform := ""
166-
for sec, p := range config.Stylesheets {
167-
pat, err := glob.Compile(sec)
168-
if err != nil {
169-
return &File{}, NewE100(path, err)
170-
} else if pat.Match(path) {
171-
transform = p
172-
break
168+
if p, found := config.Stylesheets["*"]; found {
169+
transform = p
170+
}
171+
for _, sec := range config.RuleKeys {
172+
if pat, found := config.SecToPat[sec]; found && pat.Match(path) {
173+
// Sections are visited in the order they were written, so a
174+
// later one wins -- for this file, and no other. See #965.
175+
if p, ok := config.Stylesheets[sec]; ok {
176+
transform = p
177+
}
173178
}
174179
}
175180
content := Sanitize(string(fbytes))

‎internal/core/file_test.go‎

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package core
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/errata-ai/vale/v3/internal/glob"
9+
)
10+
11+
// TestNewFileSectionOrderWins verifies that when multiple config sections
12+
// match the same file, NewFile resolves Lang and Transform deterministically
13+
// by taking the LAST matching section in the order it was written -- the
14+
// same "later one wins" semantics the rule/level resolution loop documents
15+
// for #965 -- rather than depending on Go's randomized map iteration order.
16+
func TestNewFileSectionOrderWins(t *testing.T) {
17+
sections := []struct {
18+
name string
19+
lang string
20+
xslt string
21+
}{
22+
{"*.md", "en", "one.xsl"},
23+
{"*.md", "fr", "two.xsl"},
24+
{"*.md", "de", "three.xsl"},
25+
{"*.md", "ja", "four.xsl"},
26+
{"*.md", "es", "five.xsl"},
27+
{"*.md", "it", "six.xsl"},
28+
}
29+
30+
cfg, err := NewConfig(&CLIFlags{})
31+
if err != nil {
32+
t.Fatal(err)
33+
}
34+
35+
for i, s := range sections {
36+
// Reuse the same glob pattern across sections, but track each
37+
// occurrence under its own key so every one can carry its own
38+
// Lang/Transform value -- mirroring how repeated real config
39+
// sections (e.g. multiple `[*.md]` blocks) are keyed internally.
40+
key := s.name
41+
if i > 0 {
42+
key = s.name + string(rune('a'+i))
43+
}
44+
45+
pat, cerr := glob.Compile(s.name)
46+
if cerr != nil {
47+
t.Fatal(cerr)
48+
}
49+
50+
cfg.SecToPat[key] = pat
51+
cfg.RuleKeys = append(cfg.RuleKeys, key)
52+
cfg.FormatToLang[key] = s.lang
53+
cfg.Stylesheets[key] = s.xslt
54+
cfg.SChecks[key] = map[string]bool{}
55+
cfg.SLevels[key] = map[string]string{}
56+
}
57+
58+
wantLang := sections[len(sections)-1].lang
59+
wantTransform := sections[len(sections)-1].xslt
60+
61+
docPath := filepath.Join(t.TempDir(), "doc.md")
62+
if werr := os.WriteFile(docPath, []byte("# Title\n"), 0600); werr != nil {
63+
t.Fatal(werr)
64+
}
65+
66+
const iterations = 50
67+
for i := 0; i < iterations; i++ {
68+
f, ferr := NewFile(docPath, cfg)
69+
if ferr != nil {
70+
t.Fatal(ferr)
71+
}
72+
73+
if f.NLP.Lang != wantLang {
74+
t.Fatalf("iteration %d: expected Lang %q (the last written matching section), got %q",
75+
i, wantLang, f.NLP.Lang)
76+
}
77+
78+
if f.Transform != wantTransform {
79+
t.Fatalf("iteration %d: expected Transform %q (the last written matching section), got %q",
80+
i, wantTransform, f.Transform)
81+
}
82+
}
83+
}
84+
85+
// TestNewFileGlobalLangFallback verifies that the `[*] Lang = ...` global
86+
// default still applies when no format-specific section matches the file.
87+
func TestNewFileGlobalLangFallback(t *testing.T) {
88+
cfg, err := NewConfig(&CLIFlags{})
89+
if err != nil {
90+
t.Fatal(err)
91+
}
92+
93+
cfg.FormatToLang["*"] = "ja"
94+
95+
docPath := filepath.Join(t.TempDir(), "doc.txt")
96+
if werr := os.WriteFile(docPath, []byte("hello\n"), 0600); werr != nil {
97+
t.Fatal(werr)
98+
}
99+
100+
f, err := NewFile(docPath, cfg)
101+
if err != nil {
102+
t.Fatal(err)
103+
}
104+
105+
if f.NLP.Lang != "ja" {
106+
t.Fatalf("expected global Lang fallback %q, got %q", "ja", f.NLP.Lang)
107+
}
108+
}

0 commit comments

Comments
 (0)