Skip to content

Commit fa7adcb

Browse files
authored
add ability to set issue severity (#1155)
* add ability to set issue severity for out formats that support it based on severity rules * fix lint issues * change log child name * code climate omit severity if empty * add tests for severity rules, add support for case sensitive rules, fix lint issues, better doc comments, share processor test * deduplicated rule logic into a base rule that can be used by multiple rule types, moved severity config to it's own parent key named severity, reduced size of NewRunner function to make it easier to read * put validate function under base rule struct * better validation error wording * add Fingerprint and Description methods to Issue struct, made codeclimate reporter easier to read, checkstyle output is now pretty printed
1 parent dc260be commit fa7adcb

18 files changed

+683
-217
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,5 @@
1515
/.vscode/
1616
*.test
1717
.DS_Store
18+
coverage.out
19+
coverage.xml

.golangci.example.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,3 +382,28 @@ issues:
382382

383383
# Show only new issues created in git patch with set file path.
384384
new-from-patch: path/to/patch/file
385+
386+
severity:
387+
# Default value is empty string.
388+
# Set the default severity for issues. If severity rules are defined and the issues
389+
# do not match or no severity is provided to the rule this will be the default
390+
# severity applied. Severities should match the supported severity names of the
391+
# selected out format.
392+
# - Code climate: https://docs.codeclimate.com/docs/issues#issue-severity
393+
# - Checkstyle: https://checkstyle.sourceforge.io/property_types.html#severity
394+
# - Github: https://help.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message
395+
default-severity: error
396+
397+
# The default value is false.
398+
# If set to true severity-rules regular expressions become case sensitive.
399+
case-sensitive: false
400+
401+
# Default value is empty list.
402+
# When a list of severity rules are provided, severity information will be added to lint
403+
# issues. Severity rules have the same filtering capability as exclude rules except you
404+
# are allowed to specify one matcher per severity rule.
405+
# Only affects out formats that support setting severity information.
406+
rules:
407+
- linters:
408+
- dupl
409+
severity: info

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ require (
99
github.com/fatih/color v1.9.0
1010
github.com/go-critic/go-critic v0.4.3
1111
github.com/go-lintpack/lintpack v0.5.2
12+
github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b
1213
github.com/gofrs/flock v0.7.1
1314
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2
1415
github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a

pkg/config/config.go

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -407,51 +407,71 @@ type Linters struct {
407407
Presets []string
408408
}
409409

410-
type ExcludeRule struct {
410+
type BaseRule struct {
411411
Linters []string
412412
Path string
413413
Text string
414414
Source string
415415
}
416416

417-
func validateOptionalRegex(value string) error {
418-
if value == "" {
419-
return nil
420-
}
421-
_, err := regexp.Compile(value)
422-
return err
423-
}
424-
425-
func (e ExcludeRule) Validate() error {
426-
if err := validateOptionalRegex(e.Path); err != nil {
417+
func (b BaseRule) Validate(minConditionsCount int) error {
418+
if err := validateOptionalRegex(b.Path); err != nil {
427419
return fmt.Errorf("invalid path regex: %v", err)
428420
}
429-
if err := validateOptionalRegex(e.Text); err != nil {
421+
if err := validateOptionalRegex(b.Text); err != nil {
430422
return fmt.Errorf("invalid text regex: %v", err)
431423
}
432-
if err := validateOptionalRegex(e.Source); err != nil {
424+
if err := validateOptionalRegex(b.Source); err != nil {
433425
return fmt.Errorf("invalid source regex: %v", err)
434426
}
435427
nonBlank := 0
436-
if len(e.Linters) > 0 {
428+
if len(b.Linters) > 0 {
437429
nonBlank++
438430
}
439-
if e.Path != "" {
431+
if b.Path != "" {
440432
nonBlank++
441433
}
442-
if e.Text != "" {
434+
if b.Text != "" {
443435
nonBlank++
444436
}
445-
if e.Source != "" {
437+
if b.Source != "" {
446438
nonBlank++
447439
}
448-
const minConditionsCount = 2
449440
if nonBlank < minConditionsCount {
450-
return errors.New("at least 2 of (text, source, path, linters) should be set")
441+
return fmt.Errorf("at least %d of (text, source, path, linters) should be set", minConditionsCount)
451442
}
452443
return nil
453444
}
454445

446+
const excludeRuleMinConditionsCount = 2
447+
448+
type ExcludeRule struct {
449+
BaseRule `mapstructure:",squash"`
450+
}
451+
452+
func validateOptionalRegex(value string) error {
453+
if value == "" {
454+
return nil
455+
}
456+
_, err := regexp.Compile(value)
457+
return err
458+
}
459+
460+
func (e ExcludeRule) Validate() error {
461+
return e.BaseRule.Validate(excludeRuleMinConditionsCount)
462+
}
463+
464+
const severityRuleMinConditionsCount = 1
465+
466+
type SeverityRule struct {
467+
BaseRule `mapstructure:",squash"`
468+
Severity string
469+
}
470+
471+
func (s *SeverityRule) Validate() error {
472+
return s.BaseRule.Validate(severityRuleMinConditionsCount)
473+
}
474+
455475
type Issues struct {
456476
IncludeDefaultExcludes []string `mapstructure:"include"`
457477
ExcludeCaseSensitive bool `mapstructure:"exclude-case-sensitive"`
@@ -469,6 +489,12 @@ type Issues struct {
469489
NeedFix bool `mapstructure:"fix"`
470490
}
471491

492+
type Severity struct {
493+
Default string `mapstructure:"default-severity"`
494+
CaseSensitive bool `mapstructure:"case-sensitive"`
495+
Rules []SeverityRule `mapstructure:"rules"`
496+
}
497+
472498
type Config struct {
473499
Run Run
474500

@@ -484,6 +510,7 @@ type Config struct {
484510
LintersSettings LintersSettings `mapstructure:"linters-settings"`
485511
Linters Linters
486512
Issues Issues
513+
Severity Severity
487514

488515
InternalTest bool // Option is used only for testing golangci-lint code, don't use it
489516
}

pkg/config/reader.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,14 @@ func (r *FileReader) validateConfig() error {
113113
return fmt.Errorf("error in exclude rule #%d: %v", i, err)
114114
}
115115
}
116+
if len(c.Severity.Rules) > 0 && c.Severity.Default == "" {
117+
return errors.New("can't set severity rule option: no default severity defined")
118+
}
119+
for i, rule := range c.Severity.Rules {
120+
if err := rule.Validate(); err != nil {
121+
return fmt.Errorf("error in severity rule #%d: %v", i, err)
122+
}
123+
}
116124
if err := c.LintersSettings.Govet.Validate(); err != nil {
117125
return fmt.Errorf("error in govet config: %v", err)
118126
}

pkg/lint/runner.go

Lines changed: 92 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -31,24 +31,6 @@ type Runner struct {
3131

3232
func NewRunner(cfg *config.Config, log logutils.Log, goenv *goutil.Env, es *lintersdb.EnabledSet,
3333
lineCache *fsutils.LineCache, dbManager *lintersdb.Manager, pkgs []*gopackages.Package) (*Runner, error) {
34-
icfg := cfg.Issues
35-
excludePatterns := icfg.ExcludePatterns
36-
if icfg.UseDefaultExcludes {
37-
excludePatterns = append(excludePatterns, config.GetExcludePatternsStrings(icfg.IncludeDefaultExcludes)...)
38-
}
39-
40-
var excludeTotalPattern string
41-
if len(excludePatterns) != 0 {
42-
excludeTotalPattern = fmt.Sprintf("(%s)", strings.Join(excludePatterns, "|"))
43-
}
44-
45-
var excludeProcessor processors.Processor
46-
if cfg.Issues.ExcludeCaseSensitive {
47-
excludeProcessor = processors.NewExcludeCaseSensitive(excludeTotalPattern)
48-
} else {
49-
excludeProcessor = processors.NewExclude(excludeTotalPattern)
50-
}
51-
5234
skipFilesProcessor, err := processors.NewSkipFiles(cfg.Run.SkipFiles)
5335
if err != nil {
5436
return nil, err
@@ -63,22 +45,6 @@ func NewRunner(cfg *config.Config, log logutils.Log, goenv *goutil.Env, es *lint
6345
return nil, err
6446
}
6547

66-
var excludeRules []processors.ExcludeRule
67-
for _, r := range icfg.ExcludeRules {
68-
excludeRules = append(excludeRules, processors.ExcludeRule{
69-
Text: r.Text,
70-
Source: r.Source,
71-
Path: r.Path,
72-
Linters: r.Linters,
73-
})
74-
}
75-
var excludeRulesProcessor processors.Processor
76-
if cfg.Issues.ExcludeCaseSensitive {
77-
excludeRulesProcessor = processors.NewExcludeRulesCaseSensitive(excludeRules, lineCache, log.Child("exclude_rules"))
78-
} else {
79-
excludeRulesProcessor = processors.NewExcludeRules(excludeRules, lineCache, log.Child("exclude_rules"))
80-
}
81-
8248
enabledLinters, err := es.GetEnabledLintersMap()
8349
if err != nil {
8450
return nil, errors.Wrap(err, "failed to get enabled linters")
@@ -101,17 +67,18 @@ func NewRunner(cfg *config.Config, log logutils.Log, goenv *goutil.Env, es *lint
10167
// Must be before exclude because users see already marked output and configure excluding by it.
10268
processors.NewIdentifierMarker(),
10369

104-
excludeProcessor,
105-
excludeRulesProcessor,
70+
getExcludeProcessor(&cfg.Issues),
71+
getExcludeRulesProcessor(&cfg.Issues, log, lineCache),
10672
processors.NewNolint(log.Child("nolint"), dbManager, enabledLinters),
10773

10874
processors.NewUniqByLine(cfg),
109-
processors.NewDiff(icfg.Diff, icfg.DiffFromRevision, icfg.DiffPatchFilePath),
75+
processors.NewDiff(cfg.Issues.Diff, cfg.Issues.DiffFromRevision, cfg.Issues.DiffPatchFilePath),
11076
processors.NewMaxPerFileFromLinter(cfg),
111-
processors.NewMaxSameIssues(icfg.MaxSameIssues, log.Child("max_same_issues"), cfg),
112-
processors.NewMaxFromLinter(icfg.MaxIssuesPerLinter, log.Child("max_from_linter"), cfg),
77+
processors.NewMaxSameIssues(cfg.Issues.MaxSameIssues, log.Child("max_same_issues"), cfg),
78+
processors.NewMaxFromLinter(cfg.Issues.MaxIssuesPerLinter, log.Child("max_from_linter"), cfg),
11379
processors.NewSourceCode(lineCache, log.Child("source_code")),
11480
processors.NewPathShortener(),
81+
getSeverityRulesProcessor(&cfg.Severity, log, lineCache),
11582
},
11683
Log: log,
11784
}, nil
@@ -254,3 +221,89 @@ func (r *Runner) processIssues(issues []result.Issue, sw *timeutils.Stopwatch, s
254221

255222
return issues
256223
}
224+
225+
func getExcludeProcessor(cfg *config.Issues) processors.Processor {
226+
excludePatterns := cfg.ExcludePatterns
227+
if cfg.UseDefaultExcludes {
228+
excludePatterns = append(excludePatterns, config.GetExcludePatternsStrings(cfg.IncludeDefaultExcludes)...)
229+
}
230+
231+
var excludeTotalPattern string
232+
if len(excludePatterns) != 0 {
233+
excludeTotalPattern = fmt.Sprintf("(%s)", strings.Join(excludePatterns, "|"))
234+
}
235+
236+
var excludeProcessor processors.Processor
237+
if cfg.ExcludeCaseSensitive {
238+
excludeProcessor = processors.NewExcludeCaseSensitive(excludeTotalPattern)
239+
} else {
240+
excludeProcessor = processors.NewExclude(excludeTotalPattern)
241+
}
242+
243+
return excludeProcessor
244+
}
245+
246+
func getExcludeRulesProcessor(cfg *config.Issues, log logutils.Log, lineCache *fsutils.LineCache) processors.Processor {
247+
var excludeRules []processors.ExcludeRule
248+
for _, r := range cfg.ExcludeRules {
249+
excludeRules = append(excludeRules, processors.ExcludeRule{
250+
BaseRule: processors.BaseRule{
251+
Text: r.Text,
252+
Source: r.Source,
253+
Path: r.Path,
254+
Linters: r.Linters,
255+
},
256+
})
257+
}
258+
259+
var excludeRulesProcessor processors.Processor
260+
if cfg.ExcludeCaseSensitive {
261+
excludeRulesProcessor = processors.NewExcludeRulesCaseSensitive(
262+
excludeRules,
263+
lineCache,
264+
log.Child("exclude_rules"),
265+
)
266+
} else {
267+
excludeRulesProcessor = processors.NewExcludeRules(
268+
excludeRules,
269+
lineCache,
270+
log.Child("exclude_rules"),
271+
)
272+
}
273+
274+
return excludeRulesProcessor
275+
}
276+
277+
func getSeverityRulesProcessor(cfg *config.Severity, log logutils.Log, lineCache *fsutils.LineCache) processors.Processor {
278+
var severityRules []processors.SeverityRule
279+
for _, r := range cfg.Rules {
280+
severityRules = append(severityRules, processors.SeverityRule{
281+
Severity: r.Severity,
282+
BaseRule: processors.BaseRule{
283+
Text: r.Text,
284+
Source: r.Source,
285+
Path: r.Path,
286+
Linters: r.Linters,
287+
},
288+
})
289+
}
290+
291+
var severityRulesProcessor processors.Processor
292+
if cfg.CaseSensitive {
293+
severityRulesProcessor = processors.NewSeverityRulesCaseSensitive(
294+
cfg.Default,
295+
severityRules,
296+
lineCache,
297+
log.Child("severity_rules"),
298+
)
299+
} else {
300+
severityRulesProcessor = processors.NewSeverityRules(
301+
cfg.Default,
302+
severityRules,
303+
lineCache,
304+
log.Child("severity_rules"),
305+
)
306+
}
307+
308+
return severityRulesProcessor
309+
}

pkg/printers/checkstyle.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"encoding/xml"
66
"fmt"
77

8+
"github.com/go-xmlfmt/xmlfmt"
9+
810
"github.com/golangci/golangci-lint/pkg/logutils"
911
"github.com/golangci/golangci-lint/pkg/result"
1012
)
@@ -28,7 +30,7 @@ type checkstyleError struct {
2830
Source string `xml:"source,attr"`
2931
}
3032

31-
const defaultSeverity = "error"
33+
const defaultCheckstyleSeverity = "error"
3234

3335
type Checkstyle struct{}
3436

@@ -54,12 +56,17 @@ func (Checkstyle) Print(ctx context.Context, issues []result.Issue) error {
5456
files[issue.FilePath()] = file
5557
}
5658

59+
severity := defaultCheckstyleSeverity
60+
if issue.Severity != "" {
61+
severity = issue.Severity
62+
}
63+
5764
newError := &checkstyleError{
5865
Column: issue.Column(),
5966
Line: issue.Line(),
6067
Message: issue.Text,
6168
Source: issue.FromLinter,
62-
Severity: defaultSeverity,
69+
Severity: severity,
6370
}
6471

6572
file.Errors = append(file.Errors, newError)
@@ -75,6 +82,6 @@ func (Checkstyle) Print(ctx context.Context, issues []result.Issue) error {
7582
return err
7683
}
7784

78-
fmt.Fprintf(logutils.StdOut, "%s%s\n", xml.Header, data)
85+
fmt.Fprintf(logutils.StdOut, "%s%s\n", xml.Header, xmlfmt.FormatXML(string(data), "", " "))
7986
return nil
8087
}

0 commit comments

Comments
 (0)