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 }