1package golinters
2
3import (
4	"bytes"
5	"encoding/json"
6	"fmt"
7	"go/token"
8	"io/ioutil"
9	"reflect"
10
11	"github.com/BurntSushi/toml"
12	"github.com/mgechev/dots"
13	reviveConfig "github.com/mgechev/revive/config"
14	"github.com/mgechev/revive/lint"
15	"github.com/mgechev/revive/rule"
16	"github.com/pkg/errors"
17	"golang.org/x/tools/go/analysis"
18
19	"github.com/golangci/golangci-lint/pkg/config"
20	"github.com/golangci/golangci-lint/pkg/golinters/goanalysis"
21	"github.com/golangci/golangci-lint/pkg/lint/linter"
22	"github.com/golangci/golangci-lint/pkg/logutils"
23	"github.com/golangci/golangci-lint/pkg/result"
24)
25
26const reviveName = "revive"
27
28var reviveDebugf = logutils.Debug("revive")
29
30// jsonObject defines a JSON object of an failure
31type jsonObject struct {
32	Severity     lint.Severity
33	lint.Failure `json:",inline"`
34}
35
36// NewRevive returns a new Revive linter.
37func NewRevive(cfg *config.ReviveSettings) *goanalysis.Linter {
38	var issues []goanalysis.Issue
39
40	analyzer := &analysis.Analyzer{
41		Name: goanalysis.TheOnlyAnalyzerName,
42		Doc:  goanalysis.TheOnlyanalyzerDoc,
43	}
44
45	return goanalysis.NewLinter(
46		reviveName,
47		"Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint.",
48		[]*analysis.Analyzer{analyzer},
49		nil,
50	).WithContextSetter(func(lintCtx *linter.Context) {
51		analyzer.Run = func(pass *analysis.Pass) (interface{}, error) {
52			var files []string
53
54			for _, file := range pass.Files {
55				files = append(files, pass.Fset.PositionFor(file.Pos(), false).Filename)
56			}
57
58			conf, err := getReviveConfig(cfg)
59			if err != nil {
60				return nil, err
61			}
62
63			formatter, err := reviveConfig.GetFormatter("json")
64			if err != nil {
65				return nil, err
66			}
67
68			revive := lint.New(ioutil.ReadFile)
69
70			lintingRules, err := reviveConfig.GetLintingRules(conf)
71			if err != nil {
72				return nil, err
73			}
74
75			packages, err := dots.ResolvePackages(files, []string{})
76			if err != nil {
77				return nil, err
78			}
79
80			failures, err := revive.Lint(packages, lintingRules, *conf)
81			if err != nil {
82				return nil, err
83			}
84
85			formatChan := make(chan lint.Failure)
86			exitChan := make(chan bool)
87
88			var output string
89			go func() {
90				output, err = formatter.Format(formatChan, *conf)
91				if err != nil {
92					lintCtx.Log.Errorf("Format error: %v", err)
93				}
94				exitChan <- true
95			}()
96
97			for f := range failures {
98				if f.Confidence < conf.Confidence {
99					continue
100				}
101
102				formatChan <- f
103			}
104
105			close(formatChan)
106			<-exitChan
107
108			var results []jsonObject
109			err = json.Unmarshal([]byte(output), &results)
110			if err != nil {
111				return nil, err
112			}
113
114			for i := range results {
115				issues = append(issues, reviveToIssue(pass, &results[i]))
116			}
117
118			return nil, nil
119		}
120	}).WithIssuesReporter(func(*linter.Context) []goanalysis.Issue {
121		return issues
122	}).WithLoadMode(goanalysis.LoadModeSyntax)
123}
124
125func reviveToIssue(pass *analysis.Pass, object *jsonObject) goanalysis.Issue {
126	lineRangeTo := object.Position.End.Line
127	if object.RuleName == (&rule.ExportedRule{}).Name() {
128		lineRangeTo = object.Position.Start.Line
129	}
130
131	return goanalysis.NewIssue(&result.Issue{
132		Severity: string(object.Severity),
133		Text:     fmt.Sprintf("%s: %s", object.RuleName, object.Failure.Failure),
134		Pos: token.Position{
135			Filename: object.Position.Start.Filename,
136			Line:     object.Position.Start.Line,
137			Offset:   object.Position.Start.Offset,
138			Column:   object.Position.Start.Column,
139		},
140		LineRange: &result.Range{
141			From: object.Position.Start.Line,
142			To:   lineRangeTo,
143		},
144		FromLinter: reviveName,
145	}, pass)
146}
147
148// This function mimics the GetConfig function of revive.
149// This allow to get default values and right types.
150// https://github.com/golangci/golangci-lint/issues/1745
151// https://github.com/mgechev/revive/blob/389ba853b0b3587f0c3b71b5f0c61ea4e23928ec/config/config.go#L155
152func getReviveConfig(cfg *config.ReviveSettings) (*lint.Config, error) {
153	conf := defaultConfig()
154
155	if !reflect.DeepEqual(cfg, &config.ReviveSettings{}) {
156		rawRoot := createConfigMap(cfg)
157		buf := bytes.NewBuffer(nil)
158
159		err := toml.NewEncoder(buf).Encode(rawRoot)
160		if err != nil {
161			return nil, errors.Wrap(err, "failed to encode configuration")
162		}
163
164		conf = &lint.Config{}
165		_, err = toml.DecodeReader(buf, conf)
166		if err != nil {
167			return nil, errors.Wrap(err, "failed to decode configuration")
168		}
169	}
170
171	normalizeConfig(conf)
172
173	reviveDebugf("revive configuration: %#v", conf)
174
175	return conf, nil
176}
177
178func createConfigMap(cfg *config.ReviveSettings) map[string]interface{} {
179	rawRoot := map[string]interface{}{
180		"ignoreGeneratedHeader": cfg.IgnoreGeneratedHeader,
181		"confidence":            cfg.Confidence,
182		"severity":              cfg.Severity,
183		"errorCode":             cfg.ErrorCode,
184		"warningCode":           cfg.WarningCode,
185	}
186
187	rawDirectives := map[string]map[string]interface{}{}
188	for _, directive := range cfg.Directives {
189		rawDirectives[directive.Name] = map[string]interface{}{
190			"severity": directive.Severity,
191		}
192	}
193
194	if len(rawDirectives) > 0 {
195		rawRoot["directive"] = rawDirectives
196	}
197
198	rawRules := map[string]map[string]interface{}{}
199	for _, s := range cfg.Rules {
200		rawRules[s.Name] = map[string]interface{}{
201			"severity":  s.Severity,
202			"arguments": safeTomlSlice(s.Arguments),
203			"disabled":  s.Disabled,
204		}
205	}
206
207	if len(rawRules) > 0 {
208		rawRoot["rule"] = rawRules
209	}
210
211	return rawRoot
212}
213
214func safeTomlSlice(r []interface{}) []interface{} {
215	if len(r) == 0 {
216		return nil
217	}
218
219	if _, ok := r[0].(map[interface{}]interface{}); !ok {
220		return r
221	}
222
223	var typed []interface{}
224	for _, elt := range r {
225		item := map[string]interface{}{}
226		for k, v := range elt.(map[interface{}]interface{}) {
227			item[k.(string)] = v
228		}
229
230		typed = append(typed, item)
231	}
232
233	return typed
234}
235
236// This element is not exported by revive, so we need copy the code.
237// Extracted from https://github.com/mgechev/revive/blob/389ba853b0b3587f0c3b71b5f0c61ea4e23928ec/config/config.go#L15
238var defaultRules = []lint.Rule{
239	&rule.VarDeclarationsRule{},
240	&rule.PackageCommentsRule{},
241	&rule.DotImportsRule{},
242	&rule.BlankImportsRule{},
243	&rule.ExportedRule{},
244	&rule.VarNamingRule{},
245	&rule.IndentErrorFlowRule{},
246	&rule.IfReturnRule{},
247	&rule.RangeRule{},
248	&rule.ErrorfRule{},
249	&rule.ErrorNamingRule{},
250	&rule.ErrorStringsRule{},
251	&rule.ReceiverNamingRule{},
252	&rule.IncrementDecrementRule{},
253	&rule.ErrorReturnRule{},
254	&rule.UnexportedReturnRule{},
255	&rule.TimeNamingRule{},
256	&rule.ContextKeysType{},
257	&rule.ContextAsArgumentRule{},
258}
259
260// This element is not exported by revive, so we need copy the code.
261// Extracted from https://github.com/mgechev/revive/blob/389ba853b0b3587f0c3b71b5f0c61ea4e23928ec/config/config.go#L133
262func normalizeConfig(cfg *lint.Config) {
263	if cfg.Confidence == 0 {
264		cfg.Confidence = 0.8
265	}
266	severity := cfg.Severity
267	if severity != "" {
268		for k, v := range cfg.Rules {
269			if v.Severity == "" {
270				v.Severity = severity
271			}
272			cfg.Rules[k] = v
273		}
274		for k, v := range cfg.Directives {
275			if v.Severity == "" {
276				v.Severity = severity
277			}
278			cfg.Directives[k] = v
279		}
280	}
281}
282
283// This element is not exported by revive, so we need copy the code.
284// Extracted from https://github.com/mgechev/revive/blob/389ba853b0b3587f0c3b71b5f0c61ea4e23928ec/config/config.go#L182
285func defaultConfig() *lint.Config {
286	defaultConfig := lint.Config{
287		Confidence: 0.0,
288		Severity:   lint.SeverityWarning,
289		Rules:      map[string]lint.RuleConfig{},
290	}
291	for _, r := range defaultRules {
292		defaultConfig.Rules[r.Name()] = lint.RuleConfig{}
293	}
294	return &defaultConfig
295}
296