193 lines
		
	
	
		
			5.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			193 lines
		
	
	
		
			5.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package golinters
 | |
| 
 | |
| import (
 | |
| 	"fmt"
 | |
| 	"go/ast"
 | |
| 	"go/types"
 | |
| 	"path/filepath"
 | |
| 	"reflect"
 | |
| 	"runtime"
 | |
| 	"sort"
 | |
| 	"strings"
 | |
| 	"sync"
 | |
| 
 | |
| 	gocriticlinter "github.com/go-critic/go-critic/framework/linter"
 | |
| 	"golang.org/x/tools/go/analysis"
 | |
| 
 | |
| 	"github.com/golangci/golangci-lint/pkg/config"
 | |
| 	"github.com/golangci/golangci-lint/pkg/golinters/goanalysis"
 | |
| 	"github.com/golangci/golangci-lint/pkg/lint/linter"
 | |
| 	"github.com/golangci/golangci-lint/pkg/result"
 | |
| )
 | |
| 
 | |
| const gocriticName = "gocritic"
 | |
| 
 | |
| func NewGocritic() *goanalysis.Linter {
 | |
| 	var mu sync.Mutex
 | |
| 	var resIssues []goanalysis.Issue
 | |
| 
 | |
| 	sizes := types.SizesFor("gc", runtime.GOARCH)
 | |
| 
 | |
| 	analyzer := &analysis.Analyzer{
 | |
| 		Name: gocriticName,
 | |
| 		Doc:  goanalysis.TheOnlyanalyzerDoc,
 | |
| 	}
 | |
| 	return goanalysis.NewLinter(
 | |
| 		gocriticName,
 | |
| 		`Provides diagnostics that check for bugs, performance and style issues.
 | |
| Extensible without recompilation through dynamic rules.
 | |
| Dynamic rules are written declaratively with AST patterns, filters, report message and optional suggestion.`,
 | |
| 		[]*analysis.Analyzer{analyzer},
 | |
| 		nil,
 | |
| 	).WithContextSetter(func(lintCtx *linter.Context) {
 | |
| 		analyzer.Run = func(pass *analysis.Pass) (interface{}, error) {
 | |
| 			linterCtx := gocriticlinter.NewContext(pass.Fset, sizes)
 | |
| 			enabledCheckers, err := buildEnabledCheckers(lintCtx, linterCtx)
 | |
| 			if err != nil {
 | |
| 				return nil, err
 | |
| 			}
 | |
| 
 | |
| 			linterCtx.SetPackageInfo(pass.TypesInfo, pass.Pkg)
 | |
| 			var res []goanalysis.Issue
 | |
| 			pkgIssues := runGocriticOnPackage(linterCtx, enabledCheckers, pass.Files)
 | |
| 			for i := range pkgIssues {
 | |
| 				res = append(res, goanalysis.NewIssue(&pkgIssues[i], pass))
 | |
| 			}
 | |
| 			if len(res) == 0 {
 | |
| 				return nil, nil
 | |
| 			}
 | |
| 
 | |
| 			mu.Lock()
 | |
| 			resIssues = append(resIssues, res...)
 | |
| 			mu.Unlock()
 | |
| 
 | |
| 			return nil, nil
 | |
| 		}
 | |
| 	}).WithIssuesReporter(func(*linter.Context) []goanalysis.Issue {
 | |
| 		return resIssues
 | |
| 	}).WithLoadMode(goanalysis.LoadModeTypesInfo)
 | |
| }
 | |
| 
 | |
| func normalizeCheckerInfoParams(info *gocriticlinter.CheckerInfo) gocriticlinter.CheckerParams {
 | |
| 	// lowercase info param keys here because golangci-lint's config parser lowercases all strings
 | |
| 	ret := gocriticlinter.CheckerParams{}
 | |
| 	for k, v := range info.Params {
 | |
| 		ret[strings.ToLower(k)] = v
 | |
| 	}
 | |
| 
 | |
| 	return ret
 | |
| }
 | |
| 
 | |
| func configureCheckerInfo(
 | |
| 	lintCtx *linter.Context,
 | |
| 	info *gocriticlinter.CheckerInfo,
 | |
| 	allParams map[string]config.GocriticCheckSettings) error {
 | |
| 	params := allParams[strings.ToLower(info.Name)]
 | |
| 	if params == nil { // no config for this checker
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	infoParams := normalizeCheckerInfoParams(info)
 | |
| 	for k, p := range params {
 | |
| 		v, ok := infoParams[k]
 | |
| 		if ok {
 | |
| 			v.Value = normalizeCheckerParamsValue(lintCtx, p)
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		// param `k` isn't supported
 | |
| 		if len(info.Params) == 0 {
 | |
| 			return fmt.Errorf("checker %s config param %s doesn't exist: checker doesn't have params",
 | |
| 				info.Name, k)
 | |
| 		}
 | |
| 
 | |
| 		var supportedKeys []string
 | |
| 		for sk := range info.Params {
 | |
| 			supportedKeys = append(supportedKeys, sk)
 | |
| 		}
 | |
| 		sort.Strings(supportedKeys)
 | |
| 
 | |
| 		return fmt.Errorf("checker %s config param %s doesn't exist, all existing: %s",
 | |
| 			info.Name, k, supportedKeys)
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // normalizeCheckerParamsValue normalizes value types.
 | |
| // go-critic asserts that CheckerParam.Value has some specific types,
 | |
| // but the file parsers (TOML, YAML, JSON) don't create the same representation for raw type.
 | |
| // then we have to convert value types into the expected value types.
 | |
| // Maybe in the future, this kind of conversion will be done in go-critic itself.
 | |
| func normalizeCheckerParamsValue(lintCtx *linter.Context, p interface{}) interface{} {
 | |
| 	rv := reflect.ValueOf(p)
 | |
| 	// nolint:exhaustive // only 3 types (int, bool, and string) are supported by CheckerParam.Value
 | |
| 	switch rv.Type().Kind() {
 | |
| 	case reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8, reflect.Int:
 | |
| 		return int(rv.Int())
 | |
| 	case reflect.Bool:
 | |
| 		return rv.Bool()
 | |
| 	case reflect.String:
 | |
| 		// Perform variable substitution.
 | |
| 		return strings.ReplaceAll(rv.String(), "${configDir}", lintCtx.Cfg.GetConfigDir())
 | |
| 	default:
 | |
| 		return p
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func buildEnabledCheckers(lintCtx *linter.Context, linterCtx *gocriticlinter.Context) ([]*gocriticlinter.Checker, error) {
 | |
| 	s := lintCtx.Settings().Gocritic
 | |
| 	allParams := s.GetLowercasedParams()
 | |
| 
 | |
| 	var enabledCheckers []*gocriticlinter.Checker
 | |
| 	for _, info := range gocriticlinter.GetCheckersInfo() {
 | |
| 		if !s.IsCheckEnabled(info.Name) {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		if err := configureCheckerInfo(lintCtx, info, allParams); err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 
 | |
| 		c, err := gocriticlinter.NewChecker(linterCtx, info)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		enabledCheckers = append(enabledCheckers, c)
 | |
| 	}
 | |
| 
 | |
| 	return enabledCheckers, nil
 | |
| }
 | |
| 
 | |
| func runGocriticOnPackage(linterCtx *gocriticlinter.Context, checkers []*gocriticlinter.Checker,
 | |
| 	files []*ast.File) []result.Issue {
 | |
| 	var res []result.Issue
 | |
| 	for _, f := range files {
 | |
| 		filename := filepath.Base(linterCtx.FileSet.Position(f.Pos()).Filename)
 | |
| 		linterCtx.SetFileInfo(filename, f)
 | |
| 
 | |
| 		issues := runGocriticOnFile(linterCtx, f, checkers)
 | |
| 		res = append(res, issues...)
 | |
| 	}
 | |
| 	return res
 | |
| }
 | |
| 
 | |
| func runGocriticOnFile(ctx *gocriticlinter.Context, f *ast.File, checkers []*gocriticlinter.Checker) []result.Issue {
 | |
| 	var res []result.Issue
 | |
| 
 | |
| 	for _, c := range checkers {
 | |
| 		// All checkers are expected to use *lint.Context
 | |
| 		// as read-only structure, so no copying is required.
 | |
| 		for _, warn := range c.Check(f) {
 | |
| 			pos := ctx.FileSet.Position(warn.Node.Pos())
 | |
| 			res = append(res, result.Issue{
 | |
| 				Pos:        pos,
 | |
| 				Text:       fmt.Sprintf("%s: %s", c.Info.Name, warn.Text),
 | |
| 				FromLinter: gocriticName,
 | |
| 			})
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return res
 | |
| }
 | 
