Skip to content

Commit cdf6f9e

Browse files
authored
Bugfix/path expansion (#1096)
1 parent 3b9c8bf commit cdf6f9e

2 files changed

Lines changed: 105 additions & 77 deletions

File tree

internal/core/ini.go

Lines changed: 78 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"errors"
55
"fmt"
66
"io/fs"
7+
"os"
78
"path/filepath"
89
"strings"
910

@@ -13,6 +14,10 @@ import (
1314
"github.com/errata-ai/vale/v3/internal/system"
1415
)
1516

17+
var pathKeys = []string{
18+
"StylesPath",
19+
}
20+
1621
var coreError = "'%s' is a core option; it should be defined above any syntax-specific options (`[...]`)."
1722

1823
func mergeValues(shadows []string) []string {
@@ -161,40 +166,15 @@ var globalOpts = map[string]func(*ini.Section, *Config){
161166

162167
var coreOpts = map[string]func(*ini.Section, *Config) error{
163168
"StylesPath": func(sec *ini.Section, cfg *Config) error {
164-
// NOTE: The order of these paths is important. They represent the load
165-
// order of the configuration files -- not `cfg.Paths`.
166169
paths := sec.Key("StylesPath").ValueWithShadows()
167-
files := cfg.ConfigFiles
168-
if cfg.Flags.Local && len(files) == 2 {
169-
// This represents the case where we have a default `.vale.ini`
170-
// file and a local `.vale.ini` file.
171-
//
172-
// In such a case, there are three options: (1) both files define a
173-
// `StylesPath`, (2) only one file defines a `StylesPath`, or (3)
174-
// neither file defines a `StylesPath`.
175-
basePath := system.DeterminePath(files[0], filepath.FromSlash(paths[0]))
176-
mockPath := system.DeterminePath(files[1], filepath.FromSlash(paths[0]))
177-
// ^ This case handles the situation where both configs define the
178-
// same StylesPath (e.g., `StylesPath = styles`).
179-
if len(paths) == 2 {
180-
basePath = system.DeterminePath(files[0], filepath.FromSlash(paths[0]))
181-
mockPath = system.DeterminePath(files[1], filepath.FromSlash(paths[1]))
182-
}
183-
cfg.AddStylesPath(basePath)
184-
cfg.AddStylesPath(mockPath)
185-
} else if len(paths) > 0 {
186-
// In this case, we have a local configuration file (no default)
187-
// that defines a `StylesPath`.
188-
candidate := filepath.FromSlash(paths[len(paths)-1])
189-
path := system.DeterminePath(cfg.ConfigFile(), candidate)
190-
191-
cfg.AddStylesPath(path)
170+
for _, path := range paths {
192171
if !system.FileExists(path) {
193172
return NewE201FromTarget(
194173
fmt.Sprintf("The path '%s' does not exist.", path),
195-
candidate,
174+
path,
196175
cfg.Flags.Path)
197176
}
177+
cfg.AddStylesPath(path)
198178
}
199179
return nil
200180
},
@@ -255,10 +235,78 @@ var coreOpts = map[string]func(*ini.Section, *Config) error{
255235
},
256236
}
257237

238+
func expandPaths(file *ini.File, source interface{}) {
239+
var path string
240+
241+
switch s := source.(type) {
242+
case string:
243+
abs, _ := filepath.Abs(s)
244+
path = filepath.Dir(abs)
245+
default:
246+
path, _ = os.Getwd()
247+
}
248+
249+
for _, section := range file.Sections() {
250+
for _, key := range section.Keys() {
251+
if StringInSlice(key.Name(), pathKeys) {
252+
value := key.Value()
253+
if !filepath.IsAbs(value) {
254+
key.SetValue(filepath.Join(path, value))
255+
}
256+
}
257+
}
258+
}
259+
}
260+
261+
func shadowMerge(primary *ini.File, secondary *ini.File) {
262+
for _, secondarySection := range secondary.Sections() {
263+
sectionName := secondarySection.Name()
264+
265+
primarySection, _ := primary.GetSection(sectionName)
266+
if primarySection == nil {
267+
primarySection, _ = primary.NewSection(sectionName)
268+
}
269+
270+
for _, secondaryKey := range secondarySection.Keys() {
271+
keyName := secondaryKey.Name()
272+
keyValue := secondaryKey.Value()
273+
274+
primaryKey, _ := primarySection.GetKey(keyName)
275+
if primaryKey == nil {
276+
primarySection.NewKey(keyName, keyValue)
277+
} else {
278+
primaryKey.AddShadow(keyValue)
279+
}
280+
}
281+
}
282+
}
283+
258284
func shadowLoad(source interface{}, others ...interface{}) (*ini.File, error) {
259-
return ini.LoadSources(ini.LoadOptions{
285+
options := ini.LoadOptions{
260286
AllowShadows: true,
261-
SpaceBeforeInlineComment: true}, source, others...)
287+
Loose: true,
288+
SpaceBeforeInlineComment: true,
289+
}
290+
291+
primary, err := ini.LoadSources(options, source)
292+
if err != nil {
293+
return nil, err
294+
}
295+
expandPaths(primary, source)
296+
297+
for _, other := range others {
298+
var shadow *ini.File
299+
300+
shadow, err = ini.LoadSources(options, other)
301+
if err != nil {
302+
return nil, err
303+
}
304+
305+
expandPaths(shadow, other)
306+
shadowMerge(primary, shadow)
307+
}
308+
309+
return primary, nil
262310
}
263311

264312
func processSources(cfg *Config, sources []string) (*ini.File, error) {

internal/core/source.go

Lines changed: 27 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,25 @@ func loadStdin(src string, cfg *Config, dry bool) (*ini.File, error) {
100100
}
101101

102102
func loadINI(cfg *Config, dry bool) (*ini.File, error) {
103-
uCfg := ini.Empty(ini.LoadOptions{
104-
AllowShadows: true,
105-
Loose: true,
106-
SpaceBeforeInlineComment: true,
107-
})
103+
var sources []string
104+
var uCfg *ini.File
105+
106+
// NOTE: In v3.0, we now use the user's config directory as the default
107+
// location.
108+
//
109+
// This is different from the other config-defining options (`--config`,
110+
// `VALE_CONFIG_PATH`, etc.) in that it's loaded in addition to, rather
111+
// than instead of, any other configuration sources.
112+
//
113+
// In other words, this config file is *always* loaded and is read after
114+
// any other sources to allow for project-agnostic customization.
115+
defaultCfg, _ := DefaultConfig()
116+
117+
if system.FileExists(defaultCfg) && !cfg.Flags.IgnoreGlobal && !dry {
118+
sources = append(sources, defaultCfg)
119+
cfg.Flags.Local = true
120+
cfg.AddConfigFile(defaultCfg)
121+
}
108122

109123
base, err := loadConfig(configNames)
110124
if err != nil {
@@ -115,71 +129,37 @@ func loadINI(cfg *Config, dry bool) (*ini.File, error) {
115129
if cfg.Flags.Sources != "" {
116130
// NOTE: This case shouldn't be accessible from the CLI, but it can
117131
// still be triggered by packages that include config files.
118-
var sources []string
119132

120133
for _, source := range strings.Split(cfg.Flags.Sources, ",") {
121134
abs, _ := filepath.Abs(source)
122135
sources = append(sources, abs)
123136
}
124-
125-
// We have multiple sources -- e.g., local config + remote package(s).
126-
//
127-
// See fixtures/config.feature#451 for an explanation of how this has
128-
// changed since Vale Server was deprecated.
129-
uCfg, err = processSources(cfg, sources)
130-
if err != nil {
131-
return nil, NewE100("config pipeline failed", err)
132-
}
133137
} else if cfg.Flags.Path != "" {
134138
// We've been given a value through `--config`.
135-
err = uCfg.Append(cfg.Flags.Path)
136-
if err != nil {
137-
return nil, NewE100("invalid --config", err)
138-
}
139+
sources = append(sources, cfg.Flags.Path)
139140
cfg.AddConfigFile(cfg.Flags.Path)
140141
} else if fromEnv, hasEnv := os.LookupEnv("VALE_CONFIG_PATH"); hasEnv {
141142
// We've been given a value through `VALE_CONFIG_PATH`.
142-
err = uCfg.Append(fromEnv)
143-
if err != nil {
144-
return nil, NewE100("invalid VALE_CONFIG_PATH", err)
145-
}
143+
sources = append(sources, fromEnv)
146144
cfg.AddConfigFile(fromEnv)
147145
} else if base != "" {
148146
// We're using a config file found using a local search process.
149-
err = uCfg.Append(base)
150-
if err != nil {
151-
return nil, NewE100(".vale.ini not found", err)
152-
}
147+
sources = append(sources, base)
153148
cfg.AddConfigFile(base)
154149
}
155150

156151
if StringInSlice(cfg.Flags.AlertLevel, AlertLevels) {
157152
cfg.MinAlertLevel = LevelToInt[cfg.Flags.AlertLevel]
158153
}
159154

160-
// NOTE: In v3.0, we now use the user's config directory as the default
161-
// location.
162-
//
163-
// This is different from the other config-defining options (`--config`,
164-
// `VALE_CONFIG_PATH`, etc.) in that it's loaded in addition to, rather
165-
// than instead of, any other configuration sources.
166-
//
167-
// In other words, this config file is *always* loaded and is read after
168-
// any other sources to allow for project-agnostic customization.
169-
defaultCfg, _ := DefaultConfig()
170-
171-
if system.FileExists(defaultCfg) && !cfg.Flags.IgnoreGlobal && !dry {
172-
err = uCfg.Append(defaultCfg)
173-
if err != nil {
174-
return nil, NewE100("default/ini", err)
175-
}
176-
cfg.Flags.Local = true
177-
cfg.AddConfigFile(defaultCfg)
178-
} else if base == "" && len(cfg.ConfigFiles) == 0 && !dry {
155+
if base == "" && len(cfg.ConfigFiles) == 0 && !dry {
179156
return nil, NewE100(".vale.ini not found", errors.New("no config file found"))
180157
}
181158

182-
uCfg.BlockMode = false
159+
uCfg, err = processSources(cfg, sources)
160+
if err != nil {
161+
return nil, NewE100("config pipeline failed", err)
162+
}
183163
return processConfig(uCfg, cfg, dry)
184164
}
185165

0 commit comments

Comments
 (0)