Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 78 additions & 30 deletions internal/core/ini.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"

Expand All @@ -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 {
Expand Down Expand Up @@ -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
},
Expand Down Expand Up @@ -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) {
Expand Down
74 changes: 27 additions & 47 deletions internal/core/source.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -115,71 +129,37 @@ 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)
}

if StringInSlice(cfg.Flags.AlertLevel, AlertLevels) {
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)
}

Expand Down
Loading