github.com/snyk/vervet/v3@v3.7.0/internal/linter/spectral/linter.go (about) 1 package spectral 2 3 import ( 4 "context" 5 "fmt" 6 "io/ioutil" 7 "os" 8 "os/exec" 9 "path/filepath" 10 11 "github.com/ghodss/yaml" 12 13 "github.com/snyk/vervet/v3/config" 14 "github.com/snyk/vervet/v3/internal/files" 15 "github.com/snyk/vervet/v3/internal/linter" 16 ) 17 18 // Spectral runs spectral on collections of files with a set of rules. 19 type Spectral struct { 20 rules []string 21 extraArgs []string 22 23 spectralPath string 24 rulesPath string 25 } 26 27 // New returns a new Spectral instance. 28 func New(ctx context.Context, cfg *config.SpectralLinter) (*Spectral, error) { 29 rules, extraArgs := cfg.Rules, cfg.ExtraArgs 30 if len(rules) == 0 { 31 return nil, fmt.Errorf("missing spectral rules") 32 } 33 spectralPath, ok := findSpectralAdjacent() 34 if !ok { 35 spectralPath, ok = findSpectralFromPath() 36 } 37 if !ok { 38 return nil, fmt.Errorf("cannot find spectral linter: `npm install -g spectral-cli` and try again?") 39 } 40 41 var rulesPath string 42 rulesFile, err := ioutil.TempFile("", "*.yaml") 43 if err != nil { 44 return nil, fmt.Errorf("failed to create temp rules file: %w", err) 45 } 46 defer rulesFile.Close() 47 resolvedRules := make([]string, len(rules)) 48 for i := range rules { 49 resolvedRules[i], err = filepath.Abs(rules[i]) 50 if err != nil { 51 return nil, err 52 } 53 } 54 rulesDoc := map[string]interface{}{ 55 "extends": resolvedRules, 56 } 57 rulesBuf, err := yaml.Marshal(&rulesDoc) 58 if err != nil { 59 return nil, fmt.Errorf("failed to marshal temp rules file: %w", err) 60 } 61 _, err = rulesFile.Write(rulesBuf) 62 if err != nil { 63 return nil, fmt.Errorf("failed to marshal temp rules file: %w", err) 64 } 65 rulesPath = rulesFile.Name() 66 go func() { 67 <-ctx.Done() 68 os.Remove(rulesPath) 69 }() 70 return &Spectral{ 71 rules: resolvedRules, 72 spectralPath: spectralPath, 73 rulesPath: rulesPath, 74 extraArgs: extraArgs, 75 }, nil 76 } 77 78 // Match implements linter.Linter. 79 func (s *Spectral) Match(rcConfig *config.ResourceSet) ([]string, error) { 80 return files.LocalFSSource{}.Match(rcConfig) 81 } 82 83 // WithOverride implements linter.Linter. 84 func (s *Spectral) WithOverride(ctx context.Context, override *config.Linter) (linter.Linter, error) { 85 if override.Spectral == nil { 86 return nil, fmt.Errorf("invalid linter override") 87 } 88 merged := *override.Spectral 89 merged.Rules = append(s.rules, merged.Rules...) 90 return New(ctx, &merged) 91 } 92 93 // Run runs spectral on the given paths. Linting output is written to standard 94 // output by spectral. Returns an error when lint fails configured rules. 95 func (l *Spectral) Run(ctx context.Context, _ string, paths ...string) error { 96 cmd := exec.CommandContext(ctx, l.spectralPath, append(append([]string{"lint", "-r", l.rulesPath}, l.extraArgs...), paths...)...) 97 cmd.Stdin = os.Stdin 98 cmd.Stdout = os.Stdout 99 cmd.Stderr = os.Stderr 100 return cmd.Run() 101 } 102 103 func findSpectralAdjacent() (string, bool) { 104 if len(os.Args) < 1 { 105 // hmmm 106 return "", false 107 } 108 binDir := filepath.Dir(os.Args[0]) 109 binFile := filepath.Join(binDir, "spectral") 110 st, err := os.Stat(binFile) 111 return binFile, err == nil && !st.IsDir() && st.Mode()&0111 != 0 112 } 113 114 func findSpectralFromPath() (string, bool) { 115 path, err := exec.LookPath("spectral") 116 return path, err == nil 117 }