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