1 // Package lint provides the foundation for tools like staticcheck
2 package lint // import "honnef.co/go/tools/lint"
18 "golang.org/x/tools/go/analysis"
19 "golang.org/x/tools/go/packages"
20 "honnef.co/go/tools/config"
21 "honnef.co/go/tools/internal/cache"
24 type Documentation struct {
32 func (doc *Documentation) String() string {
33 b := &strings.Builder{}
34 fmt.Fprintf(b, "%s\n\n", doc.Title)
36 fmt.Fprintf(b, "%s\n\n", doc.Text)
38 fmt.Fprint(b, "Available since\n ")
40 fmt.Fprint(b, "unreleased")
42 fmt.Fprintf(b, "%s", doc.Since)
45 fmt.Fprint(b, ", non-default")
48 if len(doc.Options) > 0 {
49 fmt.Fprintf(b, "\nOptions\n")
50 for _, opt := range doc.Options {
51 fmt.Fprintf(b, " %s", opt)
58 type Ignore interface {
62 type LineIgnore struct {
70 func (li *LineIgnore) Match(p Problem) bool {
72 if pos.Filename != li.File || pos.Line != li.Line {
75 for _, c := range li.Checks {
76 if m, _ := filepath.Match(c, p.Check); m {
84 func (li *LineIgnore) String() string {
85 matched := "not matched"
89 return fmt.Sprintf("%s:%d %s (%s)", li.File, li.Line, strings.Join(li.Checks, ", "), matched)
92 type FileIgnore struct {
97 func (fi *FileIgnore) Match(p Problem) bool {
98 if p.Pos.Filename != fi.File {
101 for _, c := range fi.Checks {
102 if m, _ := filepath.Match(c, p.Check); m {
112 Error Severity = iota
117 // Problem represents a problem in some source code.
118 type Problem struct {
127 type Related struct {
133 func (p Problem) Equal(o Problem) bool {
134 return p.Pos == o.Pos &&
136 p.Message == o.Message &&
137 p.Check == o.Check &&
138 p.Severity == o.Severity
141 func (p *Problem) String() string {
142 return fmt.Sprintf("%s (%s)", p.Message, p.Check)
145 // A Linter lints Go source code.
147 Checkers []*analysis.Analyzer
148 CumulativeCheckers []CumulativeChecker
155 type CumulativeChecker interface {
156 Analyzer() *analysis.Analyzer
157 Result() []types.Object
158 ProblemObject(*token.FileSet, types.Object) Problem
161 func (l *Linter) Lint(cfg *packages.Config, patterns []string) ([]Problem, error) {
162 var allAnalyzers []*analysis.Analyzer
163 allAnalyzers = append(allAnalyzers, l.Checkers...)
164 for _, cum := range l.CumulativeCheckers {
165 allAnalyzers = append(allAnalyzers, cum.Analyzer())
168 // The -checks command line flag overrules all configuration
169 // files, which means that for `-checks="foo"`, no check other
170 // than foo can ever be reported to the user. Make use of this
171 // fact to cull the list of analyses we need to run.
173 // replace "inherit" with "all", as we don't want to base the
174 // list of all checks on the default configuration, which
175 // disables certain checks.
176 checks := make([]string, len(l.Config.Checks))
177 copy(checks, l.Config.Checks)
178 for i, c := range checks {
184 allowed := FilterChecks(allAnalyzers, checks)
185 var allowedAnalyzers []*analysis.Analyzer
186 for _, c := range l.Checkers {
188 allowedAnalyzers = append(allowedAnalyzers, c)
191 hasCumulative := false
192 for _, cum := range l.CumulativeCheckers {
196 allowedAnalyzers = append(allowedAnalyzers, a)
200 r, err := NewRunner(&l.Stats)
204 r.goVersion = l.GoVersion
205 r.repeatAnalyzers = l.RepeatAnalyzers
207 pkgs, err := r.Run(cfg, patterns, allowedAnalyzers, hasCumulative)
212 tpkgToPkg := map[*types.Package]*Package{}
213 for _, pkg := range pkgs {
214 tpkgToPkg[pkg.Types] = pkg
216 for _, e := range pkg.errs {
217 switch e := e.(type) {
220 Pos: e.Fset.PositionFor(e.Pos, false),
225 pkg.problems = append(pkg.problems, p)
228 if len(msg) != 0 && msg[0] == '\n' {
229 // TODO(dh): See https://github.com/golang/go/issues/32363
233 var pos token.Position
235 // Under certain conditions (malformed package
236 // declarations, multiple packages in the same
237 // directory), go list emits an error on stderr
238 // instead of JSON. Those errors do not have
239 // associated position information in
240 // go/packages.Error, even though the output on
241 // stderr may contain it.
242 if p, n, err := parsePos(msg); err == nil {
243 if abs, err := filepath.Abs(p.Filename); err == nil {
251 pos, _, err = parsePos(e.Pos)
253 panic(fmt.Sprintf("internal error: %s", e))
262 pkg.problems = append(pkg.problems, p)
263 case scanner.ErrorList:
264 for _, e := range e {
271 pkg.problems = append(pkg.problems, p)
275 Pos: token.Position{},
280 pkg.problems = append(pkg.problems, p)
285 atomic.StoreUint32(&r.stats.State, StateCumulative)
286 for _, cum := range l.CumulativeCheckers {
287 for _, res := range cum.Result() {
288 pkg := tpkgToPkg[res.Pkg()]
290 panic(fmt.Sprintf("analyzer %s flagged object %s in package %s, a package that we aren't tracking", cum.Analyzer(), res, res.Pkg()))
292 allowedChecks := FilterChecks(allowedAnalyzers, pkg.cfg.Merge(l.Config).Checks)
293 if allowedChecks[cum.Analyzer().Name] {
294 pos := DisplayPosition(pkg.Fset, res.Pos())
295 // FIXME(dh): why are we ignoring generated files
296 // here? Surely this is specific to 'unused', not all
297 // cumulative checkers
298 if _, ok := pkg.gen[pos.Filename]; ok {
301 p := cum.ProblemObject(pkg.Fset, res)
302 pkg.problems = append(pkg.problems, p)
307 for _, pkg := range pkgs {
309 // Don't cache packages that we loaded from the cache
312 cpkg := cachedPackage{
313 Problems: pkg.problems,
314 Ignores: pkg.ignores,
317 buf := &bytes.Buffer{}
318 if err := gob.NewEncoder(buf).Encode(cpkg); err != nil {
321 id := cache.Subkey(pkg.actionID, "data "+r.problemsCacheKey)
322 if err := r.cache.PutBytes(id, buf.Bytes()); err != nil {
327 var problems []Problem
328 // Deduplicate line ignores. When U1000 processes a package and
329 // its test variant, it will only emit a single problem for an
330 // unused object, not two problems. We will, however, have two
331 // line ignores, one per package. Without deduplication, one line
332 // ignore will be marked as matched, while the other one won't,
333 // subsequently reporting a "this linter directive didn't match
335 ignores := map[token.Position]Ignore{}
336 for _, pkg := range pkgs {
337 for _, ig := range pkg.ignores {
338 if lig, ok := ig.(*LineIgnore); ok {
339 ig = ignores[lig.Pos]
341 ignores[lig.Pos] = lig
345 for i := range pkg.problems {
346 p := &pkg.problems[i]
354 // The package failed to load, otherwise we would have a
355 // valid config. Pass through all errors.
356 problems = append(problems, pkg.problems...)
358 for _, p := range pkg.problems {
359 allowedChecks := FilterChecks(allowedAnalyzers, pkg.cfg.Merge(l.Config).Checks)
360 allowedChecks["compile"] = true
361 if allowedChecks[p.Check] {
362 problems = append(problems, p)
367 for _, ig := range pkg.ignores {
368 ig, ok := ig.(*LineIgnore)
372 ig = ignores[ig.Pos].(*LineIgnore)
377 couldveMatched := false
378 allowedChecks := FilterChecks(allowedAnalyzers, pkg.cfg.Merge(l.Config).Checks)
379 for _, c := range ig.Checks {
380 if !allowedChecks[c] {
383 couldveMatched = true
388 // The ignored checks were disabled for the containing package.
389 // Don't flag the ignore for not having matched.
394 Message: "this linter directive didn't match anything; should it be removed?",
397 problems = append(problems, p)
401 if len(problems) == 0 {
405 sort.Slice(problems, func(i, j int) bool {
406 pi := problems[i].Pos
407 pj := problems[j].Pos
409 if pi.Filename != pj.Filename {
410 return pi.Filename < pj.Filename
412 if pi.Line != pj.Line {
413 return pi.Line < pj.Line
415 if pi.Column != pj.Column {
416 return pi.Column < pj.Column
419 return problems[i].Message < problems[j].Message
423 out = append(out, problems[0])
424 for i, p := range problems[1:] {
425 // We may encounter duplicate problems because one file
426 // can be part of many packages.
427 if !problems[i].Equal(p) {
434 func FilterChecks(allChecks []*analysis.Analyzer, checks []string) map[string]bool {
435 // OPT(dh): this entire computation could be cached per package
436 allowedChecks := map[string]bool{}
438 for _, check := range checks {
440 if len(check) > 1 && check[0] == '-' {
444 if check == "*" || check == "all" {
446 for _, c := range allChecks {
447 allowedChecks[c.Name] = b
449 } else if strings.HasSuffix(check, "*") {
451 prefix := check[:len(check)-1]
452 isCat := strings.IndexFunc(prefix, func(r rune) bool { return unicode.IsNumber(r) }) == -1
454 for _, c := range allChecks {
455 idx := strings.IndexFunc(c.Name, func(r rune) bool { return unicode.IsNumber(r) })
457 // Glob is S*, which should match S1000 but not SA1000
460 allowedChecks[c.Name] = b
464 if strings.HasPrefix(c.Name, prefix) {
465 allowedChecks[c.Name] = b
470 // Literal check name
471 allowedChecks[check] = b
477 func DisplayPosition(fset *token.FileSet, p token.Pos) token.Position {
478 if p == token.NoPos {
479 return token.Position{}
482 // Only use the adjusted position if it points to another Go file.
483 // This means we'll point to the original file for cgo files, but
484 // we won't point to a YACC grammar file.
485 pos := fset.PositionFor(p, false)
486 adjPos := fset.PositionFor(p, true)
488 if filepath.Ext(adjPos.Filename) == ".go" {
494 var bufferPool = &sync.Pool{
495 New: func() interface{} {
496 buf := bytes.NewBuffer(nil)
502 func FuncName(f *types.Func) string {
503 buf := bufferPool.Get().(*bytes.Buffer)
506 sig := f.Type().(*types.Signature)
507 if recv := sig.Recv(); recv != nil {
509 if _, ok := recv.Type().(*types.Interface); ok {
510 // gcimporter creates abstract methods of
511 // named interfaces using the interface type
512 // (not the named type) as the receiver.
513 // Don't print it in full.
514 buf.WriteString("interface")
516 types.WriteType(buf, recv.Type(), nil)
520 } else if f.Pkg() != nil {
521 writePackage(buf, f.Pkg())
524 buf.WriteString(f.Name())
530 func writePackage(buf *bytes.Buffer, pkg *types.Package) {