diff --git a/internal/core/ini.go b/internal/core/ini.go index ad8630f3..38994acf 100644 --- a/internal/core/ini.go +++ b/internal/core/ini.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "io/fs" + "os" "path/filepath" "strings" @@ -13,6 +14,10 @@ import ( "github.com/errata-ai/vale/v3/internal/system" ) +var pathKeys = []string{ + "StylesPath", +} + var coreError = "'%s' is a core option; it should be defined above any syntax-specific options (`[...]`)." func mergeValues(shadows []string) []string { @@ -161,40 +166,15 @@ var globalOpts = map[string]func(*ini.Section, *Config){ var coreOpts = map[string]func(*ini.Section, *Config) error{ "StylesPath": func(sec *ini.Section, cfg *Config) error { - // NOTE: The order of these paths is important. They represent the load - // order of the configuration files -- not `cfg.Paths`. paths := sec.Key("StylesPath").ValueWithShadows() - files := cfg.ConfigFiles - if cfg.Flags.Local && len(files) == 2 { - // This represents the case where we have a default `.vale.ini` - // file and a local `.vale.ini` file. - // - // In such a case, there are three options: (1) both files define a - // `StylesPath`, (2) only one file defines a `StylesPath`, or (3) - // neither file defines a `StylesPath`. - basePath := system.DeterminePath(files[0], filepath.FromSlash(paths[0])) - mockPath := system.DeterminePath(files[1], filepath.FromSlash(paths[0])) - // ^ This case handles the situation where both configs define the - // same StylesPath (e.g., `StylesPath = styles`). - if len(paths) == 2 { - basePath = system.DeterminePath(files[0], filepath.FromSlash(paths[0])) - mockPath = system.DeterminePath(files[1], filepath.FromSlash(paths[1])) - } - cfg.AddStylesPath(basePath) - cfg.AddStylesPath(mockPath) - } else if len(paths) > 0 { - // In this case, we have a local configuration file (no default) - // that defines a `StylesPath`. - candidate := filepath.FromSlash(paths[len(paths)-1]) - path := system.DeterminePath(cfg.ConfigFile(), candidate) - - cfg.AddStylesPath(path) + for _, path := range paths { if !system.FileExists(path) { return NewE201FromTarget( fmt.Sprintf("The path '%s' does not exist.", path), - candidate, + path, cfg.Flags.Path) } + cfg.AddStylesPath(path) } return nil }, @@ -255,10 +235,78 @@ var coreOpts = map[string]func(*ini.Section, *Config) error{ }, } +func expandPaths(file *ini.File, source interface{}) { + var path string + + switch s := source.(type) { + case string: + abs, _ := filepath.Abs(s) + path = filepath.Dir(abs) + default: + path, _ = os.Getwd() + } + + for _, section := range file.Sections() { + for _, key := range section.Keys() { + if StringInSlice(key.Name(), pathKeys) { + value := key.Value() + if !filepath.IsAbs(value) { + key.SetValue(filepath.Join(path, value)) + } + } + } + } +} + +func shadowMerge(primary *ini.File, secondary *ini.File) { + for _, secondarySection := range secondary.Sections() { + sectionName := secondarySection.Name() + + primarySection, _ := primary.GetSection(sectionName) + if primarySection == nil { + primarySection, _ = primary.NewSection(sectionName) + } + + for _, secondaryKey := range secondarySection.Keys() { + keyName := secondaryKey.Name() + keyValue := secondaryKey.Value() + + primaryKey, _ := primarySection.GetKey(keyName) + if primaryKey == nil { + primarySection.NewKey(keyName, keyValue) + } else { + primaryKey.AddShadow(keyValue) + } + } + } +} + func shadowLoad(source interface{}, others ...interface{}) (*ini.File, error) { - return ini.LoadSources(ini.LoadOptions{ + options := ini.LoadOptions{ AllowShadows: true, - SpaceBeforeInlineComment: true}, source, others...) + Loose: true, + SpaceBeforeInlineComment: true, + } + + primary, err := ini.LoadSources(options, source) + if err != nil { + return nil, err + } + expandPaths(primary, source) + + for _, other := range others { + var shadow *ini.File + + shadow, err = ini.LoadSources(options, other) + if err != nil { + return nil, err + } + + expandPaths(shadow, other) + shadowMerge(primary, shadow) + } + + return primary, nil } func processSources(cfg *Config, sources []string) (*ini.File, error) { diff --git a/internal/core/source.go b/internal/core/source.go index f3ad48c1..3e3ae062 100644 --- a/internal/core/source.go +++ b/internal/core/source.go @@ -100,11 +100,25 @@ func loadStdin(src string, cfg *Config, dry bool) (*ini.File, error) { } func loadINI(cfg *Config, dry bool) (*ini.File, error) { - uCfg := ini.Empty(ini.LoadOptions{ - AllowShadows: true, - Loose: true, - SpaceBeforeInlineComment: true, - }) + var sources []string + var uCfg *ini.File + + // NOTE: In v3.0, we now use the user's config directory as the default + // location. + // + // This is different from the other config-defining options (`--config`, + // `VALE_CONFIG_PATH`, etc.) in that it's loaded in addition to, rather + // than instead of, any other configuration sources. + // + // In other words, this config file is *always* loaded and is read after + // any other sources to allow for project-agnostic customization. + defaultCfg, _ := DefaultConfig() + + if system.FileExists(defaultCfg) && !cfg.Flags.IgnoreGlobal && !dry { + sources = append(sources, defaultCfg) + cfg.Flags.Local = true + cfg.AddConfigFile(defaultCfg) + } base, err := loadConfig(configNames) if err != nil { @@ -115,41 +129,22 @@ func loadINI(cfg *Config, dry bool) (*ini.File, error) { if cfg.Flags.Sources != "" { // NOTE: This case shouldn't be accessible from the CLI, but it can // still be triggered by packages that include config files. - var sources []string for _, source := range strings.Split(cfg.Flags.Sources, ",") { abs, _ := filepath.Abs(source) sources = append(sources, abs) } - - // We have multiple sources -- e.g., local config + remote package(s). - // - // See fixtures/config.feature#451 for an explanation of how this has - // changed since Vale Server was deprecated. - uCfg, err = processSources(cfg, sources) - if err != nil { - return nil, NewE100("config pipeline failed", err) - } } else if cfg.Flags.Path != "" { // We've been given a value through `--config`. - err = uCfg.Append(cfg.Flags.Path) - if err != nil { - return nil, NewE100("invalid --config", err) - } + sources = append(sources, cfg.Flags.Path) cfg.AddConfigFile(cfg.Flags.Path) } else if fromEnv, hasEnv := os.LookupEnv("VALE_CONFIG_PATH"); hasEnv { // We've been given a value through `VALE_CONFIG_PATH`. - err = uCfg.Append(fromEnv) - if err != nil { - return nil, NewE100("invalid VALE_CONFIG_PATH", err) - } + sources = append(sources, fromEnv) cfg.AddConfigFile(fromEnv) } else if base != "" { // We're using a config file found using a local search process. - err = uCfg.Append(base) - if err != nil { - return nil, NewE100(".vale.ini not found", err) - } + sources = append(sources, base) cfg.AddConfigFile(base) } @@ -157,29 +152,14 @@ func loadINI(cfg *Config, dry bool) (*ini.File, error) { cfg.MinAlertLevel = LevelToInt[cfg.Flags.AlertLevel] } - // NOTE: In v3.0, we now use the user's config directory as the default - // location. - // - // This is different from the other config-defining options (`--config`, - // `VALE_CONFIG_PATH`, etc.) in that it's loaded in addition to, rather - // than instead of, any other configuration sources. - // - // In other words, this config file is *always* loaded and is read after - // any other sources to allow for project-agnostic customization. - defaultCfg, _ := DefaultConfig() - - if system.FileExists(defaultCfg) && !cfg.Flags.IgnoreGlobal && !dry { - err = uCfg.Append(defaultCfg) - if err != nil { - return nil, NewE100("default/ini", err) - } - cfg.Flags.Local = true - cfg.AddConfigFile(defaultCfg) - } else if base == "" && len(cfg.ConfigFiles) == 0 && !dry { + if base == "" && len(cfg.ConfigFiles) == 0 && !dry { return nil, NewE100(".vale.ini not found", errors.New("no config file found")) } - uCfg.BlockMode = false + uCfg, err = processSources(cfg, sources) + if err != nil { + return nil, NewE100("config pipeline failed", err) + } return processConfig(uCfg, cfg, dry) }