github.com/grafana/tanka@v0.26.1-0.20240506093700-c22cfc35c21a/pkg/jsonnet/lint.go (about)

     1  package jsonnet
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"path/filepath"
     9  	"time"
    10  
    11  	"github.com/gobwas/glob"
    12  	"github.com/google/go-jsonnet/linter"
    13  	"github.com/grafana/tanka/pkg/jsonnet/implementations/goimpl"
    14  	"github.com/grafana/tanka/pkg/jsonnet/jpath"
    15  	"github.com/pkg/errors"
    16  	"github.com/rs/zerolog/log"
    17  )
    18  
    19  // LintOpts modifies the behaviour of Lint
    20  type LintOpts struct {
    21  	// Excludes are a list of globs to exclude files while searching for Jsonnet
    22  	// files
    23  	Excludes []glob.Glob
    24  
    25  	// Parallelism determines the number of workers that will process files
    26  	Parallelism int
    27  
    28  	Out io.Writer
    29  }
    30  
    31  // Lint takes a list of files and directories, processes them and prints
    32  // out to stderr if there are linting warnings
    33  func Lint(fds []string, opts *LintOpts) error {
    34  	if opts.Parallelism <= 0 {
    35  		return errors.New("parallelism must be greater than 0")
    36  	}
    37  
    38  	if opts.Out == nil {
    39  		opts.Out = os.Stdout
    40  	}
    41  
    42  	var paths []string
    43  	for _, f := range fds {
    44  		fs, err := FindFiles(f, opts.Excludes)
    45  		if err != nil {
    46  			return errors.Wrap(err, "finding Jsonnet files")
    47  		}
    48  		paths = append(paths, fs...)
    49  	}
    50  
    51  	type result struct {
    52  		success bool
    53  		output  string
    54  	}
    55  	fileCh := make(chan string, len(paths))
    56  	resultCh := make(chan result, len(paths))
    57  	lintWorker := func(fileCh <-chan string, resultCh chan result) {
    58  		for file := range fileCh {
    59  			buf, success := lintWithRecover(file)
    60  			resultCh <- result{success: success, output: buf.String()}
    61  		}
    62  	}
    63  
    64  	for i := 0; i < opts.Parallelism; i++ {
    65  		go lintWorker(fileCh, resultCh)
    66  	}
    67  
    68  	for _, file := range paths {
    69  		fileCh <- file
    70  	}
    71  	close(fileCh)
    72  
    73  	lintingFailed := false
    74  	for i := 0; i < len(paths); i++ {
    75  		result := <-resultCh
    76  		lintingFailed = lintingFailed || !result.success
    77  		if result.output != "" {
    78  			fmt.Fprint(opts.Out, result.output)
    79  		}
    80  	}
    81  
    82  	if lintingFailed {
    83  		return errors.New("Linting has failed for at least one file")
    84  	}
    85  	return nil
    86  }
    87  
    88  func lintWithRecover(file string) (buf bytes.Buffer, success bool) {
    89  	file, err := filepath.Abs(file)
    90  	if err != nil {
    91  		fmt.Fprintf(&buf, "got an error getting the absolute path for %s: %v\n\n", file, err)
    92  		return
    93  	}
    94  
    95  	log.Debug().Str("file", file).Msg("linting file")
    96  	startTime := time.Now()
    97  	defer func() {
    98  		if err := recover(); err != nil {
    99  			fmt.Fprintf(&buf, "caught a panic while linting %s: %v\n\n", file, err)
   100  		}
   101  		log.Debug().Str("file", file).Dur("duration_ms", time.Since(startTime)).Msg("linted file")
   102  	}()
   103  
   104  	content, err := os.ReadFile(file)
   105  	if err != nil {
   106  		fmt.Fprintf(&buf, "got an error reading file %s: %v\n\n", file, err)
   107  		return
   108  	}
   109  
   110  	jpaths, _, _, err := jpath.Resolve(file, true)
   111  	if err != nil {
   112  		fmt.Fprintf(&buf, "got an error getting jpath for %s: %v\n\n", file, err)
   113  		return
   114  	}
   115  	vm := goimpl.MakeRawVM(jpaths, nil, nil, 0)
   116  
   117  	failed := linter.LintSnippet(vm, &buf, []linter.Snippet{{FileName: file, Code: string(content)}})
   118  	return buf, !failed
   119  }