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  }