Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
281 changes: 281 additions & 0 deletions cli/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
package cli

import (
"flag"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"runtime/debug"
"strings"

"github.com/fatih/color"
"github.com/mgechev/dots"
"github.com/mgechev/revive/config"
"github.com/mgechev/revive/lint"
"github.com/mgechev/revive/logging"
"github.com/mitchellh/go-homedir"
)

var (
version = "dev"
commit = "none"
date = "unknown"
builtBy = "unknown"
)

func fail(err string) {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}

// ExtraRule configures a new rule to be used with revive.
type ExtraRule struct {
Rule lint.Rule
DefaultConfig lint.RuleConfig
}

// NewExtraRule returns a configured extra rule
func NewExtraRule(rule lint.Rule, defaultConfig lint.RuleConfig) ExtraRule {
return ExtraRule{
Rule: rule,
DefaultConfig: defaultConfig,
}
}

// RunRevive runs the CLI for revive.
func RunRevive(extraRules ...ExtraRule) {
log, err := logging.GetLogger()
if err != nil {
fail(err.Error())
}

formatter, err := config.GetFormatter(formatterName)
if err != nil {
fail(err.Error())
}

conf, err := config.GetConfig(configPath)
if err != nil {
fail(err.Error())
}

if setExitStatus {
conf.ErrorCode = 1
conf.WarningCode = 1
}

extraRuleInstances := make([]lint.Rule, len(extraRules))
for i, extraRule := range extraRules {
extraRuleInstances[i] = extraRule.Rule

ruleName := extraRule.Rule.Name()
_, isRuleAlreadyConfigured := conf.Rules[ruleName]
if !isRuleAlreadyConfigured {
conf.Rules[ruleName] = extraRule.DefaultConfig
}
}

lintingRules, err := config.GetLintingRules(conf, extraRuleInstances)
if err != nil {
fail(err.Error())
}

log.Println("Config loaded")

if len(excludePaths) == 0 { // if no excludes were set in the command line
excludePaths = conf.Exclude // use those from the configuration
}

packages, err := getPackages(excludePaths)
if err != nil {
fail(err.Error())
}
revive := lint.New(func(file string) ([]byte, error) {
return ioutil.ReadFile(file)
}, maxOpenFiles)

failures, err := revive.Lint(packages, lintingRules, *conf)
if err != nil {
fail(err.Error())
}

formatChan := make(chan lint.Failure)
exitChan := make(chan bool)

var output string
go (func() {
output, err = formatter.Format(formatChan, *conf)
if err != nil {
fail(err.Error())
}
exitChan <- true
})()

exitCode := 0
for f := range failures {
if f.Confidence < conf.Confidence {
continue
}
if exitCode == 0 {
exitCode = conf.WarningCode
}
if c, ok := conf.Rules[f.RuleName]; ok && c.Severity == lint.SeverityError {
exitCode = conf.ErrorCode
}
if c, ok := conf.Directives[f.RuleName]; ok && c.Severity == lint.SeverityError {
exitCode = conf.ErrorCode
}

formatChan <- f
}

close(formatChan)
<-exitChan
if output != "" {
fmt.Println(output)
}

os.Exit(exitCode)
}

func normalizeSplit(strs []string) []string {
res := []string{}
for _, s := range strs {
t := strings.Trim(s, " \t")
if len(t) > 0 {
res = append(res, t)
}
}
return res
}

func getPackages(excludePaths arrayFlags) ([][]string, error) {
globs := normalizeSplit(flag.Args())
if len(globs) == 0 {
globs = append(globs, ".")
}

packages, err := dots.ResolvePackages(globs, normalizeSplit(excludePaths))
if err != nil {
return nil, err
}

return packages, nil
}

type arrayFlags []string

func (i *arrayFlags) String() string {
return strings.Join([]string(*i), " ")
}

func (i *arrayFlags) Set(value string) error {
*i = append(*i, value)
return nil
}

var (
configPath string
excludePaths arrayFlags
formatterName string
help bool
versionFlag bool
setExitStatus bool
maxOpenFiles int
)

var originalUsage = flag.Usage

func getLogo() string {
return color.YellowString(` _ __ _____ _(_)__ _____
| '__/ _ \ \ / / \ \ / / _ \
| | | __/\ V /| |\ V / __/
|_| \___| \_/ |_| \_/ \___|`)
}

func getCall() string {
return color.MagentaString("revive -config c.toml -formatter friendly -exclude a.go -exclude b.go ./...")
}

func getBanner() string {
return fmt.Sprintf(`
%s

Example:
%s
`, getLogo(), getCall())
}

func buildDefaultConfigPath() string {
var result string
if homeDir, err := homedir.Dir(); err == nil {
result = filepath.Join(homeDir, "revive.toml")
if _, err := os.Stat(result); err != nil {
result = ""
}
}

return result
}

func init() {
// Force colorizing for no TTY environments
if os.Getenv("REVIVE_FORCE_COLOR") == "1" {
color.NoColor = false
}

flag.Usage = func() {
fmt.Println(getBanner())
originalUsage()
}

// command line help strings
const (
configUsage = "path to the configuration TOML file, defaults to $HOME/revive.toml, if present (i.e. -config myconf.toml)"
excludeUsage = "list of globs which specify files to be excluded (i.e. -exclude foo/...)"
formatterUsage = "formatter to be used for the output (i.e. -formatter stylish)"
versionUsage = "get revive version"
exitStatusUsage = "set exit status to 1 if any issues are found, overwrites errorCode and warningCode in config"
maxOpenFilesUsage = "maximum number of open files at the same time"
)

defaultConfigPath := buildDefaultConfigPath()

flag.StringVar(&configPath, "config", defaultConfigPath, configUsage)
flag.Var(&excludePaths, "exclude", excludeUsage)
flag.StringVar(&formatterName, "formatter", "", formatterUsage)
flag.BoolVar(&versionFlag, "version", false, versionUsage)
flag.BoolVar(&setExitStatus, "set_exit_status", false, exitStatusUsage)
flag.IntVar(&maxOpenFiles, "max_open_files", 0, maxOpenFilesUsage)
flag.Parse()

// Output build info (version, commit, date and builtBy)
if versionFlag {
var buildInfo string
if date != "unknown" && builtBy != "unknown" {
buildInfo = fmt.Sprintf("Built\t\t%s by %s\n", date, builtBy)
}

if commit != "none" {
buildInfo = fmt.Sprintf("Commit:\t\t%s\n%s", commit, buildInfo)
}

if version == "dev" {
bi, ok := debug.ReadBuildInfo()
if ok {
version = bi.Main.Version
if strings.HasPrefix(version, "v") {
version = bi.Main.Version[1:]
}
if len(buildInfo) == 0 {
fmt.Printf("version %s\n", version)
os.Exit(0)
}
}
}

fmt.Printf("Version:\t%s\n%s", version, buildInfo)
os.Exit(0)
}
}
8 changes: 7 additions & 1 deletion config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,17 @@ func getFormatters() map[string]lint.Formatter {
}

// GetLintingRules yields the linting rules that must be applied by the linter
func GetLintingRules(config *lint.Config) ([]lint.Rule, error) {
func GetLintingRules(config *lint.Config, extraRules []lint.Rule) ([]lint.Rule, error) {
rulesMap := map[string]lint.Rule{}
for _, r := range allRules {
rulesMap[r.Name()] = r
}
for _, r := range extraRules {
if _, ok := rulesMap[r.Name()]; ok {
continue
}
rulesMap[r.Name()] = r
}

var lintingRules []lint.Rule
for name, ruleConfig := range config.Rules {
Expand Down
4 changes: 2 additions & 2 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ func TestGetLintingRules(t *testing.T) {
if err != nil {
t.Fatalf("Unexpected error while loading conf: %v", err)
}
rules, err := GetLintingRules(cfg)
rules, err := GetLintingRules(cfg, []lint.Rule{})
switch {
case err != nil:
t.Fatalf("Unexpected error\n\t%v", err)
Expand Down Expand Up @@ -130,7 +130,7 @@ func TestGetGlobalSeverity(t *testing.T) {
if err != nil {
t.Fatalf("Unexpected error while loading conf: %v", err)
}
rules, err := GetLintingRules(cfg)
rules, err := GetLintingRules(cfg, []lint.Rule{})
if err != nil {
t.Fatalf("Unexpected error while loading conf: %v", err)
}
Expand Down
Loading