diff --git a/cmd/vale/api.go b/cmd/vale/api.go index 7e174ba4..be153bc0 100644 --- a/cmd/vale/api.go +++ b/cmd/vale/api.go @@ -10,6 +10,7 @@ import ( "github.com/spf13/pflag" "github.com/errata-ai/vale/v3/internal/core" + "github.com/errata-ai/vale/v3/internal/system" ) // Style represents an externally-hosted style. @@ -80,7 +81,7 @@ func fetch(src, dst string) error { } resp.Body.Close() - return unarchive(tmpfile.Name(), dst) + return system.Unarchive(tmpfile.Name(), dst) } func install(args []string, flags *core.CLIFlags) error { @@ -90,7 +91,7 @@ func install(args []string, flags *core.CLIFlags) error { } style := filepath.Join(cfg.StylesPath(), args[0]) - if core.IsDir(style) { + if system.IsDir(style) { os.RemoveAll(style) // Remove existing version } diff --git a/cmd/vale/command.go b/cmd/vale/command.go index cfde0a4d..eee9db64 100644 --- a/cmd/vale/command.go +++ b/cmd/vale/command.go @@ -13,6 +13,7 @@ import ( "github.com/errata-ai/vale/v3/internal/core" "github.com/errata-ai/vale/v3/internal/lint" "github.com/errata-ai/vale/v3/internal/nlp" + "github.com/errata-ai/vale/v3/internal/system" ) // TaggedWord is a word with an NLP context. @@ -65,7 +66,7 @@ func fix(args []string, flags *core.CLIFlags) error { } alert := args[0] - if core.FileExists(args[0]) { + if system.FileExists(args[0]) { b, err := os.ReadFile(args[0]) if err != nil { return err @@ -112,7 +113,7 @@ func sync(_ []string, flags *core.CLIFlags) error { } for idx, pkg := range pkgs { - name := fileNameWithoutExt(pkg) + name := system.FileNameWithoutExt(pkg) p.UpdateTitle("Syncing " + name) p.Increment() @@ -140,7 +141,7 @@ func printConfig(_ []string, flags *core.CLIFlags) error { func printMetrics(args []string, _ *core.CLIFlags) error { if len(args) != 1 { return core.NewE100("ls-metrics", errors.New("one argument expected")) - } else if !core.FileExists(args[0]) { + } else if !system.FileExists(args[0]) { return errors.New("file not found") } @@ -272,14 +273,14 @@ func printDirs(_ []string, flags *core.CLIFlags) error { styles, _ := core.DefaultStylesPath() stylesFound := pterm.FgGreen.Sprint("✓") - if !core.IsDir(styles) { + if !system.IsDir(styles) { stylesFound = pterm.FgRed.Sprint("✗") } cfg, _ := core.DefaultConfig() configFound := pterm.FgGreen.Sprint("✓") - if !core.FileExists(cfg) { + if !system.FileExists(cfg) { configFound = pterm.FgRed.Sprint("✗") } @@ -288,7 +289,7 @@ func printDirs(_ []string, flags *core.CLIFlags) error { nativeExe := filepath.Join(nativeDir, getExecName("vale-native")) nativeFound := pterm.FgGreen.Sprint("✓") - if !core.FileExists(native) { + if !system.FileExists(native) { nativeFound = pterm.FgRed.Sprint("✗") } @@ -313,7 +314,7 @@ func printDirs(_ []string, flags *core.CLIFlags) error { func transform(args []string, flags *core.CLIFlags) error { if len(args) != 1 { return core.NewE100("transform", errors.New("one argument expected")) - } else if !core.FileExists(args[0]) { + } else if !system.FileExists(args[0]) { return fmt.Errorf("file not found: %s", args[0]) } diff --git a/cmd/vale/custom.go b/cmd/vale/custom.go index d0ebb282..8d63542b 100644 --- a/cmd/vale/custom.go +++ b/cmd/vale/custom.go @@ -8,6 +8,7 @@ import ( "github.com/Masterminds/sprig/v3" "github.com/errata-ai/vale/v3/internal/core" + "github.com/errata-ai/vale/v3/internal/system" ) // ProcessedFile represents a file that Vale has linted. @@ -27,7 +28,7 @@ func PrintCustomAlerts(linted []*core.File, cfg *core.Config) (bool, error) { var alertCount int path := cfg.Flags.Output - if !core.FileExists(path) { + if !system.FileExists(path) { path = core.FindAsset(cfg, path) } diff --git a/cmd/vale/main.go b/cmd/vale/main.go index 0735f9fb..722d7f0a 100755 --- a/cmd/vale/main.go +++ b/cmd/vale/main.go @@ -9,6 +9,7 @@ import ( "github.com/errata-ai/vale/v3/internal/core" "github.com/errata-ai/vale/v3/internal/lint" + "github.com/errata-ai/vale/v3/internal/system" ) // version is set during the release build process. @@ -23,8 +24,8 @@ func stat() bool { } func looksLikeStdin(s string) int { - isDir := core.IsDir(s) - if !(core.FileExists(s) || isDir) && s != "" { + isDir := system.IsDir(s) + if !(system.FileExists(s) || isDir) && s != "" { return 1 } else if isDir { return 0 diff --git a/cmd/vale/native.go b/cmd/vale/native.go index 025bf3be..b524e133 100644 --- a/cmd/vale/native.go +++ b/cmd/vale/native.go @@ -7,13 +7,13 @@ import ( "os" "os/exec" "path/filepath" - "runtime" "strings" "github.com/adrg/xdg" "github.com/pterm/pterm" "github.com/errata-ai/vale/v3/internal/core" + "github.com/errata-ai/vale/v3/internal/system" ) const nativeHostName = "sh.vale.native" @@ -60,8 +60,9 @@ func getNativeConfig() (string, error) { if err != nil { return "", err } + name := system.Name() - switch runtime.GOOS { + switch name { case "windows": cfg, notFound := xdg.ConfigFile("vale/native/config.json") if notFound != nil { @@ -70,23 +71,23 @@ func getNativeConfig() (string, error) { return cfg, nil case "linux": path := filepath.Join(home, ".config/vale/native/config.json") - if err = mkdir(filepath.Dir(path)); err != nil { + if err = system.Mkdir(filepath.Dir(path)); err != nil { return "", err } return path, nil case "darwin": path := filepath.Join(home, "Library/Application Support/vale/native/config.json") - if err = mkdir(filepath.Dir(path)); err != nil { + if err = system.Mkdir(filepath.Dir(path)); err != nil { return "", err } return path, nil default: - return "", fmt.Errorf("unsupported OS: %s", runtime.GOOS) + return "", fmt.Errorf("unsupported OS: %s", name) } } func getExecName(name string) string { - if runtime.GOOS == "windows" { + if system.IsWindows() { return name + ".exe" } return name @@ -99,7 +100,7 @@ func getManifestDirs() (map[string]string, error) { } manifests := map[string]string{} - switch runtime.GOOS { + switch system.Name() { case "linux": manifests = map[string]string{ "chrome": filepath.Join(home, ".config/google-chrome/NativeMessagingHosts"), @@ -127,7 +128,7 @@ func getLocation(browser string) (map[string]string, error) { } bin := filepath.Dir(cfg) - if runtime.GOOS == "windows" { + if system.IsWindows() { return map[string]string{ "appDir": bin, "manifestDir": "", @@ -231,18 +232,20 @@ func hostDownloadURL() (string, error) { if err != nil { return "", err } - name := platformAndArch() + name := system.PlatformAndArch() return fmt.Sprintf(releaseURL, hostVersion, name, "zip"), nil } func installHost(manifestJSON []byte, manifestFile, browser string) error { - switch runtime.GOOS { + name := system.Name() + + switch name { case "linux", "darwin": return installNativeHostUnix(manifestJSON, manifestFile) case "windows": return installNativeHostWindows(manifestJSON, manifestFile, browser) default: - return fmt.Errorf("unsupported OS: %s", runtime.GOOS) + return fmt.Errorf("unsupported OS: %s", name) } } @@ -281,7 +284,7 @@ func installNativeHost(args []string, _ *core.CLIFlags) error { //nolint:funlen oldInstall := []string{exeName, "LICENSE", "README.md"} for _, file := range oldInstall { fp := filepath.Join(locations["appDir"], file) - if core.FileExists(fp) { + if system.FileExists(fp) { err = os.Remove(fp) if err != nil { return progressError("host-install", err, p) @@ -361,7 +364,7 @@ func uninstallNativeHost(args []string, _ *core.CLIFlags) error { exeName := getExecName("vale-native") for _, file := range []string{"config.json", exeName, "LICENSE", "README.md", "host.log"} { fp := filepath.Join(locations["appDir"], file) - if core.FileExists(fp) { + if system.FileExists(fp) { err = os.Remove(filepath.Join(locations["appDir"], file)) if err != nil { return progressError("host-uninstall", err, p) @@ -374,7 +377,7 @@ func uninstallNativeHost(args []string, _ *core.CLIFlags) error { p.UpdateTitle(steps[1]) manifestFile := filepath.Join(locations["manifestDir"], nativeHostName+".json") - if core.FileExists(manifestFile) { + if system.FileExists(manifestFile) { err = os.Remove(manifestFile) if err != nil { return progressError("host-uninstall", err, p) diff --git a/cmd/vale/pkg_test.go b/cmd/vale/pkg_test.go index 83a9103c..47d1c9e7 100644 --- a/cmd/vale/pkg_test.go +++ b/cmd/vale/pkg_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/errata-ai/vale/v3/internal/core" + "github.com/errata-ai/vale/v3/internal/system" ) var TestData = "../../testdata/pkg" @@ -54,11 +55,11 @@ func TestLibrary(t *testing.T) { t.Fatal(err) } - if !core.IsDir(filepath.Join(path, "write-good")) { + if !system.IsDir(filepath.Join(path, "write-good")) { t.Fatal("unable to find 'write-good' in StylesPath") } - if !core.FileExists(filepath.Join(path, "write-good", "E-Prime.yml")) { + if !system.FileExists(filepath.Join(path, "write-good", "E-Prime.yml")) { t.Fatal("unable to find 'E-Prime' in StylesPath") } } @@ -79,11 +80,11 @@ func TestLocalZip(t *testing.T) { t.Fatal(err) } - if !core.IsDir(filepath.Join(path, "write-good")) { + if !system.IsDir(filepath.Join(path, "write-good")) { t.Fatal("unable to find 'write-good' in StylesPath") } - if !core.FileExists(filepath.Join(path, "write-good", "E-Prime.yml")) { + if !system.FileExists(filepath.Join(path, "write-good", "E-Prime.yml")) { t.Fatal("unable to find 'E-Prime' in StylesPath") } } @@ -104,11 +105,11 @@ func TestLocalDir(t *testing.T) { t.Fatal(err) } - if !core.IsDir(filepath.Join(path, "write-good")) { + if !system.IsDir(filepath.Join(path, "write-good")) { t.Fatal("unable to find 'write-good' in StylesPath") } - if !core.FileExists(filepath.Join(path, "write-good", "E-Prime.yml")) { + if !system.FileExists(filepath.Join(path, "write-good", "E-Prime.yml")) { t.Fatal("unable to find 'E-Prime' in StylesPath") } } @@ -129,12 +130,12 @@ func TestLocalComplete(t *testing.T) { //nolint:dupl t.Fatal(err) } - if !core.IsDir(filepath.Join(path, "ISC")) { + if !system.IsDir(filepath.Join(path, "ISC")) { t.Fatal("unable to find 'ISC' in StylesPath") } vocab := filepath.Join(path, "Vocab", "ISC_General", "accept.txt") - if !core.FileExists(vocab) { + if !system.FileExists(vocab) { t.Fatal("unable to find 'ISC_General' in Vocab") } @@ -165,12 +166,12 @@ func TestLocalOnlyStyles(t *testing.T) { //nolint:dupl t.Fatal(err) } - if !core.IsDir(filepath.Join(path, "ISC")) { + if !system.IsDir(filepath.Join(path, "ISC")) { t.Fatal("unable to find 'ISC' in StylesPath") } vocab := filepath.Join(path, "Vocab", "ISC_General", "accept.txt") - if !core.FileExists(vocab) { + if !system.FileExists(vocab) { t.Fatal("unable to find 'ISC_General' in Vocab") } @@ -201,15 +202,15 @@ func TestV3Pkg(t *testing.T) { t.Fatal(err) } - if !core.IsDir(filepath.Join(path, "config")) { + if !system.IsDir(filepath.Join(path, "config")) { t.Fatal("unable to find 'config' in StylesPath") } - if !core.FileExists(filepath.Join(path, core.VocabDir, "Basic", "accept.txt")) { + if !system.FileExists(filepath.Join(path, core.VocabDir, "Basic", "accept.txt")) { t.Fatal("unable to find 'accept.txt'") } - if !core.FileExists(filepath.Join(path, core.TmplDir, "t.tmpl")) { + if !system.FileExists(filepath.Join(path, core.TmplDir, "t.tmpl")) { t.Fatal("unable to find 't.tmpl'") } } diff --git a/cmd/vale/sync.go b/cmd/vale/sync.go index 52ac669e..ea8929d0 100644 --- a/cmd/vale/sync.go +++ b/cmd/vale/sync.go @@ -9,13 +9,14 @@ import ( cp "github.com/otiai10/copy" "github.com/errata-ai/vale/v3/internal/core" + "github.com/errata-ai/vale/v3/internal/system" ) func initPath(cfg *core.Config) error { // The first entry is always the default `StylesPath`. stylesPath := cfg.StylesPath() - if !core.IsDir(stylesPath) { + if !system.IsDir(stylesPath) { if err := os.MkdirAll(cfg.StylesPath(), os.ModePerm); err != nil { e := fmt.Errorf("unable to initialize StylesPath (value = '%s')", cfg.StylesPath()) return core.NewE100("initPath", e) @@ -32,13 +33,13 @@ func initPath(cfg *core.Config) error { } func readPkg(pkg, path string, idx int) error { - if core.IsPhrase(pkg) && !core.IsDir(pkg) { + if core.IsPhrase(pkg) && !system.IsDir(pkg) { entry := inLibrary(pkg, path) if entry != "" { return download(pkg, entry, path, idx) } } - return loadPkg(fileNameWithoutExt(pkg), pkg, path, idx) + return loadPkg(system.FileNameWithoutExt(pkg), pkg, path, idx) } func loadPkg(name, urlOrPath, styles string, index int) error { @@ -61,7 +62,7 @@ func loadLocalZipPkg(name, pkgPath, styles string, index int) error { return err } - if err = unarchive(pkgPath, dir); err != nil { + if err = system.Unarchive(pkgPath, dir); err != nil { return err } @@ -90,12 +91,12 @@ func installPkg(dir, name, styles string, index int) error { pipe := filepath.Join(styles, core.PipeDir) cfg := filepath.Join(root, ".vale.ini") - if !core.IsDir(path) && !core.FileExists(cfg) { + if !system.IsDir(path) && !system.FileExists(cfg) { return moveAsset(name, dir, styles) // style-only } // StylesPath - if core.IsDir(path) { + if system.IsDir(path) { if err := moveDir(path, styles); err != nil { return err } @@ -106,7 +107,7 @@ func installPkg(dir, name, styles string, index int) error { // $StylesPath/config directory. for _, dir := range core.ConfigDirs { loc1 := filepath.Join(path, dir) - if core.IsDir(loc1) { + if system.IsDir(loc1) { loc2 := filepath.Join(styles, dir) if err := moveDir(loc1, loc2); err != nil { return err @@ -116,7 +117,7 @@ func installPkg(dir, name, styles string, index int) error { } // .vale.ini - if core.FileExists(cfg) { + if system.FileExists(cfg) { pkgs, err := core.GetPackages(cfg) if err != nil { return err @@ -161,7 +162,7 @@ func moveAsset(name, old, new string) error { //nolint:predeclared src := filepath.Join(old, name) dst := filepath.Join(new, name) - if core.FileExists(dst) || core.IsDir(dst) { + if system.FileExists(dst) || system.IsDir(dst) { if err := os.RemoveAll(dst); err != nil { return err } diff --git a/cmd/vale/util.go b/cmd/vale/util.go index bf6fb8b7..10b7e687 100644 --- a/cmd/vale/util.go +++ b/cmd/vale/util.go @@ -1,15 +1,10 @@ package main import ( - "archive/zip" "encoding/json" "fmt" "io" "net/http" - "os" - "path/filepath" - "runtime" - "strings" "github.com/pterm/pterm" @@ -71,72 +66,6 @@ func sendResponse(msg string, err error) error { return printJSON(resp) } -func fileNameWithoutExt(fileName string) string { - base := filepath.Base(fileName) - return strings.TrimSuffix(base, filepath.Ext(base)) -} - -func platformAndArch() string { - platform := strings.Title(runtime.GOOS) //nolint:staticcheck - - arch := strings.ToLower(runtime.GOARCH) - if arch == "amd64" { - arch = "x86_64" - } - - return fmt.Sprintf("%s_%s", platform, arch) -} - -func mkdir(dir string) error { - return os.MkdirAll(dir, os.ModeDir|0700) -} - func toCodeStyle(s string) string { return pterm.Fuzzy.Sprint(s) } - -func unarchive(src, dest string) error { - r, err := zip.OpenReader(src) - if err != nil { - return err - } - defer r.Close() - - if err = mkdir(dest); err != nil { - return err - } - - for _, file := range r.File { - destPath := filepath.Join(dest, filepath.Clean(file.Name)) - if !strings.HasPrefix(destPath, filepath.Clean(dest)+string(os.PathSeparator)) { - return fmt.Errorf("invalid file path: %s", file.Name) - } - - if file.FileInfo().IsDir() { - if err = mkdir(destPath); err != nil { - return err - } - continue - } - if err = mkdir(filepath.Dir(destPath)); err != nil { - return err - } - - dstFile, dstErr := os.OpenFile(destPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode()) - if dstErr != nil { - return dstErr - } - defer dstFile.Close() - - srcFile, srcErr := file.Open() - if srcErr != nil { - return srcErr - } - defer srcFile.Close() - - if _, err = io.Copy(dstFile, io.LimitReader(srcFile, 1024*1024*1024*10)); err != nil { - return err - } - } - return nil -} diff --git a/internal/check/filter.go b/internal/check/filter.go index d1f5b099..b73757ca 100644 --- a/internal/check/filter.go +++ b/internal/check/filter.go @@ -8,6 +8,7 @@ import ( "github.com/expr-lang/expr" "github.com/errata-ai/vale/v3/internal/core" + "github.com/errata-ai/vale/v3/internal/system" ) func filter(mgr *Manager) (map[string]Rule, error) { @@ -15,7 +16,7 @@ func filter(mgr *Manager) (map[string]Rule, error) { if stringOrPath == "" { return mgr.rules, nil - } else if !core.FileExists(stringOrPath) { + } else if !system.FileExists(stringOrPath) { name := stringOrPath stringOrPath = core.FindAsset(mgr.Config, name) diff --git a/internal/check/manager.go b/internal/check/manager.go index c49cb0f1..037537f2 100755 --- a/internal/check/manager.go +++ b/internal/check/manager.go @@ -12,6 +12,7 @@ import ( "github.com/errata-ai/vale/v3/internal/core" "github.com/errata-ai/vale/v3/internal/nlp" + "github.com/errata-ai/vale/v3/internal/system" ) // Manager controls the loading and validating of the check extension points. @@ -61,7 +62,7 @@ func NewManager(config *core.Config) (*Manager, error) { fName := parts[1] + ".yml" for _, p := range mgr.Config.SearchPaths() { path = filepath.Join(p, parts[0], fName) - if !core.FileExists(path) { + if !system.FileExists(path) { continue } if err = mgr.addRuleFromSource(fName, path); err != nil { @@ -122,14 +123,13 @@ func (mgr *Manager) AssignNLP(f *core.File) nlp.Info { } func (mgr *Manager) addStyle(path string) error { - return filepath.WalkDir(path, func(fp string, d fs.DirEntry, err error) error { + return system.Walk(path, func(fp string, info fs.FileInfo, err error) error { if err != nil { return err - } - if d.IsDir() { + } else if info.IsDir() { return nil } - return mgr.addRuleFromSource(d.Name(), fp) + return mgr.addRuleFromSource(info.Name(), fp) }) } @@ -243,11 +243,10 @@ func (mgr *Manager) loadStyles(styles []string) error { if mgr.hasStyle(style) { // We've already loaded this style. continue - } else if has := core.IsDir(p); !has { + } else if has := system.IsDir(p); !has { need = append(need, style) continue - } - if err := mgr.addStyle(p); err != nil { + } else if err := mgr.addStyle(p); err != nil { return err } found = append(found, style) diff --git a/internal/check/spelling.go b/internal/check/spelling.go index 09fa0c9c..9f752398 100644 --- a/internal/check/spelling.go +++ b/internal/check/spelling.go @@ -14,6 +14,7 @@ import ( "github.com/errata-ai/vale/v3/internal/core" "github.com/errata-ai/vale/v3/internal/nlp" "github.com/errata-ai/vale/v3/internal/spell" + "github.com/errata-ai/vale/v3/internal/system" ) var defaultFilters = []*regexp.Regexp{ @@ -138,7 +139,7 @@ func NewSpelling(cfg *core.Config, generic baseCheck, path string) (Spelling, er } for _, p := range paths { - if err = model.AddWordListFile(p); err != nil && core.FileExists(p) { + if err = model.AddWordListFile(p); err != nil && system.FileExists(p) { return rule, err } } @@ -211,7 +212,7 @@ func makeSpeller(s *Spelling, cfg *core.Config, rulePath string) (*spell.Checker affloc := core.FindAsset(cfg, s.Aff) dicloc := core.FindAsset(cfg, s.Dic) - if core.FileExists(affloc) && core.FileExists(dicloc) { + if system.FileExists(affloc) && system.FileExists(dicloc) { return spell.NewChecker(spell.UsingDictionaryByPath(dicloc, affloc)) } @@ -230,7 +231,7 @@ func makeSpeller(s *Spelling, cfg *core.Config, rulePath string) (*spell.Checker } for _, p := range paths { - if core.IsDir(p) { + if system.IsDir(p) { options = append(options, spell.WithPath(p)) found = true break diff --git a/internal/core/config.go b/internal/core/config.go index 4b4878bb..14bbb9a8 100644 --- a/internal/core/config.go +++ b/internal/core/config.go @@ -14,6 +14,7 @@ import ( "github.com/errata-ai/ini" "github.com/errata-ai/vale/v3/internal/glob" + "github.com/errata-ai/vale/v3/internal/system" ) var ( @@ -89,7 +90,7 @@ func FindAsset(cfg *Config, path string) string { for _, p := range cfg.SearchPaths() { inPath := filepath.Join(p, path) - if FileExists(inPath) { + if system.FileExists(inPath) { return inPath } } @@ -99,7 +100,7 @@ func FindAsset(cfg *Config, path string) string { } p := determinePath(cfg.Flags.Path, path) - if FileExists(p) { + if system.FileExists(p) { return p } @@ -118,7 +119,7 @@ func getConfigAsset(target string, paths, dirs []string) string { for _, p := range paths { for _, dir := range dirs { path := filepath.Join(p, dir, target) - if FileExists(path) { + if system.FileExists(path) { return path } } @@ -249,7 +250,7 @@ func NewConfig(flags *CLIFlags) (*Config, error) { cfg.ConfigFiles = []string{} found, _ := DefaultStylesPath() - if !flags.IgnoreGlobal && IsDir(found) { + if !flags.IgnoreGlobal && system.IsDir(found) { cfg.AddStylesPath(found) } @@ -292,7 +293,7 @@ func (c *Config) Root() (string, error) { return "", err } - if !FileExists(root) { + if !system.FileExists(root) { return "", fmt.Errorf("no .vale.ini file found") } @@ -365,7 +366,7 @@ func pipeConfig(cfg *Config) ([]string, error) { var sources []string pipeline := filepath.Join(cfg.StylesPath(), ".vale-config") - if IsDir(pipeline) && len(cfg.Flags.Sources) == 0 { + if system.IsDir(pipeline) && len(cfg.Flags.Sources) == 0 { configs, err := os.ReadDir(pipeline) if err != nil { return sources, err diff --git a/internal/core/config_test.go b/internal/core/config_test.go index 905f00f4..bf42813d 100644 --- a/internal/core/config_test.go +++ b/internal/core/config_test.go @@ -4,6 +4,8 @@ import ( "os" "path/filepath" "testing" + + "github.com/errata-ai/vale/v3/internal/system" ) var testData = filepath.Join("..", "..", "testdata") @@ -18,7 +20,7 @@ func TestInitCfg(t *testing.T) { // In v3.0, these should have defaults. if path == "" { t.Fatal("StylesPath is empty") - } else if !IsDir(path) { + } else if !system.IsDir(path) { t.Fatalf("%s is not a directory", path) } } diff --git a/internal/core/file.go b/internal/core/file.go index cf572dbb..a806ed76 100755 --- a/internal/core/file.go +++ b/internal/core/file.go @@ -14,6 +14,7 @@ import ( "github.com/errata-ai/vale/v3/internal/glob" "github.com/errata-ai/vale/v3/internal/nlp" + "github.com/errata-ai/vale/v3/internal/system" ) var commentControlRE = regexp.MustCompile(`^vale (.+\..+|[^.]+) = (YES|NO|on|off)$`) @@ -53,7 +54,7 @@ func NewFile(src string, config *Config) (*File, error) { var fbytes []byte var lookup bool - if FileExists(src) { + if system.FileExists(src) { fbytes, _ = os.ReadFile(src) if config.Flags.InExt != ".txt" { ext, format = FormatFromExt(config.Flags.InExt, config.Formats) diff --git a/internal/core/ini.go b/internal/core/ini.go index c3453e76..74d81e66 100644 --- a/internal/core/ini.go +++ b/internal/core/ini.go @@ -10,6 +10,7 @@ import ( "github.com/errata-ai/ini" "github.com/errata-ai/vale/v3/internal/glob" + "github.com/errata-ai/vale/v3/internal/system" ) var coreError = "'%s' is a core option; it should be defined above any syntax-specific options (`[...]`)." @@ -17,7 +18,7 @@ var coreError = "'%s' is a core option; it should be defined above any syntax-sp func determinePath(configPath string, keyPath string) string { // expand tilde at this point as this is where user-provided paths are provided keyPath = normalizePath(keyPath) - if !IsDir(configPath) { + if !system.IsDir(configPath) { configPath = filepath.Dir(configPath) } sep := string(filepath.Separator) @@ -45,7 +46,7 @@ func loadVocab(root string, cfg *Config) error { target := "" for _, p := range cfg.SearchPaths() { opt := filepath.Join(p, VocabDir, root) - if IsDir(opt) { + if system.IsDir(opt) { target = opt break } @@ -56,7 +57,7 @@ func loadVocab(root string, cfg *Config) error { "'%s/%s' directory does not exist", VocabDir, root)) } - err := filepath.Walk(target, func(fp string, info fs.FileInfo, err error) error { + err := system.Walk(target, func(fp string, info fs.FileInfo, err error) error { if err != nil { return err } @@ -204,7 +205,7 @@ var coreOpts = map[string]func(*ini.Section, *Config) error{ path := determinePath(cfg.ConfigFile(), candidate) cfg.AddStylesPath(path) - if !FileExists(path) { + if !system.FileExists(path) { return NewE201FromTarget( fmt.Sprintf("The path '%s' does not exist.", path), candidate, diff --git a/internal/core/source.go b/internal/core/source.go index 7c76c362..f3ad48c1 100644 --- a/internal/core/source.go +++ b/internal/core/source.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/errata-ai/ini" + "github.com/errata-ai/vale/v3/internal/system" ) // ConfigSrc is a source of configuration values. @@ -82,7 +83,7 @@ func FromString(src string, cfg *Config, dry bool) (*ini.File, error) { } func validateFlags(cfg *Config) error { - if cfg.Flags.Path != "" && !FileExists(cfg.Flags.Path) { + if cfg.Flags.Path != "" && !system.FileExists(cfg.Flags.Path) { return NewE100( "--config", fmt.Errorf("path '%s' does not exist", cfg.Flags.Path)) @@ -167,7 +168,7 @@ func loadINI(cfg *Config, dry bool) (*ini.File, error) { // any other sources to allow for project-agnostic customization. defaultCfg, _ := DefaultConfig() - if FileExists(defaultCfg) && !cfg.Flags.IgnoreGlobal && !dry { + if system.FileExists(defaultCfg) && !cfg.Flags.IgnoreGlobal && !dry { err = uCfg.Append(defaultCfg) if err != nil { return nil, NewE100("default/ini", err) @@ -199,7 +200,7 @@ func loadConfig(names []string) (string, error) { for _, name := range names { loc := path.Join(cwd, name) - if FileExists(loc) && !IsDir(loc) { + if system.FileExists(loc) && !system.IsDir(loc) { return loc, nil } } @@ -217,7 +218,7 @@ func loadConfig(names []string) (string, error) { for _, name := range names { loc := path.Join(homeDir, name) - if FileExists(loc) && !IsDir(loc) { + if system.FileExists(loc) && !system.IsDir(loc) { return loc, nil } } diff --git a/internal/core/util.go b/internal/core/util.go index 11334276..c974f2c6 100755 --- a/internal/core/util.go +++ b/internal/core/util.go @@ -7,9 +7,7 @@ import ( "os/exec" "path/filepath" "regexp" - "runtime" "strings" - "syscall" "unicode" "github.com/errata-ai/vale/v3/internal/nlp" @@ -183,18 +181,6 @@ func Indent(text, indent string) string { return result[:len(result)-1] } -// IsDir determines if the path given by `filename` is a directory. -func IsDir(filename string) bool { - fi, err := os.Stat(filename) - return err == nil && fi.IsDir() -} - -// FileExists determines if the path given by `filename` exists. -func FileExists(filename string) bool { - _, err := os.Stat(filename) - return err == nil -} - // StringInSlice determines if `slice` contains the string `a`. func StringInSlice(a string, slice []string) bool { for _, b := range slice { @@ -334,24 +320,6 @@ func ReplaceExt(fp string, formats map[string]string) string { return fp } -// FindProcess checks if a process with the given PID exists. -func FindProcess(pid int) *os.Process { - if pid <= 0 { - return nil - } - - p, err := os.FindProcess(pid) - if runtime.GOOS != "windows" { - err = p.Signal(os.Signal(syscall.Signal(0))) - } - - if err != nil { - return nil - } - - return p -} - // UniqueStrings returns a new slice with all duplicate strings removed. func UniqueStrings(slice []string) []string { keys := make(map[string]bool) diff --git a/internal/lint/lint.go b/internal/lint/lint.go index 32d7164c..5f06afb6 100755 --- a/internal/lint/lint.go +++ b/internal/lint/lint.go @@ -4,7 +4,6 @@ import ( "errors" "io/fs" "net/http" - "os" "path/filepath" "strings" @@ -14,12 +13,11 @@ import ( "github.com/errata-ai/vale/v3/internal/core" "github.com/errata-ai/vale/v3/internal/glob" "github.com/errata-ai/vale/v3/internal/nlp" + "github.com/errata-ai/vale/v3/internal/system" ) // A Linter lints a File. type Linter struct { - pids []int - temps []*os.File Manager *check.Manager glob *glob.Glob client *http.Client @@ -94,20 +92,12 @@ func (l *Linter) Lint(input []string, pat string) ([]*core.File, error) { return linted, err } - if err = l.setup(); err != nil { - return linted, err - } - l.glob = &gp for _, src := range input { filesChan, errChan := l.lintFiles(done, src) for result := range filesChan { if result.err != nil { - err = l.teardown() - if err != nil { - return linted, err - } return linted, result.err } else if l.Manager.Config.Flags.Normalize { result.file.Path = filepath.ToSlash(result.file.Path) @@ -116,19 +106,10 @@ func (l *Linter) Lint(input []string, pat string) ([]*core.File, error) { } if err = <-errChan; err != nil { - terr := l.teardown() - if terr != nil { - return linted, terr - } return linted, err } } - err = l.teardown() - if err != nil { - return linted, err - } - return linted, nil } @@ -141,7 +122,7 @@ func (l *Linter) lintFiles(done <-chan core.File, root string) (<-chan lintResul go func() { wg := sizedwaitgroup.New(5) - err := filepath.Walk(root, func(fp string, info fs.FileInfo, err error) error { + err := system.Walk(root, func(fp string, info fs.FileInfo, err error) error { if err != nil { return err } @@ -354,29 +335,6 @@ func (l *Linter) shouldRun(name string, f *core.File, chk check.Rule, blk nlp.Bl return true } -// setup handles any necessary building, compiling, or pre-processing. -func (l *Linter) setup() error { - return nil -} - -func (l *Linter) teardown() error { - for _, pid := range l.pids { - if p := core.FindProcess(pid); p != nil { - if procErr := p.Kill(); procErr != nil { - return procErr - } - } - } - - for _, f := range l.temps { - if ferr := os.Remove(f.Name()); ferr != nil { - return ferr - } - } - - return nil -} - func (l *Linter) match(s string) bool { if l.glob == nil { return true diff --git a/internal/lint/lint_test.go b/internal/lint/lint_test.go index 8a75323a..c21a2952 100755 --- a/internal/lint/lint_test.go +++ b/internal/lint/lint_test.go @@ -1,13 +1,70 @@ package lint import ( + "bytes" + "os" + "os/exec" "path/filepath" "regexp" "testing" "github.com/errata-ai/vale/v3/internal/core" + "github.com/errata-ai/vale/v3/internal/system" ) +func TestSymlinkFixture(t *testing.T) { + fixture := "../../testdata/fixtures/misc/symlinks" + + targetSrc := system.AbsPath(filepath.Join(fixture, "Symlinked")) + targetDst := system.AbsPath(filepath.Join(fixture, "styles", "Symlinked")) + + if _, err := os.Stat(targetSrc); os.IsNotExist(err) { + t.Fatalf("Target source does not exist: %v", targetSrc) + } + + if err := os.Symlink(targetSrc, targetDst); err != nil { + t.Fatalf("Failed to create symlink: %v", err) + } + + t.Cleanup(func() { + err := os.Remove(targetDst) + if err != nil { + t.Fatalf("Failed to remove symlink: %v", err) + } + }) + + info, err := os.Lstat(targetDst) + if err != nil { + t.Fatalf("Failed to stat symlink: %v", err) + } + + if info.Mode()&os.ModeSymlink == 0 { + t.Fatalf("Expected %v to be a symlink", targetDst) + } + + resolvedPath, err := os.Readlink(targetDst) + if err != nil { + t.Fatalf("Failed to read symlink: %v", err) + } + + if resolvedPath != targetSrc { + t.Fatalf("Symlink points to %v, expected %v", resolvedPath, targetSrc) + } + + // Call Vale on the symlinked file. + cmd := exec.Command("vale", "--output=JSON", "--no-global", "test.md") + cmd.Dir = fixture + + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("Failed to run Vale: %s", string(out)) + } + + if !bytes.Contains(out, []byte("Symlinked")) { + t.Fatalf("Expected output from Vale, got %s", string(out)) + } +} + func TestGenderBias(t *testing.T) { reToMatches := map[string][]string{ "(?:alumna|alumnus)": {"alumna", "alumnus"}, diff --git a/internal/spell/multi.go b/internal/spell/multi.go index 4a5414e5..884d3b93 100644 --- a/internal/spell/multi.go +++ b/internal/spell/multi.go @@ -8,7 +8,7 @@ import ( "path/filepath" "sort" - "github.com/errata-ai/vale/v3/internal/core" + "github.com/errata-ai/vale/v3/internal/system" ) //go:embed data/en_US-web.aff @@ -204,14 +204,14 @@ func (m *Checker) readAsset(name string) (string, error) { } option := filepath.Join(p, name) - if core.FileExists(option) { + if system.FileExists(option) { return option, nil } ln, err := os.Readlink(option) if err != nil { return "", err - } else if core.FileExists(ln) { + } else if system.FileExists(ln) { return ln, nil } } diff --git a/internal/system/dir.go b/internal/system/dir.go new file mode 100644 index 00000000..a8ce45aa --- /dev/null +++ b/internal/system/dir.go @@ -0,0 +1,14 @@ +package system + +import "os" + +// Mkdir creates a directory at the given path. +func Mkdir(dir string) error { + return os.MkdirAll(dir, os.ModeDir|0700) +} + +// IsDir determines if the path given by `filename` is a directory. +func IsDir(filename string) bool { + fi, err := os.Stat(filename) + return err == nil && fi.IsDir() +} diff --git a/internal/system/file.go b/internal/system/file.go new file mode 100644 index 00000000..a83ecfea --- /dev/null +++ b/internal/system/file.go @@ -0,0 +1,31 @@ +package system + +import ( + "os" + "path/filepath" + "strings" +) + +// AbsPath returns the absolute path of `path`. +func AbsPath(path string) string { + if filepath.IsAbs(path) { + return path + } + absPath, err := filepath.Abs(path) + if err != nil { + return path + } + return absPath +} + +// FileExists determines if the path given by `filename` exists. +func FileExists(filename string) bool { + _, err := os.Stat(filename) + return err == nil +} + +// FileNameWithoutExt returns the filename without its extension. +func FileNameWithoutExt(fileName string) string { + base := filepath.Base(fileName) + return strings.TrimSuffix(base, filepath.Ext(base)) +} diff --git a/internal/system/system.go b/internal/system/system.go new file mode 100644 index 00000000..9c0993ea --- /dev/null +++ b/internal/system/system.go @@ -0,0 +1,44 @@ +package system + +import ( + "fmt" + "runtime" + "strings" +) + +// Name returns the current OS. +func Name() string { + return runtime.GOOS +} + +// IsMac returns true if the current OS is macOS. +func IsMac() bool { + return Name() == "darwin" +} + +// IsWindows returns true if the current OS is Windows. +func IsWindows() bool { + return Name() == "windows" +} + +// IsLinux returns true if the current OS is Linux. +func IsLinux() bool { + return Name() == "linux" +} + +// IsUnix returns true if the current OS is either macOS or Linux. +func IsUnix() bool { + return IsMac() || IsLinux() +} + +// PlatformAndArch returns the current platform and architecture. +func PlatformAndArch() string { + platform := strings.Title(Name()) //nolint:staticcheck + + arch := strings.ToLower(runtime.GOARCH) + if arch == "amd64" { + arch = "x86_64" + } + + return fmt.Sprintf("%s_%s", platform, arch) +} diff --git a/internal/system/walk.go b/internal/system/walk.go new file mode 100644 index 00000000..8a082b8c --- /dev/null +++ b/internal/system/walk.go @@ -0,0 +1,39 @@ +package system + +import ( + "os" + "path/filepath" +) + +func walk(filename string, linkDirname string, walkFn filepath.WalkFunc) error { + symWalkFunc := func(path string, info os.FileInfo, err error) error { + + if fname, err := filepath.Rel(filename, path); err == nil { + path = filepath.Join(linkDirname, fname) + } else { + return err + } + + if err == nil && info.Mode()&os.ModeSymlink == os.ModeSymlink { + finalPath, err := filepath.EvalSymlinks(path) + if err != nil { + return err + } + info, err := os.Lstat(finalPath) + if err != nil { + return walkFn(path, info, err) + } + if info.IsDir() { + return walk(finalPath, path, walkFn) + } + } + + return walkFn(path, info, err) + } + return filepath.Walk(filename, symWalkFunc) +} + +// Walk extends filepath.Walk to also follow symlinks +func Walk(path string, walkFn filepath.WalkFunc) error { + return walk(path, path, walkFn) +} diff --git a/internal/system/zip.go b/internal/system/zip.go new file mode 100644 index 00000000..437b4038 --- /dev/null +++ b/internal/system/zip.go @@ -0,0 +1,57 @@ +package system + +import ( + "archive/zip" + "fmt" + "io" + "os" + "path/filepath" + "strings" +) + +// Unarchive extracts a ZIP archive to a destination directory. +func Unarchive(src, dest string) error { + r, err := zip.OpenReader(src) + if err != nil { + return err + } + defer r.Close() + + if err = Mkdir(dest); err != nil { + return err + } + + for _, file := range r.File { + destPath := filepath.Join(dest, filepath.Clean(file.Name)) + if !strings.HasPrefix(destPath, filepath.Clean(dest)+string(os.PathSeparator)) { + return fmt.Errorf("invalid file path: %s", file.Name) + } + + if file.FileInfo().IsDir() { + if err = Mkdir(destPath); err != nil { + return err + } + continue + } + if err = Mkdir(filepath.Dir(destPath)); err != nil { + return err + } + + dstFile, dstErr := os.OpenFile(destPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode()) + if dstErr != nil { + return dstErr + } + defer dstFile.Close() + + srcFile, srcErr := file.Open() + if srcErr != nil { + return srcErr + } + defer srcFile.Close() + + if _, err = io.Copy(dstFile, io.LimitReader(srcFile, 1024*1024*1024*10)); err != nil { + return err + } + } + return nil +} diff --git a/testdata/features/blueprints.feature b/testdata/features/blueprints.feature index 8248261b..3c08657d 100644 --- a/testdata/features/blueprints.feature +++ b/testdata/features/blueprints.feature @@ -9,7 +9,6 @@ Feature: Blueprints API.yml:13:17:Vale.Spelling:Did you really mean 'serrver'? API.yml:15:70:Vale.Spelling:Did you really mean 'serrver'? Rule.yml:3:39:Vale.Repetition:'can' is repeated! - test.py:1:3:Scopes.Code:'FIXME' should not be capitalized test.py:1:3:vale.Annotations:'FIXME' left in text test.py:11:3:vale.Annotations:'XXX' left in text test.py:13:16:vale.Annotations:'XXX' left in text diff --git a/testdata/fixtures/blueprints/.vale.ini b/testdata/fixtures/blueprints/.vale.ini index 8ac724bc..cf93ad6a 100644 --- a/testdata/fixtures/blueprints/.vale.ini +++ b/testdata/fixtures/blueprints/.vale.ini @@ -6,7 +6,6 @@ py = md [*.py] vale.Annotations = YES -Scopes.Code = YES Blueprint = Python diff --git a/testdata/fixtures/misc/symlinks/.vale.ini b/testdata/fixtures/misc/symlinks/.vale.ini new file mode 100644 index 00000000..cdbf913d --- /dev/null +++ b/testdata/fixtures/misc/symlinks/.vale.ini @@ -0,0 +1,4 @@ +StylesPath = styles + +[*.md] +BasedOnStyles = Symlinked diff --git a/testdata/fixtures/misc/symlinks/Symlinked/Code.yml b/testdata/fixtures/misc/symlinks/Symlinked/Code.yml new file mode 100644 index 00000000..bd79568c --- /dev/null +++ b/testdata/fixtures/misc/symlinks/Symlinked/Code.yml @@ -0,0 +1,5 @@ +extends: existence +message: "'%s' should not be capitalized" +level: warning +tokens: + - FIXME diff --git a/testdata/fixtures/misc/symlinks/styles/.gitkeep b/testdata/fixtures/misc/symlinks/styles/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/testdata/fixtures/misc/symlinks/test.md b/testdata/fixtures/misc/symlinks/test.md new file mode 100644 index 00000000..791bdba5 --- /dev/null +++ b/testdata/fixtures/misc/symlinks/test.md @@ -0,0 +1,7 @@ +# Test + +FIXME: This is a test file. + +```python +print("Hello, world!") +```