github.com/elek/golangci-lint@v1.42.2-0.20211208090441-c05b7fcb3a9a/pkg/golinters/gocritic.go (about)

     1  package golinters
     2  
     3  import (
     4  	"fmt"
     5  	"go/ast"
     6  	"go/types"
     7  	"path/filepath"
     8  	"reflect"
     9  	"runtime"
    10  	"sort"
    11  	"strings"
    12  	"sync"
    13  
    14  	gocriticlinter "github.com/go-critic/go-critic/framework/linter"
    15  	"golang.org/x/tools/go/analysis"
    16  
    17  	"github.com/elek/golangci-lint/pkg/config"
    18  	"github.com/elek/golangci-lint/pkg/golinters/goanalysis"
    19  	"github.com/elek/golangci-lint/pkg/lint/linter"
    20  	"github.com/elek/golangci-lint/pkg/result"
    21  )
    22  
    23  const gocriticName = "gocritic"
    24  
    25  func NewGocritic() *goanalysis.Linter {
    26  	var mu sync.Mutex
    27  	var resIssues []goanalysis.Issue
    28  
    29  	sizes := types.SizesFor("gc", runtime.GOARCH)
    30  
    31  	analyzer := &analysis.Analyzer{
    32  		Name: gocriticName,
    33  		Doc:  goanalysis.TheOnlyanalyzerDoc,
    34  	}
    35  	return goanalysis.NewLinter(
    36  		gocriticName,
    37  		`Provides many diagnostics that check for bugs, performance and style issues.
    38  Extensible without recompilation through dynamic rules.
    39  Dynamic rules are written declaratively with AST patterns, filters, report message and optional suggestion.`,
    40  		[]*analysis.Analyzer{analyzer},
    41  		nil,
    42  	).WithContextSetter(func(lintCtx *linter.Context) {
    43  		analyzer.Run = func(pass *analysis.Pass) (interface{}, error) {
    44  			linterCtx := gocriticlinter.NewContext(pass.Fset, sizes)
    45  			enabledCheckers, err := buildEnabledCheckers(lintCtx, linterCtx)
    46  			if err != nil {
    47  				return nil, err
    48  			}
    49  
    50  			linterCtx.SetPackageInfo(pass.TypesInfo, pass.Pkg)
    51  			var res []goanalysis.Issue
    52  			pkgIssues := runGocriticOnPackage(linterCtx, enabledCheckers, pass.Files)
    53  			for i := range pkgIssues {
    54  				res = append(res, goanalysis.NewIssue(&pkgIssues[i], pass))
    55  			}
    56  			if len(res) == 0 {
    57  				return nil, nil
    58  			}
    59  
    60  			mu.Lock()
    61  			resIssues = append(resIssues, res...)
    62  			mu.Unlock()
    63  
    64  			return nil, nil
    65  		}
    66  	}).WithIssuesReporter(func(*linter.Context) []goanalysis.Issue {
    67  		return resIssues
    68  	}).WithLoadMode(goanalysis.LoadModeTypesInfo)
    69  }
    70  
    71  func normalizeCheckerInfoParams(info *gocriticlinter.CheckerInfo) gocriticlinter.CheckerParams {
    72  	// lowercase info param keys here because golangci-lint's config parser lowercases all strings
    73  	ret := gocriticlinter.CheckerParams{}
    74  	for k, v := range info.Params {
    75  		ret[strings.ToLower(k)] = v
    76  	}
    77  
    78  	return ret
    79  }
    80  
    81  func configureCheckerInfo(info *gocriticlinter.CheckerInfo, allParams map[string]config.GocriticCheckSettings) error {
    82  	params := allParams[strings.ToLower(info.Name)]
    83  	if params == nil { // no config for this checker
    84  		return nil
    85  	}
    86  
    87  	infoParams := normalizeCheckerInfoParams(info)
    88  	for k, p := range params {
    89  		v, ok := infoParams[k]
    90  		if ok {
    91  			v.Value = normalizeCheckerParamsValue(p)
    92  			continue
    93  		}
    94  
    95  		// param `k` isn't supported
    96  		if len(info.Params) == 0 {
    97  			return fmt.Errorf("checker %s config param %s doesn't exist: checker doesn't have params",
    98  				info.Name, k)
    99  		}
   100  
   101  		var supportedKeys []string
   102  		for sk := range info.Params {
   103  			supportedKeys = append(supportedKeys, sk)
   104  		}
   105  		sort.Strings(supportedKeys)
   106  
   107  		return fmt.Errorf("checker %s config param %s doesn't exist, all existing: %s",
   108  			info.Name, k, supportedKeys)
   109  	}
   110  
   111  	return nil
   112  }
   113  
   114  // normalizeCheckerParamsValue normalizes value types.
   115  // go-critic asserts that CheckerParam.Value has some specific types,
   116  // but the file parsers (TOML, YAML, JSON) don't create the same representation for raw type.
   117  // then we have to convert value types into the expected value types.
   118  // Maybe in the future, this kind of conversion will be done in go-critic itself.
   119  //nolint:exhaustive // only 3 types (int, bool, and string) are supported by CheckerParam.Value
   120  func normalizeCheckerParamsValue(p interface{}) interface{} {
   121  	rv := reflect.ValueOf(p)
   122  	switch rv.Type().Kind() {
   123  	case reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8, reflect.Int:
   124  		return int(rv.Int())
   125  	case reflect.Bool:
   126  		return rv.Bool()
   127  	case reflect.String:
   128  		return rv.String()
   129  	default:
   130  		return p
   131  	}
   132  }
   133  
   134  func buildEnabledCheckers(lintCtx *linter.Context, linterCtx *gocriticlinter.Context) ([]*gocriticlinter.Checker, error) {
   135  	s := lintCtx.Settings().Gocritic
   136  	allParams := s.GetLowercasedParams()
   137  
   138  	var enabledCheckers []*gocriticlinter.Checker
   139  	for _, info := range gocriticlinter.GetCheckersInfo() {
   140  		if !s.IsCheckEnabled(info.Name) {
   141  			continue
   142  		}
   143  
   144  		if err := configureCheckerInfo(info, allParams); err != nil {
   145  			return nil, err
   146  		}
   147  
   148  		c, err := gocriticlinter.NewChecker(linterCtx, info)
   149  		if err != nil {
   150  			return nil, err
   151  		}
   152  		enabledCheckers = append(enabledCheckers, c)
   153  	}
   154  
   155  	return enabledCheckers, nil
   156  }
   157  
   158  func runGocriticOnPackage(linterCtx *gocriticlinter.Context, checkers []*gocriticlinter.Checker,
   159  	files []*ast.File) []result.Issue {
   160  	var res []result.Issue
   161  	for _, f := range files {
   162  		filename := filepath.Base(linterCtx.FileSet.Position(f.Pos()).Filename)
   163  		linterCtx.SetFileInfo(filename, f)
   164  
   165  		issues := runGocriticOnFile(linterCtx, f, checkers)
   166  		res = append(res, issues...)
   167  	}
   168  	return res
   169  }
   170  
   171  func runGocriticOnFile(ctx *gocriticlinter.Context, f *ast.File, checkers []*gocriticlinter.Checker) []result.Issue {
   172  	var res []result.Issue
   173  
   174  	for _, c := range checkers {
   175  		// All checkers are expected to use *lint.Context
   176  		// as read-only structure, so no copying is required.
   177  		for _, warn := range c.Check(f) {
   178  			pos := ctx.FileSet.Position(warn.Node.Pos())
   179  			res = append(res, result.Issue{
   180  				Pos:        pos,
   181  				Text:       fmt.Sprintf("%s: %s", c.Info.Name, warn.Text),
   182  				FromLinter: gocriticName,
   183  			})
   184  		}
   185  	}
   186  
   187  	return res
   188  }