github.com/errata-ai/vale/v3@v3.4.2/internal/core/source.go (about)

     1  package core
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"os"
     7  	"path"
     8  	"path/filepath"
     9  	"strings"
    10  
    11  	"github.com/errata-ai/ini"
    12  )
    13  
    14  // ConfigSrc is a source of configuration values.
    15  //
    16  // This could be a local file, a string, or a remote URL.
    17  type ConfigSrc int
    18  
    19  const (
    20  	FileSrc ConfigSrc = iota
    21  	StringSrc
    22  )
    23  
    24  // ReadPipeline loads Vale's configuration according to the local search
    25  // process.
    26  //
    27  // A `dry` run means that we can't expect the `StylesPath` to fully formed yet.
    28  // For example, some assets may not have been downloaded yet via the `sync`
    29  // command.
    30  func ReadPipeline(flags *CLIFlags, dry bool) (*Config, error) {
    31  	config, err := NewConfig(flags)
    32  	if err != nil {
    33  		return config, err
    34  	} else if err = validateFlags(config); err != nil {
    35  		return config, err
    36  	}
    37  
    38  	_, err = FromFile(config, dry)
    39  	if err != nil {
    40  		return config, err
    41  	}
    42  
    43  	sources, err := pipeConfig(config)
    44  	if err != nil {
    45  		return config, err
    46  	}
    47  
    48  	if len(sources) > 0 {
    49  		config.Flags.Sources = strings.Join(sources, ",")
    50  
    51  		_, err = FromFile(config, true)
    52  		if err != nil {
    53  			return config, err
    54  		}
    55  	}
    56  
    57  	return config, nil
    58  }
    59  
    60  // from updates an existing configuration with values From a user-provided
    61  // source.
    62  func from(provider ConfigSrc, src string, cfg *Config, dry bool) (*ini.File, error) {
    63  	switch provider {
    64  	case FileSrc:
    65  		return loadINI(cfg, dry)
    66  	case StringSrc:
    67  		return loadStdin(src, cfg, dry)
    68  	default:
    69  		return nil, NewE100(
    70  			"source/From", fmt.Errorf("unknown provider '%v'", provider))
    71  	}
    72  }
    73  
    74  // FromFile loads an INI configuration from a file.
    75  func FromFile(cfg *Config, dry bool) (*ini.File, error) {
    76  	return from(FileSrc, "", cfg, dry)
    77  }
    78  
    79  // FromString loads an INI configuration from a string.
    80  func FromString(src string, cfg *Config, dry bool) (*ini.File, error) {
    81  	return from(StringSrc, src, cfg, dry)
    82  }
    83  
    84  func validateFlags(cfg *Config) error {
    85  	if cfg.Flags.Path != "" && !FileExists(cfg.Flags.Path) {
    86  		return NewE100(
    87  			"--config",
    88  			fmt.Errorf("path '%s' does not exist", cfg.Flags.Path))
    89  	}
    90  	return nil
    91  }
    92  
    93  func loadStdin(src string, cfg *Config, dry bool) (*ini.File, error) {
    94  	uCfg, err := shadowLoad([]byte(src))
    95  	if err != nil {
    96  		return nil, NewE100("loadStdin", err)
    97  	}
    98  	return processConfig(uCfg, cfg, dry)
    99  }
   100  
   101  func loadINI(cfg *Config, dry bool) (*ini.File, error) {
   102  	uCfg := ini.Empty(ini.LoadOptions{
   103  		AllowShadows:             true,
   104  		Loose:                    true,
   105  		SpaceBeforeInlineComment: true,
   106  	})
   107  
   108  	base, err := loadConfig(configNames)
   109  	if err != nil {
   110  		return nil, NewE100("loadINI/homedir", err)
   111  	}
   112  	cfg.RootINI = base
   113  
   114  	if cfg.Flags.Sources != "" {
   115  		// NOTE: This case shouldn't be accessible from the CLI, but it can
   116  		// still be triggered by packages that include config files.
   117  		var sources []string
   118  
   119  		for _, source := range strings.Split(cfg.Flags.Sources, ",") {
   120  			abs, _ := filepath.Abs(source)
   121  			sources = append(sources, abs)
   122  		}
   123  
   124  		// We have multiple sources -- e.g., local config + remote package(s).
   125  		//
   126  		// See fixtures/config.feature#451 for an explanation of how this has
   127  		// changed since Vale Server was deprecated.
   128  		uCfg, err = processSources(cfg, sources)
   129  		if err != nil {
   130  			return nil, NewE100("config pipeline failed", err)
   131  		}
   132  	} else if cfg.Flags.Path != "" {
   133  		// We've been given a value through `--config`.
   134  		err = uCfg.Append(cfg.Flags.Path)
   135  		if err != nil {
   136  			return nil, NewE100("invalid --config", err)
   137  		}
   138  		cfg.AddConfigFile(cfg.Flags.Path)
   139  	} else if fromEnv, hasEnv := os.LookupEnv("VALE_CONFIG_PATH"); hasEnv {
   140  		// We've been given a value through `VALE_CONFIG_PATH`.
   141  		err = uCfg.Append(fromEnv)
   142  		if err != nil {
   143  			return nil, NewE100("invalid VALE_CONFIG_PATH", err)
   144  		}
   145  		cfg.AddConfigFile(fromEnv)
   146  	} else if base != "" {
   147  		// We're using a config file found using a local search process.
   148  		err = uCfg.Append(base)
   149  		if err != nil {
   150  			return nil, NewE100(".vale.ini not found", err)
   151  		}
   152  		cfg.AddConfigFile(base)
   153  	}
   154  
   155  	if StringInSlice(cfg.Flags.AlertLevel, AlertLevels) {
   156  		cfg.MinAlertLevel = LevelToInt[cfg.Flags.AlertLevel]
   157  	}
   158  
   159  	// NOTE: In v3.0, we now use the user's config directory as the default
   160  	// location.
   161  	//
   162  	// This is different from the other config-defining options (`--config`,
   163  	// `VALE_CONFIG_PATH`, etc.) in that it's loaded in addition to, rather
   164  	// than instead of, any other configuration sources.
   165  	//
   166  	// In other words, this config file is *always* loaded and is read after
   167  	// any other sources to allow for project-agnostic customization.
   168  	defaultCfg, _ := DefaultConfig()
   169  
   170  	if FileExists(defaultCfg) && !cfg.Flags.IgnoreGlobal && !dry {
   171  		err = uCfg.Append(defaultCfg)
   172  		if err != nil {
   173  			return nil, NewE100("default/ini", err)
   174  		}
   175  		cfg.Flags.Local = true
   176  		cfg.AddConfigFile(defaultCfg)
   177  	} else if base == "" && len(cfg.ConfigFiles) == 0 && !dry {
   178  		return nil, NewE100(".vale.ini not found", errors.New("no config file found"))
   179  	}
   180  
   181  	uCfg.BlockMode = false
   182  	return processConfig(uCfg, cfg, dry)
   183  }
   184  
   185  // loadConfig loads the .vale file. It checks the ancestors of the current
   186  // directory, stopping on the first occurrence of a .vale or _vale file. If
   187  // no ancestor of the current directory has a configuration file, it checks
   188  // the user's home directory for a configuration file.
   189  func loadConfig(names []string) (string, error) {
   190  	var parent string
   191  
   192  	cwd, err := os.Getwd()
   193  	if err != nil {
   194  		return "", err
   195  	}
   196  
   197  	for {
   198  		parent = filepath.Dir(cwd)
   199  
   200  		for _, name := range names {
   201  			loc := path.Join(cwd, name)
   202  			if FileExists(loc) && !IsDir(loc) {
   203  				return loc, nil
   204  			}
   205  		}
   206  
   207  		if cwd == parent {
   208  			break
   209  		}
   210  		cwd = parent
   211  	}
   212  
   213  	homeDir, _ := os.UserHomeDir()
   214  	if homeDir == "" {
   215  		return "", nil
   216  	}
   217  
   218  	for _, name := range names {
   219  		loc := path.Join(homeDir, name)
   220  		if FileExists(loc) && !IsDir(loc) {
   221  			return loc, nil
   222  		}
   223  	}
   224  
   225  	return "", nil
   226  }