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  }