github.com/songshiyun/revive@v1.1.5-0.20220323112655-f8433a19b3c5/cli/main.go (about)

     1  package cli
     2  
     3  import (
     4  	"flag"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"os"
     8  	"path/filepath"
     9  	"runtime/debug"
    10  	"strings"
    11  
    12  	"github.com/fatih/color"
    13  	"github.com/mgechev/dots"
    14  	"github.com/mitchellh/go-homedir"
    15  	"github.com/songshiyun/revive/config"
    16  	"github.com/songshiyun/revive/lint"
    17  	"github.com/songshiyun/revive/logging"
    18  )
    19  
    20  var (
    21  	version = "dev"
    22  	commit  = "none"
    23  	date    = "unknown"
    24  	builtBy = "unknown"
    25  )
    26  
    27  func fail(err string) {
    28  	fmt.Fprintln(os.Stderr, err)
    29  	os.Exit(1)
    30  }
    31  
    32  // ExtraRule configures a new rule to be used with revive.
    33  type ExtraRule struct {
    34  	Rule          lint.Rule
    35  	DefaultConfig lint.RuleConfig
    36  }
    37  
    38  // NewExtraRule returns a configured extra rule
    39  func NewExtraRule(rule lint.Rule, defaultConfig lint.RuleConfig) ExtraRule {
    40  	return ExtraRule{
    41  		Rule:          rule,
    42  		DefaultConfig: defaultConfig,
    43  	}
    44  }
    45  
    46  // RunRevive runs the CLI for revive.
    47  func RunRevive(extraRules ...ExtraRule) {
    48  	log, err := logging.GetLogger()
    49  	if err != nil {
    50  		fail(err.Error())
    51  	}
    52  
    53  	formatter, err := config.GetFormatter(formatterName)
    54  	if err != nil {
    55  		fail(err.Error())
    56  	}
    57  
    58  	conf, err := mergeConf()
    59  	if err != nil {
    60  		fail(err.Error())
    61  	}
    62  
    63  	if setExitStatus {
    64  		conf.ErrorCode = 1
    65  		conf.WarningCode = 1
    66  	}
    67  
    68  	extraRuleInstances := make([]lint.Rule, len(extraRules))
    69  	for i, extraRule := range extraRules {
    70  		extraRuleInstances[i] = extraRule.Rule
    71  
    72  		ruleName := extraRule.Rule.Name()
    73  		_, isRuleAlreadyConfigured := conf.Rules[ruleName]
    74  		if !isRuleAlreadyConfigured {
    75  			conf.Rules[ruleName] = extraRule.DefaultConfig
    76  		}
    77  	}
    78  
    79  	lintingRules, err := config.GetLintingRules(conf, extraRuleInstances)
    80  	if err != nil {
    81  		fail(err.Error())
    82  	}
    83  
    84  	log.Println("Config loaded")
    85  
    86  	if len(excludePaths) == 0 { // if no excludes were set in the command line
    87  		excludePaths = conf.Exclude // use those from the configuration
    88  	}
    89  
    90  	packages, err := getPackages(excludePaths)
    91  	if err != nil {
    92  		fail(err.Error())
    93  	}
    94  	revive := lint.New(func(file string) ([]byte, error) {
    95  		return ioutil.ReadFile(file)
    96  	}, maxOpenFiles)
    97  
    98  	failures, err := revive.Lint(packages, lintingRules, *conf)
    99  	if err != nil {
   100  		fail(err.Error())
   101  	}
   102  
   103  	formatChan := make(chan lint.Failure)
   104  	exitChan := make(chan bool)
   105  
   106  	var output string
   107  	go (func() {
   108  		output, err = formatter.Format(formatChan, *conf)
   109  		if err != nil {
   110  			fail(err.Error())
   111  		}
   112  		exitChan <- true
   113  	})()
   114  
   115  	exitCode := 0
   116  	for f := range failures {
   117  		if f.Confidence < conf.Confidence {
   118  			continue
   119  		}
   120  		if exitCode == 0 {
   121  			exitCode = conf.WarningCode
   122  		}
   123  		if c, ok := conf.Rules[f.RuleName]; ok && c.Severity == lint.SeverityError {
   124  			exitCode = conf.ErrorCode
   125  		}
   126  		if c, ok := conf.Directives[f.RuleName]; ok && c.Severity == lint.SeverityError {
   127  			exitCode = conf.ErrorCode
   128  		}
   129  
   130  		formatChan <- f
   131  	}
   132  
   133  	close(formatChan)
   134  	<-exitChan
   135  	if output != "" {
   136  		fmt.Println(output)
   137  	}
   138  
   139  	os.Exit(exitCode)
   140  }
   141  
   142  func normalizeSplit(strs []string) []string {
   143  	res := []string{}
   144  	for _, s := range strs {
   145  		t := strings.Trim(s, " \t")
   146  		if len(t) > 0 {
   147  			res = append(res, t)
   148  		}
   149  	}
   150  	return res
   151  }
   152  
   153  func getPackages(excludePaths arrayFlags) ([][]string, error) {
   154  	globs := normalizeSplit(flag.Args())
   155  	if len(globs) == 0 {
   156  		//globs = append(globs, ".")
   157  	}
   158  	changeFiles, err := GetChangedFiles(rootPath)
   159  	if err != nil {
   160  		return nil, err
   161  	}
   162  	globs = append(globs, changeFiles...)
   163  	packages, err := dots.ResolvePackages(globs, normalizeSplit(excludePaths))
   164  	if err != nil {
   165  		return nil, err
   166  	}
   167  
   168  	return packages, nil
   169  }
   170  
   171  type arrayFlags []string
   172  
   173  func (i *arrayFlags) String() string {
   174  	return strings.Join([]string(*i), " ")
   175  }
   176  
   177  func (i *arrayFlags) Set(value string) error {
   178  	*i = append(*i, value)
   179  	return nil
   180  }
   181  
   182  var (
   183  	configPath    string
   184  	excludePaths  arrayFlags
   185  	formatterName string
   186  	help          bool
   187  	versionFlag   bool
   188  	setExitStatus bool
   189  	maxOpenFiles  int
   190  	rootPath      string
   191  )
   192  
   193  var originalUsage = flag.Usage
   194  
   195  func getLogo() string {
   196  	return color.YellowString(` _ __ _____   _(_)__  _____
   197  | '__/ _ \ \ / / \ \ / / _ \
   198  | | |  __/\ V /| |\ V /  __/
   199  |_|  \___| \_/ |_| \_/ \___|`)
   200  }
   201  
   202  func getCall() string {
   203  	return color.MagentaString("revive -config c.toml -formatter friendly -exclude a.go -exclude b.go ./...")
   204  }
   205  
   206  func getBanner() string {
   207  	return fmt.Sprintf(`
   208  %s
   209  
   210  Example:
   211    %s
   212  `, getLogo(), getCall())
   213  }
   214  
   215  func buildDefaultConfigPath() string {
   216  	var result string
   217  	if homeDir, err := homedir.Dir(); err == nil {
   218  		result = filepath.Join(homeDir, "revive.toml")
   219  		if _, err := os.Stat(result); err != nil {
   220  			result = ""
   221  		}
   222  	}
   223  
   224  	return result
   225  }
   226  
   227  func init() {
   228  	// Force colorizing for no TTY environments
   229  	if os.Getenv("REVIVE_FORCE_COLOR") == "1" {
   230  		color.NoColor = false
   231  	}
   232  
   233  	flag.Usage = func() {
   234  		fmt.Println(getBanner())
   235  		originalUsage()
   236  	}
   237  
   238  	// command line help strings
   239  	const (
   240  		configUsage       = "path to the configuration TOML file, defaults to $HOME/revive.toml, if present (i.e. -config myconf.toml)"
   241  		excludeUsage      = "list of globs which specify files to be excluded (i.e. -exclude foo/...)"
   242  		formatterUsage    = "formatter to be used for the output (i.e. -formatter stylish)"
   243  		versionUsage      = "get revive version"
   244  		exitStatusUsage   = "set exit status to 1 if any issues are found, overwrites errorCode and warningCode in config"
   245  		maxOpenFilesUsage = "maximum number of open files at the same time"
   246  		rootPathUsage     = "the root path for the git directory"
   247  	)
   248  
   249  	defaultConfigPath := buildDefaultConfigPath()
   250  
   251  	flag.StringVar(&configPath, "config", defaultConfigPath, configUsage)
   252  	flag.Var(&excludePaths, "exclude", excludeUsage)
   253  	flag.StringVar(&formatterName, "formatter", "", formatterUsage)
   254  	flag.BoolVar(&versionFlag, "version", false, versionUsage)
   255  	flag.BoolVar(&setExitStatus, "set_exit_status", false, exitStatusUsage)
   256  	flag.IntVar(&maxOpenFiles, "max_open_files", 0, maxOpenFilesUsage)
   257  	flag.StringVar(&rootPath, "root", "./", rootPathUsage)
   258  	flag.Parse()
   259  
   260  	// Output build info (version, commit, date and builtBy)
   261  	if versionFlag {
   262  		var buildInfo string
   263  		if date != "unknown" && builtBy != "unknown" {
   264  			buildInfo = fmt.Sprintf("Built\t\t%s by %s\n", date, builtBy)
   265  		}
   266  
   267  		if commit != "none" {
   268  			buildInfo = fmt.Sprintf("Commit:\t\t%s\n%s", commit, buildInfo)
   269  		}
   270  
   271  		if version == "dev" {
   272  			bi, ok := debug.ReadBuildInfo()
   273  			if ok {
   274  				version = bi.Main.Version
   275  				if strings.HasPrefix(version, "v") {
   276  					version = bi.Main.Version[1:]
   277  				}
   278  				if len(buildInfo) == 0 {
   279  					fmt.Printf("version %s\n", version)
   280  					os.Exit(0)
   281  				}
   282  			}
   283  		}
   284  
   285  		fmt.Printf("Version:\t%s\n%s", version, buildInfo)
   286  		os.Exit(0)
   287  	}
   288  }