1package check
2
3import (
4	"errors"
5	"flag"
6	"fmt"
7	"go/ast"
8	"go/build"
9	"go/token"
10	"go/types"
11	"log"
12	"os"
13	"path/filepath"
14	"regexp"
15	"runtime"
16	"sort"
17	"strings"
18	"sync"
19
20	"github.com/go-critic/go-critic/framework/linter"
21	"github.com/go-critic/go-critic/framework/lintmain/internal/hotload"
22	"github.com/go-toolsmith/pkgload"
23	"github.com/logrusorgru/aurora"
24	"golang.org/x/tools/go/packages"
25)
26
27// Main implements sub-command entry point.
28func Main() {
29	var p program
30	p.infoList = linter.GetCheckersInfo()
31
32	steps := []struct {
33		name string
34		fn   func() error
35	}{
36		{"load plugin", p.loadPlugin},
37		{"bind checker params", p.bindCheckerParams},
38		{"bind default enabled list", p.bindDefaultEnabledList},
39		{"parse args", p.parseArgs},
40		{"assign checker params", p.assignCheckerParams},
41		{"load program", p.loadProgram},
42		{"init checkers", p.initCheckers},
43		{"run checkers", p.runCheckers},
44		{"exit if found issues", p.exit},
45	}
46
47	for _, step := range steps {
48		if err := step.fn(); err != nil {
49			log.Fatalf("%s: %v", step.name, err)
50		}
51	}
52}
53
54type program struct {
55	ctx *linter.Context
56
57	fset *token.FileSet
58
59	loadedPackages []*packages.Package
60
61	infoList []*linter.CheckerInfo
62
63	checkers []*linter.Checker
64
65	packages []string
66
67	foundIssues bool
68
69	checkerParams boundCheckerParams
70
71	filters struct {
72		enableAll       bool
73		enable          []string
74		disable         []string
75		defaultCheckers []string
76	}
77
78	workDir string
79	gopath  string
80	goroot  string
81
82	exitCode           int
83	checkTests         bool
84	checkGenerated     bool
85	shorterErrLocation bool
86	coloredOutput      bool
87	verbose            bool
88}
89
90func (p *program) exit() error {
91	if p.foundIssues {
92		os.Exit(p.exitCode)
93	}
94	return nil
95}
96
97func (p *program) runCheckers() error {
98	for _, pkg := range p.loadedPackages {
99		if p.verbose {
100			log.Printf("\tdebug: checking %q package (%d files)",
101				pkg.String(), len(pkg.Syntax))
102		}
103		p.checkPackage(pkg)
104	}
105
106	return nil
107}
108
109func (p *program) checkPackage(pkg *packages.Package) {
110	p.ctx.SetPackageInfo(pkg.TypesInfo, pkg.Types)
111	for _, f := range pkg.Syntax {
112		filename := p.getFilename(f)
113		if !p.checkTests && strings.HasSuffix(filename, "_test.go") {
114			continue
115		}
116		if !p.checkGenerated && p.isGenerated(f) {
117			continue
118		}
119		p.ctx.SetFileInfo(filename, f)
120		p.checkFile(f)
121	}
122}
123
124func (p *program) checkFile(f *ast.File) {
125	warnings := make([][]linter.Warning, len(p.checkers))
126
127	var wg sync.WaitGroup
128	wg.Add(len(p.checkers))
129	for i, c := range p.checkers {
130		// All checkers are expected to use *lint.Context
131		// as read-only structure, so no copying is required.
132		go func(i int, c *linter.Checker) {
133			defer func() {
134				wg.Done()
135				// Checker signals unexpected error with panic(error).
136				r := recover()
137				if r == nil {
138					return // There were no panic
139				}
140				if err, ok := r.(error); ok {
141					log.Printf("%s: error: %v\n", c.Info.Name, err)
142					panic(err)
143				} else {
144					// Some other kind of run-time panic.
145					// Undo the recover and resume panic.
146					panic(r)
147				}
148			}()
149
150			warnings[i] = append(warnings[i], c.Check(f)...)
151		}(i, c)
152	}
153	wg.Wait()
154
155	for i, c := range p.checkers {
156		for _, warn := range warnings[i] {
157			p.foundIssues = true
158			loc := p.ctx.FileSet.Position(warn.Node.Pos()).String()
159			if p.shorterErrLocation {
160				loc = p.shortenLocation(loc)
161			}
162			printWarning(p, c.Info.Name, loc, warn.Text)
163		}
164	}
165
166}
167
168func (p *program) initCheckers() error {
169	parseKeys := func(keys []string, byName, byTag map[string]bool) {
170		for _, key := range keys {
171			if strings.HasPrefix(key, "#") {
172				byTag[key[len("#"):]] = true
173			} else {
174				byName[key] = true
175			}
176		}
177	}
178
179	enabledByName := make(map[string]bool)
180	enabledTags := make(map[string]bool)
181	parseKeys(p.filters.enable, enabledByName, enabledTags)
182	disabledByName := make(map[string]bool)
183	disabledTags := make(map[string]bool)
184	parseKeys(p.filters.disable, disabledByName, disabledTags)
185
186	enabledByTag := func(info *linter.CheckerInfo) bool {
187		for _, tag := range info.Tags {
188			if enabledTags[tag] {
189				return true
190			}
191		}
192		return false
193	}
194	disabledByTag := func(info *linter.CheckerInfo) string {
195		for _, tag := range info.Tags {
196			if disabledTags[tag] {
197				return tag
198			}
199		}
200		return ""
201	}
202
203	for _, info := range p.infoList {
204		enabled := p.filters.enableAll ||
205			enabledByName[info.Name] ||
206			enabledByTag(info)
207		notice := ""
208
209		switch {
210		case !enabled:
211			notice = "not enabled by name or tag (-enable)"
212		case disabledByName[info.Name]:
213			enabled = false
214			notice = "disabled by name (-disable)"
215		default:
216			if tag := disabledByTag(info); tag != "" {
217				enabled = false
218				notice = fmt.Sprintf("disabled by %q tag (-disable)", tag)
219			}
220		}
221
222		if p.verbose && !enabled {
223			log.Printf("\tdebug: %s: %s", info.Name, notice)
224		}
225		if enabled {
226			p.checkers = append(p.checkers, linter.NewChecker(p.ctx, info))
227		}
228	}
229	if p.verbose {
230		for _, c := range p.checkers {
231			log.Printf("\tdebug: %s is enabled", c.Info.Name)
232		}
233	}
234
235	if len(p.checkers) == 0 {
236		return errors.New("empty checkers set selected")
237	}
238	return nil
239}
240
241func (p *program) loadProgram() error {
242	sizes := types.SizesFor("gc", runtime.GOARCH)
243	if sizes == nil {
244		return fmt.Errorf("can't find sizes info for %s", runtime.GOARCH)
245	}
246
247	p.fset = token.NewFileSet()
248	mode := packages.NeedName |
249		packages.NeedFiles |
250		packages.NeedCompiledGoFiles |
251		packages.NeedImports |
252		packages.NeedTypes |
253		packages.NeedSyntax |
254		packages.NeedTypesInfo |
255		packages.NeedTypesSizes
256	cfg := packages.Config{
257		Mode:  mode,
258		Tests: true,
259		Fset:  p.fset,
260	}
261	pkgs, err := loadPackages(&cfg, p.packages)
262	if err != nil {
263		log.Fatalf("load packages: %v", err)
264	}
265	sort.SliceStable(pkgs, func(i, j int) bool {
266		return pkgs[i].PkgPath < pkgs[j].PkgPath
267	})
268
269	p.loadedPackages = pkgs
270	p.ctx = linter.NewContext(p.fset, sizes)
271
272	return nil
273}
274
275func (p *program) loadPlugin() error {
276	const pluginFilename = "gocritic-plugin.so"
277	if _, err := os.Stat(pluginFilename); os.IsNotExist(err) {
278		return nil
279	}
280	infoList, err := hotload.CheckersFromDylib(p.infoList, pluginFilename)
281	p.infoList = infoList
282	return err
283}
284
285type boundCheckerParams struct {
286	ints    map[string]*int
287	bools   map[string]*bool
288	strings map[string]*string
289}
290
291// bindCheckerParams registers command-line flags for every checker parameter.
292func (p *program) bindCheckerParams() error {
293	intParams := make(map[string]*int)
294	boolParams := make(map[string]*bool)
295	stringParams := make(map[string]*string)
296
297	for _, info := range p.infoList {
298		for pname, param := range info.Params {
299			key := p.checkerParamKey(info, pname)
300			switch v := param.Value.(type) {
301			case int:
302				intParams[key] = flag.Int(key, v, param.Usage)
303			case bool:
304				boolParams[key] = flag.Bool(key, v, param.Usage)
305			case string:
306				stringParams[key] = flag.String(key, v, param.Usage)
307			default:
308				panic("unreachable") // Checked in AddChecker
309			}
310		}
311	}
312
313	p.checkerParams.ints = intParams
314	p.checkerParams.bools = boolParams
315	p.checkerParams.strings = stringParams
316
317	return nil
318}
319
320func (p *program) checkerParamKey(info *linter.CheckerInfo, pname string) string {
321	return "@" + info.Name + "." + pname
322}
323
324// bindDefaultEnabledList calculates the default value for -enable param.
325func (p *program) bindDefaultEnabledList() error {
326	var enabled []string
327	for _, info := range p.infoList {
328		enable := !info.HasTag("experimental") &&
329			!info.HasTag("opinionated") &&
330			!info.HasTag("performance") &&
331			!info.HasTag("security")
332		if enable {
333			enabled = append(enabled, info.Name)
334		}
335	}
336	p.filters.defaultCheckers = enabled
337	return nil
338}
339
340func (p *program) parseArgs() error {
341	flag.BoolVar(&p.filters.enableAll, "enableAll", false,
342		`identical to -enable with all checkers listed. If true, -enable is ignored`)
343	enable := flag.String("enable", strings.Join(p.filters.defaultCheckers, ","),
344		`comma-separated list of enabled checkers. Can include #tags`)
345	disable := flag.String("disable", "",
346		`comma-separated list of checkers to be disabled. Can include #tags`)
347	flag.IntVar(&p.exitCode, "exitCode", 1,
348		`exit code to be used when lint issues are found`)
349	flag.BoolVar(&p.checkTests, "checkTests", true,
350		`whether to check test files`)
351	flag.BoolVar(&p.shorterErrLocation, `shorterErrLocation`, true,
352		`whether to replace error location prefix with $GOROOT and $GOPATH`)
353	flag.BoolVar(&p.coloredOutput, `coloredOutput`, false,
354		`whether to use colored output`)
355	flag.BoolVar(&p.verbose, "v", false,
356		`whether to print output useful during linter debugging`)
357
358	flag.Parse()
359
360	p.packages = flag.Args()
361	p.filters.enable = strings.Split(*enable, ",")
362	p.filters.disable = strings.Split(*disable, ",")
363
364	if p.shorterErrLocation {
365		wd, err := os.Getwd()
366		if err != nil {
367			log.Printf("getwd: %v", err)
368		}
369		p.workDir = addTrailingSlash(wd)
370		p.gopath = addTrailingSlash(build.Default.GOPATH)
371		p.goroot = addTrailingSlash(build.Default.GOROOT)
372	}
373
374	return nil
375}
376
377func addTrailingSlash(s string) string {
378	if strings.HasSuffix(s, string(os.PathSeparator)) {
379		return s
380	}
381	return s + string(os.PathSeparator)
382}
383
384// assignCheckerParams initializes checker parameter values using
385// values that are coming from the command-line arguments.
386func (p *program) assignCheckerParams() error {
387	intParams := p.checkerParams.ints
388	boolParams := p.checkerParams.bools
389	stringParams := p.checkerParams.strings
390
391	for _, info := range p.infoList {
392		for pname, param := range info.Params {
393			key := p.checkerParamKey(info, pname)
394			switch param.Value.(type) {
395			case int:
396				info.Params[pname].Value = *intParams[key]
397			case bool:
398				info.Params[pname].Value = *boolParams[key]
399			case string:
400				info.Params[pname].Value = *stringParams[key]
401			default:
402				panic("unreachable") // Checked in AddChecker
403			}
404		}
405	}
406
407	return nil
408}
409
410var generatedFileCommentRE = regexp.MustCompile("Code generated .* DO NOT EDIT.")
411
412func (p *program) isGenerated(f *ast.File) bool {
413	return len(f.Comments) != 0 &&
414		generatedFileCommentRE.MatchString(f.Comments[0].Text())
415}
416
417func (p *program) getFilename(f *ast.File) string {
418	// See https://github.com/golang/go/issues/24498.
419	return filepath.Base(p.fset.Position(f.Pos()).Filename)
420}
421
422func (p *program) shortenLocation(loc string) string {
423	// If possible, construct relative path.
424	relLoc := loc
425	if p.workDir != "" {
426		relLoc = strings.Replace(loc, p.workDir, "./", 1)
427	}
428
429	switch {
430	case strings.HasPrefix(loc, p.gopath):
431		loc = strings.Replace(loc, p.gopath, "$GOPATH"+string(os.PathSeparator), 1)
432	case strings.HasPrefix(loc, p.goroot):
433		loc = strings.Replace(loc, p.goroot, "$GOROOT"+string(os.PathSeparator), 1)
434	}
435
436	// Return the representation that is shorter.
437	if len(relLoc) < len(loc) {
438		return relLoc
439	}
440	return loc
441}
442
443func printWarning(p *program, rule, loc, warn string) {
444	switch {
445	case p.coloredOutput:
446		log.Printf("%v: %v: %v\n",
447			aurora.Magenta(aurora.Bold(loc)),
448			aurora.Red(rule),
449			warn)
450
451	default:
452		log.Printf("%s: %s: %s\n", loc, rule, warn)
453	}
454}
455
456func loadPackages(cfg *packages.Config, patterns []string) ([]*packages.Package, error) {
457	pkgs, err := packages.Load(cfg, patterns...)
458	if err != nil {
459		return nil, err
460	}
461
462	result := pkgs[:0]
463	pkgload.VisitUnits(pkgs, func(u *pkgload.Unit) {
464		if u.ExternalTest != nil {
465			result = append(result, u.ExternalTest)
466		}
467
468		if u.Test != nil {
469			// Prefer tests to the base package, if present.
470			result = append(result, u.Test)
471		} else {
472			result = append(result, u.Base)
473		}
474	})
475	return result, nil
476}
477