github.com/khulnasoft/codebase@v0.0.0-20231214144635-a707781cbb24/project/run.go (about)

     1  package project
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"log"
     9  	"os"
    10  	"runtime"
    11  	"strings"
    12  
    13  	"golang.org/x/sync/errgroup"
    14  
    15  	"github.com/khulnasoft/codebase"
    16  	"github.com/khulnasoft/codebase/diff"
    17  	"github.com/khulnasoft/codebase/filter"
    18  	"github.com/khulnasoft/codebase/parser"
    19  )
    20  
    21  // RunAndParse runs commands and parse results. Returns map of tool name to check results.
    22  func RunAndParse(ctx context.Context, conf *Config, runners map[string]bool, defaultLevel string, teeMode bool) (*codebase.ResultMap, error) {
    23  	var results codebase.ResultMap
    24  	// environment variables for each commands
    25  	envs := filteredEnviron()
    26  	cmdBuilder := newCmdBuilder(envs, teeMode)
    27  	var usedRunners []string
    28  	var g errgroup.Group
    29  	semaphoreNum := runtime.NumCPU()
    30  	if teeMode {
    31  		semaphoreNum = 1
    32  	}
    33  	semaphore := make(chan int, semaphoreNum)
    34  	for key, runner := range conf.Runner {
    35  		runner := runner
    36  		runnerName := getRunnerName(key, runner)
    37  		if len(runners) != 0 && !runners[runnerName] {
    38  			continue // Skip this runner.
    39  		}
    40  		usedRunners = append(usedRunners, runnerName)
    41  		semaphore <- 1
    42  		log.Printf("codebase: [start]\trunner=%s", runnerName)
    43  		fname := runner.Format
    44  		if fname == "" && len(runner.Errorformat) == 0 {
    45  			fname = runnerName
    46  		}
    47  		opt := &parser.Option{FormatName: fname, Errorformat: runner.Errorformat}
    48  		p, err := parser.New(opt)
    49  		if err != nil {
    50  			return nil, err
    51  		}
    52  		cmd, stdout, stderr, err := cmdBuilder.build(ctx, runner.Cmd)
    53  		if err != nil {
    54  			return nil, err
    55  		}
    56  		if err := cmd.Start(); err != nil {
    57  			return nil, fmt.Errorf("fail to start command: %w", err)
    58  		}
    59  		g.Go(func() error {
    60  			defer func() { <-semaphore }()
    61  			diagnostics, err := p.Parse(io.MultiReader(stdout, stderr))
    62  			if err != nil {
    63  				return err
    64  			}
    65  			level := runner.Level
    66  			if level == "" {
    67  				level = defaultLevel
    68  			}
    69  			cmdErr := cmd.Wait()
    70  			results.Store(runnerName, &codebase.Result{
    71  				Name:        runnerName,
    72  				Level:       level,
    73  				Diagnostics: diagnostics,
    74  				CmdErr:      cmdErr,
    75  			})
    76  			msg := fmt.Sprintf("codebase: [finish]\trunner=%s", runnerName)
    77  			if cmdErr != nil {
    78  				msg += fmt.Sprintf("\terror=%v", cmdErr)
    79  			}
    80  			log.Println(msg)
    81  			return nil
    82  		})
    83  	}
    84  	if err := g.Wait(); err != nil {
    85  		return nil, fmt.Errorf("fail to run codebase: %w", err)
    86  	}
    87  	if err := checkUnknownRunner(runners, usedRunners); err != nil {
    88  		return nil, err
    89  	}
    90  	return &results, nil
    91  }
    92  
    93  // Run runs codebase tasks based on Config.
    94  func Run(ctx context.Context, conf *Config, runners map[string]bool, c codebase.CommentService, d codebase.DiffService, teeMode bool, filterMode filter.Mode, failOnError bool) error {
    95  	results, err := RunAndParse(ctx, conf, runners, "", teeMode) // Level is not used.
    96  	if err != nil {
    97  		return err
    98  	}
    99  	if results.Len() == 0 {
   100  		return nil
   101  	}
   102  
   103  	b, err := d.Diff(ctx)
   104  	if err != nil {
   105  		return err
   106  	}
   107  	filediffs, err := diff.ParseMultiFile(bytes.NewReader(b))
   108  	if err != nil {
   109  		return err
   110  	}
   111  	var g errgroup.Group
   112  	results.Range(func(toolname string, result *codebase.Result) {
   113  		ds := result.Diagnostics
   114  		g.Go(func() error {
   115  			if err := result.CheckUnexpectedFailure(); err != nil {
   116  				return err
   117  			}
   118  			return codebase.RunFromResult(ctx, c, ds, filediffs, d.Strip(), toolname, filterMode, failOnError)
   119  		})
   120  	})
   121  	return g.Wait()
   122  }
   123  
   124  var secretEnvs = [...]string{
   125  	"CODEBASE_GITHUB_API_TOKEN",
   126  	"CODEBASE_GITLAB_API_TOKEN",
   127  	"CODEBASE_TOKEN",
   128  }
   129  
   130  func filteredEnviron() []string {
   131  	for _, name := range secretEnvs {
   132  		defer func(name, value string) {
   133  			if value != "" {
   134  				os.Setenv(name, value)
   135  			}
   136  		}(name, os.Getenv(name))
   137  		os.Unsetenv(name)
   138  	}
   139  	return os.Environ()
   140  }
   141  
   142  func checkUnknownRunner(specifiedRunners map[string]bool, usedRunners []string) error {
   143  	if len(specifiedRunners) == 0 {
   144  		return nil
   145  	}
   146  	for _, r := range usedRunners {
   147  		delete(specifiedRunners, r)
   148  	}
   149  	var rs []string
   150  	for r := range specifiedRunners {
   151  		rs = append(rs, r)
   152  	}
   153  	if len(specifiedRunners) != 0 {
   154  		return fmt.Errorf("runner not found: [%s]", strings.Join(rs, ","))
   155  	}
   156  	return nil
   157  }
   158  
   159  func getRunnerName(key string, runner *Runner) string {
   160  	if runner.Name != "" {
   161  		return runner.Name
   162  	}
   163  	return key
   164  }