Skip to content

Commit 1c28383

Browse files
heynemannheynemann-cbchavacava
authored
Allow revive to be called with extra linters (#650)
This change allows revive to be called from main.go in other libraries and pass in a list of custom linters to be added to the built-in linters found in config Co-authored-by: Bernardo Heynemann <[email protected]> Co-authored-by: chavacava <[email protected]>
1 parent 5ce2ff5 commit 1c28383

File tree

4 files changed

+292
-252
lines changed

4 files changed

+292
-252
lines changed

cli/main.go

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
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/mgechev/revive/config"
15+
"github.com/mgechev/revive/lint"
16+
"github.com/mgechev/revive/logging"
17+
"github.com/mitchellh/go-homedir"
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 := config.GetConfig(configPath)
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+
159+
packages, err := dots.ResolvePackages(globs, normalizeSplit(excludePaths))
160+
if err != nil {
161+
return nil, err
162+
}
163+
164+
return packages, nil
165+
}
166+
167+
type arrayFlags []string
168+
169+
func (i *arrayFlags) String() string {
170+
return strings.Join([]string(*i), " ")
171+
}
172+
173+
func (i *arrayFlags) Set(value string) error {
174+
*i = append(*i, value)
175+
return nil
176+
}
177+
178+
var (
179+
configPath string
180+
excludePaths arrayFlags
181+
formatterName string
182+
help bool
183+
versionFlag bool
184+
setExitStatus bool
185+
maxOpenFiles int
186+
)
187+
188+
var originalUsage = flag.Usage
189+
190+
func getLogo() string {
191+
return color.YellowString(` _ __ _____ _(_)__ _____
192+
| '__/ _ \ \ / / \ \ / / _ \
193+
| | | __/\ V /| |\ V / __/
194+
|_| \___| \_/ |_| \_/ \___|`)
195+
}
196+
197+
func getCall() string {
198+
return color.MagentaString("revive -config c.toml -formatter friendly -exclude a.go -exclude b.go ./...")
199+
}
200+
201+
func getBanner() string {
202+
return fmt.Sprintf(`
203+
%s
204+
205+
Example:
206+
%s
207+
`, getLogo(), getCall())
208+
}
209+
210+
func buildDefaultConfigPath() string {
211+
var result string
212+
if homeDir, err := homedir.Dir(); err == nil {
213+
result = filepath.Join(homeDir, "revive.toml")
214+
if _, err := os.Stat(result); err != nil {
215+
result = ""
216+
}
217+
}
218+
219+
return result
220+
}
221+
222+
func init() {
223+
// Force colorizing for no TTY environments
224+
if os.Getenv("REVIVE_FORCE_COLOR") == "1" {
225+
color.NoColor = false
226+
}
227+
228+
flag.Usage = func() {
229+
fmt.Println(getBanner())
230+
originalUsage()
231+
}
232+
233+
// command line help strings
234+
const (
235+
configUsage = "path to the configuration TOML file, defaults to $HOME/revive.toml, if present (i.e. -config myconf.toml)"
236+
excludeUsage = "list of globs which specify files to be excluded (i.e. -exclude foo/...)"
237+
formatterUsage = "formatter to be used for the output (i.e. -formatter stylish)"
238+
versionUsage = "get revive version"
239+
exitStatusUsage = "set exit status to 1 if any issues are found, overwrites errorCode and warningCode in config"
240+
maxOpenFilesUsage = "maximum number of open files at the same time"
241+
)
242+
243+
defaultConfigPath := buildDefaultConfigPath()
244+
245+
flag.StringVar(&configPath, "config", defaultConfigPath, configUsage)
246+
flag.Var(&excludePaths, "exclude", excludeUsage)
247+
flag.StringVar(&formatterName, "formatter", "", formatterUsage)
248+
flag.BoolVar(&versionFlag, "version", false, versionUsage)
249+
flag.BoolVar(&setExitStatus, "set_exit_status", false, exitStatusUsage)
250+
flag.IntVar(&maxOpenFiles, "max_open_files", 0, maxOpenFilesUsage)
251+
flag.Parse()
252+
253+
// Output build info (version, commit, date and builtBy)
254+
if versionFlag {
255+
var buildInfo string
256+
if date != "unknown" && builtBy != "unknown" {
257+
buildInfo = fmt.Sprintf("Built\t\t%s by %s\n", date, builtBy)
258+
}
259+
260+
if commit != "none" {
261+
buildInfo = fmt.Sprintf("Commit:\t\t%s\n%s", commit, buildInfo)
262+
}
263+
264+
if version == "dev" {
265+
bi, ok := debug.ReadBuildInfo()
266+
if ok {
267+
version = bi.Main.Version
268+
if strings.HasPrefix(version, "v") {
269+
version = bi.Main.Version[1:]
270+
}
271+
if len(buildInfo) == 0 {
272+
fmt.Printf("version %s\n", version)
273+
os.Exit(0)
274+
}
275+
}
276+
}
277+
278+
fmt.Printf("Version:\t%s\n%s", version, buildInfo)
279+
os.Exit(0)
280+
}
281+
}

config/config.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,17 @@ func getFormatters() map[string]lint.Formatter {
107107
}
108108

109109
// GetLintingRules yields the linting rules that must be applied by the linter
110-
func GetLintingRules(config *lint.Config) ([]lint.Rule, error) {
110+
func GetLintingRules(config *lint.Config, extraRules []lint.Rule) ([]lint.Rule, error) {
111111
rulesMap := map[string]lint.Rule{}
112112
for _, r := range allRules {
113113
rulesMap[r.Name()] = r
114114
}
115+
for _, r := range extraRules {
116+
if _, ok := rulesMap[r.Name()]; ok {
117+
continue
118+
}
119+
rulesMap[r.Name()] = r
120+
}
115121

116122
var lintingRules []lint.Rule
117123
for name, ruleConfig := range config.Rules {

config/config_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ func TestGetLintingRules(t *testing.T) {
9292
if err != nil {
9393
t.Fatalf("Unexpected error while loading conf: %v", err)
9494
}
95-
rules, err := GetLintingRules(cfg)
95+
rules, err := GetLintingRules(cfg, []lint.Rule{})
9696
switch {
9797
case err != nil:
9898
t.Fatalf("Unexpected error\n\t%v", err)
@@ -130,7 +130,7 @@ func TestGetGlobalSeverity(t *testing.T) {
130130
if err != nil {
131131
t.Fatalf("Unexpected error while loading conf: %v", err)
132132
}
133-
rules, err := GetLintingRules(cfg)
133+
rules, err := GetLintingRules(cfg, []lint.Rule{})
134134
if err != nil {
135135
t.Fatalf("Unexpected error while loading conf: %v", err)
136136
}

0 commit comments

Comments
 (0)