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

     1  package core
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"path/filepath"
     7  	"strings"
     8  
     9  	"github.com/errata-ai/ini"
    10  	"github.com/karrick/godirwalk"
    11  
    12  	"github.com/errata-ai/vale/v3/internal/glob"
    13  )
    14  
    15  func determinePath(configPath string, keyPath string) string {
    16  	// expand tilde at this point as this is where user-provided paths are provided
    17  	keyPath = normalizePath(keyPath)
    18  	if !IsDir(configPath) {
    19  		configPath = filepath.Dir(configPath)
    20  	}
    21  	sep := string(filepath.Separator)
    22  	abs, _ := filepath.Abs(keyPath)
    23  	rel := strings.TrimRight(keyPath, sep)
    24  	if abs != rel || !strings.Contains(keyPath, sep) {
    25  		// The path was relative
    26  		return filepath.Join(configPath, keyPath)
    27  	}
    28  	return abs
    29  }
    30  
    31  func mergeValues(shadows []string) []string {
    32  	values := []string{}
    33  	for _, v := range shadows {
    34  		entry := strings.TrimSpace(v)
    35  		if entry != "" && !StringInSlice(entry, values) {
    36  			values = append(values, entry)
    37  		}
    38  	}
    39  	return values
    40  }
    41  
    42  func loadVocab(root string, cfg *Config) error {
    43  	target := ""
    44  	for _, p := range cfg.SearchPaths() {
    45  		opt := filepath.Join(p, VocabDir, root)
    46  		if IsDir(opt) {
    47  			target = opt
    48  			break
    49  		}
    50  	}
    51  
    52  	if target == "" {
    53  		return NewE100("vocab", fmt.Errorf(
    54  			"'%s/%s' directory does not exist", VocabDir, root))
    55  	}
    56  
    57  	err := godirwalk.Walk(target, &godirwalk.Options{
    58  		Callback: func(fp string, de *godirwalk.Dirent) error {
    59  			name := de.Name()
    60  			if name == "accept.txt" {
    61  				return cfg.AddWordListFile(fp, true)
    62  			} else if name == "reject.txt" {
    63  				return cfg.AddWordListFile(fp, false)
    64  			}
    65  			return nil
    66  		},
    67  		Unsorted:            true,
    68  		AllowNonDirectory:   true,
    69  		FollowSymbolicLinks: true})
    70  
    71  	return err
    72  }
    73  
    74  func validateLevel(key, val string, cfg *Config) bool {
    75  	options := []string{"YES", "suggestion", "warning", "error"}
    76  	if val == "NO" || !StringInSlice(val, options) {
    77  		return false
    78  	} else if val != "YES" {
    79  		cfg.RuleToLevel[key] = val
    80  	}
    81  	return true
    82  }
    83  
    84  var syntaxOpts = map[string]func(string, *ini.Section, *Config) error{
    85  	"BasedOnStyles": func(lbl string, sec *ini.Section, cfg *Config) error {
    86  		pat, err := glob.Compile(lbl)
    87  		if err != nil {
    88  			return NewE201FromTarget(
    89  				fmt.Sprintf("The glob pattern '%s' could not be compiled.", lbl),
    90  				lbl,
    91  				cfg.Flags.Path)
    92  		} else if _, found := cfg.SecToPat[lbl]; !found {
    93  			cfg.SecToPat[lbl] = pat
    94  		}
    95  		sStyles := mergeValues(sec.Key("BasedOnStyles").StringsWithShadows(","))
    96  
    97  		cfg.Styles = append(cfg.Styles, sStyles...)
    98  		cfg.StyleKeys = append(cfg.StyleKeys, lbl)
    99  		cfg.SBaseStyles[lbl] = sStyles
   100  
   101  		return nil
   102  	},
   103  	"IgnorePatterns": func(label string, sec *ini.Section, cfg *Config) error { //nolint:unparam
   104  		cfg.BlockIgnores[label] = sec.Key("IgnorePatterns").Strings(",")
   105  		return nil
   106  	},
   107  	"BlockIgnores": func(label string, sec *ini.Section, cfg *Config) error { //nolint:unparam
   108  		cfg.BlockIgnores[label] = mergeValues(sec.Key("BlockIgnores").StringsWithShadows(","))
   109  		return nil
   110  	},
   111  	"TokenIgnores": func(label string, sec *ini.Section, cfg *Config) error { //nolint:unparam
   112  		cfg.TokenIgnores[label] = mergeValues(sec.Key("TokenIgnores").StringsWithShadows(","))
   113  		return nil
   114  	},
   115  	"Transform": func(label string, sec *ini.Section, cfg *Config) error { //nolint:unparam
   116  		candidate := sec.Key("Transform").String()
   117  		cfg.Stylesheets[label] = determinePath(cfg.Flags.Path, candidate)
   118  		return nil
   119  
   120  	},
   121  	"Lang": func(label string, sec *ini.Section, cfg *Config) error { //nolint:unparam
   122  		cfg.FormatToLang[label] = sec.Key("Lang").String()
   123  		return nil
   124  	},
   125  }
   126  
   127  var globalOpts = map[string]func(*ini.Section, *Config){
   128  	"BasedOnStyles": func(sec *ini.Section, cfg *Config) {
   129  		cfg.GBaseStyles = mergeValues(sec.Key("BasedOnStyles").StringsWithShadows(","))
   130  		cfg.Styles = append(cfg.Styles, cfg.GBaseStyles...)
   131  	},
   132  	"IgnorePatterns": func(sec *ini.Section, cfg *Config) {
   133  		cfg.BlockIgnores["*"] = sec.Key("IgnorePatterns").Strings(",")
   134  	},
   135  	"BlockIgnores": func(sec *ini.Section, cfg *Config) {
   136  		cfg.BlockIgnores["*"] = mergeValues(sec.Key("BlockIgnores").StringsWithShadows(","))
   137  	},
   138  	"TokenIgnores": func(sec *ini.Section, cfg *Config) {
   139  		cfg.TokenIgnores["*"] = mergeValues(sec.Key("TokenIgnores").StringsWithShadows(","))
   140  	},
   141  	"Lang": func(sec *ini.Section, cfg *Config) {
   142  		cfg.FormatToLang["*"] = sec.Key("Lang").String()
   143  	},
   144  }
   145  
   146  var coreOpts = map[string]func(*ini.Section, *Config) error{
   147  	"StylesPath": func(sec *ini.Section, cfg *Config) error {
   148  		// NOTE: The order of these paths is important. They represent the load
   149  		// order of the configuration files -- not `cfg.Paths`.
   150  		paths := sec.Key("StylesPath").ValueWithShadows()
   151  		files := cfg.ConfigFiles
   152  		if cfg.Flags.Local && len(files) == 2 {
   153  			// This represents the case where we have a default `.vale.ini`
   154  			// file and a local `.vale.ini` file.
   155  			//
   156  			// In such a case, there are three options: (1) both files define a
   157  			// `StylesPath`, (2) only one file defines a `StylesPath`, or (3)
   158  			// neither file defines a `StylesPath`.
   159  			basePath := determinePath(files[0], filepath.FromSlash(paths[0]))
   160  			mockPath := determinePath(files[1], filepath.FromSlash(paths[0]))
   161  			// ^ This case handles the situation where both configs define the
   162  			// same StylesPath (e.g., `StylesPath = styles`).
   163  			if len(paths) == 2 {
   164  				basePath = determinePath(files[0], filepath.FromSlash(paths[0]))
   165  				mockPath = determinePath(files[1], filepath.FromSlash(paths[1]))
   166  			}
   167  			cfg.AddStylesPath(basePath)
   168  			cfg.AddStylesPath(mockPath)
   169  		} else if len(paths) > 0 {
   170  			// In this case, we have a local configuration file (no default)
   171  			// that defines a `StylesPath`.
   172  			candidate := filepath.FromSlash(paths[len(paths)-1])
   173  			path := determinePath(cfg.ConfigFile(), candidate)
   174  
   175  			cfg.AddStylesPath(path)
   176  			if !FileExists(path) {
   177  				return NewE201FromTarget(
   178  					fmt.Sprintf("The path '%s' does not exist.", path),
   179  					candidate,
   180  					cfg.Flags.Path)
   181  			}
   182  		}
   183  		return nil
   184  	},
   185  	"MinAlertLevel": func(sec *ini.Section, cfg *Config) error {
   186  		if !StringInSlice(cfg.Flags.AlertLevel, AlertLevels) {
   187  			level := sec.Key("MinAlertLevel").String() // .In("suggestion", AlertLevels)
   188  			if index, found := LevelToInt[level]; found {
   189  				cfg.MinAlertLevel = index
   190  			} else {
   191  				return NewE201FromTarget(
   192  					"MinAlertLevel must be 'suggestion', 'warning', or 'error'.",
   193  					level,
   194  					cfg.Flags.Path)
   195  			}
   196  		}
   197  		return nil
   198  	},
   199  	"IgnoredScopes": func(sec *ini.Section, cfg *Config) error { //nolint:unparam
   200  		cfg.IgnoredScopes = mergeValues(sec.Key("IgnoredScopes").StringsWithShadows(","))
   201  		return nil
   202  	},
   203  	"WordTemplate": func(sec *ini.Section, cfg *Config) error { //nolint:unparam
   204  		cfg.WordTemplate = sec.Key("WordTemplate").String()
   205  		return nil
   206  	},
   207  	"SkippedScopes": func(sec *ini.Section, cfg *Config) error { //nolint:unparam
   208  		cfg.SkippedScopes = mergeValues(sec.Key("SkippedScopes").StringsWithShadows(","))
   209  		return nil
   210  	},
   211  	"IgnoredClasses": func(sec *ini.Section, cfg *Config) error { //nolint:unparam
   212  		cfg.IgnoredClasses = mergeValues(sec.Key("IgnoredClasses").StringsWithShadows(","))
   213  		return nil
   214  	},
   215  	"Vocab": func(sec *ini.Section, cfg *Config) error {
   216  		cfg.Vocab = mergeValues(sec.Key("Vocab").StringsWithShadows(","))
   217  		for _, v := range cfg.Vocab {
   218  			if err := loadVocab(v, cfg); err != nil {
   219  				return err
   220  			}
   221  		}
   222  		return nil
   223  	},
   224  	"NLPEndpoint": func(sec *ini.Section, cfg *Config) error { //nolint:unparam
   225  		cfg.NLPEndpoint = sec.Key("NLPEndpoint").MustString("")
   226  		return nil
   227  	},
   228  }
   229  
   230  func shadowLoad(source interface{}, others ...interface{}) (*ini.File, error) {
   231  	return ini.LoadSources(ini.LoadOptions{
   232  		AllowShadows:             true,
   233  		SpaceBeforeInlineComment: true}, source, others...)
   234  }
   235  
   236  func processSources(cfg *Config, sources []string) (*ini.File, error) {
   237  	var err error
   238  
   239  	uCfg := ini.Empty(ini.LoadOptions{
   240  		AllowShadows:             true,
   241  		Loose:                    true,
   242  		SpaceBeforeInlineComment: true,
   243  	})
   244  
   245  	if len(sources) == 0 {
   246  		return uCfg, errors.New("no sources provided")
   247  	} else if len(sources) == 1 {
   248  		cfg.Flags.Path = sources[0]
   249  		return shadowLoad(cfg.Flags.Path)
   250  	}
   251  
   252  	t := sources[1:]
   253  	s := make([]interface{}, len(t))
   254  	for i, v := range t {
   255  		s[i] = v
   256  	}
   257  
   258  	uCfg, err = shadowLoad(sources[0], s...)
   259  	cfg.Flags.Path = sources[len(sources)-1]
   260  
   261  	return uCfg, err
   262  }
   263  
   264  func processConfig(uCfg *ini.File, cfg *Config, dry bool) (*ini.File, error) {
   265  	core := uCfg.Section("")
   266  	global := uCfg.Section("*")
   267  
   268  	formats := uCfg.Section("formats")
   269  	adoc := uCfg.Section("asciidoctor")
   270  
   271  	// Default settings
   272  	for _, k := range core.KeyStrings() {
   273  		if f, found := coreOpts[k]; found {
   274  			if err := f(core, cfg); err != nil && !dry {
   275  				return nil, err
   276  			}
   277  		} else if _, found = syntaxOpts[k]; found {
   278  			msg := fmt.Sprintf("'%s' is a syntax-specific option", k)
   279  			return nil, NewE201FromTarget(msg, k, cfg.RootINI)
   280  		}
   281  	}
   282  
   283  	// Format mappings
   284  	for _, k := range formats.KeyStrings() {
   285  		cfg.Formats[k] = formats.Key(k).String()
   286  	}
   287  
   288  	// Asciidoctor attributes
   289  	for _, k := range adoc.KeyStrings() {
   290  		cfg.Asciidoctor[k] = adoc.Key(k).String()
   291  	}
   292  
   293  	// Global settings
   294  	for _, k := range global.KeyStrings() {
   295  		if f, found := globalOpts[k]; found {
   296  			f(global, cfg)
   297  		} else if _, found = syntaxOpts[k]; found {
   298  			msg := fmt.Sprintf("'%s' is a syntax-specific option", k)
   299  			return nil, NewE201FromTarget(msg, k, cfg.RootINI)
   300  		} else {
   301  			cfg.GChecks[k] = validateLevel(k, global.Key(k).String(), cfg)
   302  			cfg.Checks = append(cfg.Checks, k)
   303  		}
   304  	}
   305  
   306  	// Syntax-specific settings
   307  	for _, sec := range uCfg.SectionStrings() {
   308  		if StringInSlice(sec, []string{"*", "DEFAULT", "formats", "asciidoctor"}) {
   309  			continue
   310  		}
   311  
   312  		pat, err := glob.Compile(sec)
   313  		if err != nil {
   314  			return nil, err
   315  		}
   316  		cfg.SecToPat[sec] = pat
   317  
   318  		syntaxMap := make(map[string]bool)
   319  		for _, k := range uCfg.Section(sec).KeyStrings() {
   320  			if f, found := syntaxOpts[k]; found {
   321  				if err = f(sec, uCfg.Section(sec), cfg); err != nil && !dry {
   322  					return nil, err
   323  				}
   324  			} else {
   325  				syntaxMap[k] = validateLevel(k, uCfg.Section(sec).Key(k).String(), cfg)
   326  				cfg.Checks = append(cfg.Checks, k)
   327  			}
   328  		}
   329  		cfg.RuleKeys = append(cfg.RuleKeys, sec)
   330  		cfg.SChecks[sec] = syntaxMap
   331  	}
   332  
   333  	return uCfg, nil
   334  }