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 }