github.com/w3security/vervet/v5@v5.3.1-0.20230618081846-5bd9b5d799dc/internal/linter/spectral/linter.go (about)

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