github.com/vanstinator/golangci-lint@v0.0.0-20240223191551-cc572f00d9d1/pkg/golinters/revive.go (about) 1 package golinters 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "go/token" 8 "os" 9 "reflect" 10 "sync" 11 12 "github.com/BurntSushi/toml" 13 reviveConfig "github.com/mgechev/revive/config" 14 "github.com/mgechev/revive/lint" 15 "github.com/mgechev/revive/rule" 16 "golang.org/x/tools/go/analysis" 17 18 "github.com/vanstinator/golangci-lint/pkg/config" 19 "github.com/vanstinator/golangci-lint/pkg/golinters/goanalysis" 20 "github.com/vanstinator/golangci-lint/pkg/lint/linter" 21 "github.com/vanstinator/golangci-lint/pkg/logutils" 22 "github.com/vanstinator/golangci-lint/pkg/result" 23 ) 24 25 const reviveName = "revive" 26 27 var reviveDebugf = logutils.Debug(logutils.DebugKeyRevive) 28 29 // jsonObject defines a JSON object of a failure 30 type jsonObject struct { 31 Severity lint.Severity 32 lint.Failure `json:",inline"` 33 } 34 35 // NewRevive returns a new Revive linter. 36 // 37 38 func NewRevive(settings *config.ReviveSettings) *goanalysis.Linter { 39 var mu sync.Mutex 40 var resIssues []goanalysis.Issue 41 42 analyzer := &analysis.Analyzer{ 43 Name: goanalysis.TheOnlyAnalyzerName, 44 Doc: goanalysis.TheOnlyanalyzerDoc, 45 Run: goanalysis.DummyRun, 46 } 47 48 return goanalysis.NewLinter( 49 reviveName, 50 "Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint.", 51 []*analysis.Analyzer{analyzer}, 52 nil, 53 ).WithContextSetter(func(lintCtx *linter.Context) { 54 analyzer.Run = func(pass *analysis.Pass) (any, error) { 55 issues, err := runRevive(lintCtx, pass, settings) 56 if err != nil { 57 return nil, err 58 } 59 60 if len(issues) == 0 { 61 return nil, nil 62 } 63 64 mu.Lock() 65 resIssues = append(resIssues, issues...) 66 mu.Unlock() 67 68 return nil, nil 69 } 70 }).WithIssuesReporter(func(*linter.Context) []goanalysis.Issue { 71 return resIssues 72 }).WithLoadMode(goanalysis.LoadModeSyntax) 73 } 74 75 func runRevive(lintCtx *linter.Context, pass *analysis.Pass, settings *config.ReviveSettings) ([]goanalysis.Issue, error) { 76 packages := [][]string{getFileNames(pass)} 77 78 conf, err := getReviveConfig(settings) 79 if err != nil { 80 return nil, err 81 } 82 83 formatter, err := reviveConfig.GetFormatter("json") 84 if err != nil { 85 return nil, err 86 } 87 88 revive := lint.New(os.ReadFile, settings.MaxOpenFiles) 89 90 lintingRules, err := reviveConfig.GetLintingRules(conf, []lint.Rule{}) 91 if err != nil { 92 return nil, err 93 } 94 95 failures, err := revive.Lint(packages, lintingRules, *conf) 96 if err != nil { 97 return nil, err 98 } 99 100 formatChan := make(chan lint.Failure) 101 exitChan := make(chan bool) 102 103 var output string 104 go func() { 105 output, err = formatter.Format(formatChan, *conf) 106 if err != nil { 107 lintCtx.Log.Errorf("Format error: %v", err) 108 } 109 exitChan <- true 110 }() 111 112 for f := range failures { 113 if f.Confidence < conf.Confidence { 114 continue 115 } 116 117 formatChan <- f 118 } 119 120 close(formatChan) 121 <-exitChan 122 123 var results []jsonObject 124 err = json.Unmarshal([]byte(output), &results) 125 if err != nil { 126 return nil, err 127 } 128 129 var issues []goanalysis.Issue 130 for i := range results { 131 issues = append(issues, reviveToIssue(pass, &results[i])) 132 } 133 134 return issues, nil 135 } 136 137 func reviveToIssue(pass *analysis.Pass, object *jsonObject) goanalysis.Issue { 138 lineRangeTo := object.Position.End.Line 139 if object.RuleName == (&rule.ExportedRule{}).Name() { 140 lineRangeTo = object.Position.Start.Line 141 } 142 143 return goanalysis.NewIssue(&result.Issue{ 144 Severity: string(object.Severity), 145 Text: fmt.Sprintf("%s: %s", object.RuleName, object.Failure.Failure), 146 Pos: token.Position{ 147 Filename: object.Position.Start.Filename, 148 Line: object.Position.Start.Line, 149 Offset: object.Position.Start.Offset, 150 Column: object.Position.Start.Column, 151 }, 152 LineRange: &result.Range{ 153 From: object.Position.Start.Line, 154 To: lineRangeTo, 155 }, 156 FromLinter: reviveName, 157 }, pass) 158 } 159 160 // This function mimics the GetConfig function of revive. 161 // This allows to get default values and right types. 162 // https://github.com/vanstinator/golangci-lint/issues/1745 163 // https://github.com/mgechev/revive/blob/v1.1.4/config/config.go#L182 164 func getReviveConfig(cfg *config.ReviveSettings) (*lint.Config, error) { 165 conf := defaultConfig() 166 167 if !reflect.DeepEqual(cfg, &config.ReviveSettings{}) { 168 rawRoot := createConfigMap(cfg) 169 buf := bytes.NewBuffer(nil) 170 171 err := toml.NewEncoder(buf).Encode(rawRoot) 172 if err != nil { 173 return nil, fmt.Errorf("failed to encode configuration: %w", err) 174 } 175 176 conf = &lint.Config{} 177 _, err = toml.NewDecoder(buf).Decode(conf) 178 if err != nil { 179 return nil, fmt.Errorf("failed to decode configuration: %w", err) 180 } 181 } 182 183 normalizeConfig(conf) 184 185 reviveDebugf("revive configuration: %#v", conf) 186 187 return conf, nil 188 } 189 190 func createConfigMap(cfg *config.ReviveSettings) map[string]any { 191 rawRoot := map[string]any{ 192 "ignoreGeneratedHeader": cfg.IgnoreGeneratedHeader, 193 "confidence": cfg.Confidence, 194 "severity": cfg.Severity, 195 "errorCode": cfg.ErrorCode, 196 "warningCode": cfg.WarningCode, 197 "enableAllRules": cfg.EnableAllRules, 198 } 199 200 rawDirectives := map[string]map[string]any{} 201 for _, directive := range cfg.Directives { 202 rawDirectives[directive.Name] = map[string]any{ 203 "severity": directive.Severity, 204 } 205 } 206 207 if len(rawDirectives) > 0 { 208 rawRoot["directive"] = rawDirectives 209 } 210 211 rawRules := map[string]map[string]any{} 212 for _, s := range cfg.Rules { 213 rawRules[s.Name] = map[string]any{ 214 "severity": s.Severity, 215 "arguments": safeTomlSlice(s.Arguments), 216 "disabled": s.Disabled, 217 } 218 } 219 220 if len(rawRules) > 0 { 221 rawRoot["rule"] = rawRules 222 } 223 224 return rawRoot 225 } 226 227 func safeTomlSlice(r []any) []any { 228 if len(r) == 0 { 229 return nil 230 } 231 232 if _, ok := r[0].(map[any]any); !ok { 233 return r 234 } 235 236 var typed []any 237 for _, elt := range r { 238 item := map[string]any{} 239 for k, v := range elt.(map[any]any) { 240 item[k.(string)] = v 241 } 242 243 typed = append(typed, item) 244 } 245 246 return typed 247 } 248 249 // This element is not exported by revive, so we need copy the code. 250 // Extracted from https://github.com/mgechev/revive/blob/v1.3.5/config/config.go#L15 251 var defaultRules = []lint.Rule{ 252 &rule.VarDeclarationsRule{}, 253 &rule.PackageCommentsRule{}, 254 &rule.DotImportsRule{}, 255 &rule.BlankImportsRule{}, 256 &rule.ExportedRule{}, 257 &rule.VarNamingRule{}, 258 &rule.IndentErrorFlowRule{}, 259 &rule.RangeRule{}, 260 &rule.ErrorfRule{}, 261 &rule.ErrorNamingRule{}, 262 &rule.ErrorStringsRule{}, 263 &rule.ReceiverNamingRule{}, 264 &rule.IncrementDecrementRule{}, 265 &rule.ErrorReturnRule{}, 266 &rule.UnexportedReturnRule{}, 267 &rule.TimeNamingRule{}, 268 &rule.ContextKeysType{}, 269 &rule.ContextAsArgumentRule{}, 270 &rule.EmptyBlockRule{}, 271 &rule.SuperfluousElseRule{}, 272 &rule.UnusedParamRule{}, 273 &rule.UnreachableCodeRule{}, 274 &rule.RedefinesBuiltinIDRule{}, 275 } 276 277 var allRules = append([]lint.Rule{ 278 &rule.ArgumentsLimitRule{}, 279 &rule.CyclomaticRule{}, 280 &rule.FileHeaderRule{}, 281 &rule.ConfusingNamingRule{}, 282 &rule.GetReturnRule{}, 283 &rule.ModifiesParamRule{}, 284 &rule.ConfusingResultsRule{}, 285 &rule.DeepExitRule{}, 286 &rule.AddConstantRule{}, 287 &rule.FlagParamRule{}, 288 &rule.UnnecessaryStmtRule{}, 289 &rule.StructTagRule{}, 290 &rule.ModifiesValRecRule{}, 291 &rule.ConstantLogicalExprRule{}, 292 &rule.BoolLiteralRule{}, 293 &rule.ImportsBlacklistRule{}, 294 &rule.FunctionResultsLimitRule{}, 295 &rule.MaxPublicStructsRule{}, 296 &rule.RangeValInClosureRule{}, 297 &rule.RangeValAddress{}, 298 &rule.WaitGroupByValueRule{}, 299 &rule.AtomicRule{}, 300 &rule.EmptyLinesRule{}, 301 &rule.LineLengthLimitRule{}, 302 &rule.CallToGCRule{}, 303 &rule.DuplicatedImportsRule{}, 304 &rule.ImportShadowingRule{}, 305 &rule.BareReturnRule{}, 306 &rule.UnusedReceiverRule{}, 307 &rule.UnhandledErrorRule{}, 308 &rule.CognitiveComplexityRule{}, 309 &rule.StringOfIntRule{}, 310 &rule.StringFormatRule{}, 311 &rule.EarlyReturnRule{}, 312 &rule.UnconditionalRecursionRule{}, 313 &rule.IdenticalBranchesRule{}, 314 &rule.DeferRule{}, 315 &rule.UnexportedNamingRule{}, 316 &rule.FunctionLength{}, 317 &rule.NestedStructs{}, 318 &rule.UselessBreak{}, 319 &rule.UncheckedTypeAssertionRule{}, 320 &rule.TimeEqualRule{}, 321 &rule.BannedCharsRule{}, 322 &rule.OptimizeOperandsOrderRule{}, 323 &rule.UseAnyRule{}, 324 &rule.DataRaceRule{}, 325 &rule.CommentSpacingsRule{}, 326 &rule.IfReturnRule{}, 327 &rule.RedundantImportAlias{}, 328 &rule.ImportAliasNamingRule{}, 329 &rule.EnforceMapStyleRule{}, 330 &rule.EnforceRepeatedArgTypeStyleRule{}, 331 &rule.EnforceSliceStyleRule{}, 332 }, defaultRules...) 333 334 const defaultConfidence = 0.8 335 336 // This element is not exported by revive, so we need copy the code. 337 // Extracted from https://github.com/mgechev/revive/blob/v1.1.4/config/config.go#L145 338 func normalizeConfig(cfg *lint.Config) { 339 // NOTE(ldez): this custom section for golangci-lint should be kept. 340 // --- 341 if cfg.Confidence == 0 { 342 cfg.Confidence = defaultConfidence 343 } 344 if cfg.Severity == "" { 345 cfg.Severity = lint.SeverityWarning 346 } 347 // --- 348 349 if len(cfg.Rules) == 0 { 350 cfg.Rules = map[string]lint.RuleConfig{} 351 } 352 if cfg.EnableAllRules { 353 // Add to the configuration all rules not yet present in it 354 for _, r := range allRules { 355 ruleName := r.Name() 356 _, alreadyInConf := cfg.Rules[ruleName] 357 if alreadyInConf { 358 continue 359 } 360 // Add the rule with an empty conf for 361 cfg.Rules[ruleName] = lint.RuleConfig{} 362 } 363 } 364 365 severity := cfg.Severity 366 if severity != "" { 367 for k, v := range cfg.Rules { 368 if v.Severity == "" { 369 v.Severity = severity 370 } 371 cfg.Rules[k] = v 372 } 373 for k, v := range cfg.Directives { 374 if v.Severity == "" { 375 v.Severity = severity 376 } 377 cfg.Directives[k] = v 378 } 379 } 380 } 381 382 // This element is not exported by revive, so we need copy the code. 383 // Extracted from https://github.com/mgechev/revive/blob/v1.1.4/config/config.go#L214 384 func defaultConfig() *lint.Config { 385 defaultConfig := lint.Config{ 386 Confidence: defaultConfidence, 387 Severity: lint.SeverityWarning, 388 Rules: map[string]lint.RuleConfig{}, 389 } 390 for _, r := range defaultRules { 391 defaultConfig.Rules[r.Name()] = lint.RuleConfig{} 392 } 393 return &defaultConfig 394 }