lint.go raw

   1  package lintcmd
   2  
   3  import (
   4  	"crypto/sha256"
   5  	"fmt"
   6  	"go/token"
   7  	"io"
   8  	"os"
   9  	"os/signal"
  10  	"path/filepath"
  11  	"regexp"
  12  	"strconv"
  13  	"strings"
  14  	"time"
  15  	"unicode"
  16  
  17  	"honnef.co/go/tools/analysis/lint"
  18  	"honnef.co/go/tools/config"
  19  	"honnef.co/go/tools/go/buildid"
  20  	"honnef.co/go/tools/go/loader"
  21  	"honnef.co/go/tools/lintcmd/cache"
  22  	"honnef.co/go/tools/lintcmd/runner"
  23  	"honnef.co/go/tools/unused"
  24  
  25  	"golang.org/x/tools/go/analysis"
  26  	"golang.org/x/tools/go/packages"
  27  )
  28  
  29  // A linter lints Go source code.
  30  type linter struct {
  31  	analyzers map[string]*lint.Analyzer
  32  	cache     *cache.Cache
  33  	opts      options
  34  }
  35  
  36  func computeSalt() ([]byte, error) {
  37  	p, err := os.Executable()
  38  	if err != nil {
  39  		return nil, err
  40  	}
  41  
  42  	if id, err := buildid.ReadFile(p); err == nil {
  43  		return []byte(id), nil
  44  	} else {
  45  		// For some reason we couldn't read the build id from the executable.
  46  		// Fall back to hashing the entire executable.
  47  		f, err := os.Open(p)
  48  		if err != nil {
  49  			return nil, err
  50  		}
  51  		defer f.Close()
  52  		h := sha256.New()
  53  		if _, err := io.Copy(h, f); err != nil {
  54  			return nil, err
  55  		}
  56  		return h.Sum(nil), nil
  57  	}
  58  }
  59  
  60  func newLinter(opts options) (*linter, error) {
  61  	c, err := cache.Default()
  62  	if err != nil {
  63  		return nil, err
  64  	}
  65  	salt, err := computeSalt()
  66  	if err != nil {
  67  		return nil, fmt.Errorf("could not compute salt for cache: %s", err)
  68  	}
  69  	c.SetSalt(salt)
  70  
  71  	analyzers := make(map[string]*lint.Analyzer, len(opts.analyzers))
  72  	for _, a := range opts.analyzers {
  73  		analyzers[a.Analyzer.Name] = a
  74  	}
  75  
  76  	return &linter{
  77  		cache:     c,
  78  		analyzers: analyzers,
  79  		opts:      opts,
  80  	}, nil
  81  }
  82  
  83  type lintResult struct {
  84  	// These fields are exported so that we can gob encode them.
  85  
  86  	CheckedFiles []string
  87  	Diagnostics  []diagnostic
  88  	Warnings     []string
  89  }
  90  
  91  type options struct {
  92  	config                   config.Config
  93  	analyzers                []*lint.Analyzer
  94  	patterns                 []string
  95  	lintTests                bool
  96  	goVersion                string
  97  	printAnalyzerMeasurement func(analysis *analysis.Analyzer, pkg *loader.PackageSpec, d time.Duration)
  98  }
  99  
 100  func (l *linter) run(bconf buildConfig) (lintResult, error) {
 101  	cfg := &packages.Config{}
 102  	if l.opts.lintTests {
 103  		cfg.Tests = true
 104  	}
 105  
 106  	cfg.BuildFlags = bconf.Flags
 107  	cfg.Env = append(os.Environ(), bconf.Envs...)
 108  
 109  	r, err := runner.New(l.opts.config, l.cache)
 110  	if err != nil {
 111  		return lintResult{}, err
 112  	}
 113  	r.GoVersion = l.opts.goVersion
 114  	r.Stats.PrintAnalyzerMeasurement = l.opts.printAnalyzerMeasurement
 115  
 116  	printStats := func() {
 117  		// Individual stats are read atomically, but overall there
 118  		// is no synchronisation. For printing rough progress
 119  		// information, this doesn't matter.
 120  		switch r.Stats.State() {
 121  		case runner.StateInitializing:
 122  			fmt.Fprintln(os.Stderr, "Status: initializing")
 123  		case runner.StateLoadPackageGraph:
 124  			fmt.Fprintln(os.Stderr, "Status: loading package graph")
 125  		case runner.StateBuildActionGraph:
 126  			fmt.Fprintln(os.Stderr, "Status: building action graph")
 127  		case runner.StateProcessing:
 128  			fmt.Fprintf(os.Stderr, "Packages: %d/%d initial, %d/%d total; Workers: %d/%d\n",
 129  				r.Stats.ProcessedInitialPackages(),
 130  				r.Stats.InitialPackages(),
 131  				r.Stats.ProcessedPackages(),
 132  				r.Stats.TotalPackages(),
 133  				r.ActiveWorkers(),
 134  				r.TotalWorkers(),
 135  			)
 136  		case runner.StateFinalizing:
 137  			fmt.Fprintln(os.Stderr, "Status: finalizing")
 138  		}
 139  	}
 140  	if len(infoSignals) > 0 {
 141  		ch := make(chan os.Signal, 1)
 142  		signal.Notify(ch, infoSignals...)
 143  		defer signal.Stop(ch)
 144  		go func() {
 145  			for range ch {
 146  				printStats()
 147  			}
 148  		}()
 149  	}
 150  	res, err := l.lint(r, cfg, l.opts.patterns)
 151  	for i := range res.Diagnostics {
 152  		res.Diagnostics[i].BuildName = bconf.Name
 153  	}
 154  	return res, err
 155  }
 156  
 157  func (l *linter) lint(r *runner.Runner, cfg *packages.Config, patterns []string) (lintResult, error) {
 158  	var out lintResult
 159  
 160  	as := make([]*analysis.Analyzer, 0, len(l.analyzers))
 161  	for _, a := range l.analyzers {
 162  		as = append(as, a.Analyzer)
 163  	}
 164  	results, err := r.Run(cfg, as, patterns)
 165  	if err != nil {
 166  		return out, err
 167  	}
 168  
 169  	if len(results) == 0 {
 170  		// TODO(dh): emulate Go's behavior more closely once we have
 171  		// access to go list's Match field.
 172  		for _, pattern := range patterns {
 173  			fmt.Fprintf(os.Stderr, "warning: %q matched no packages\n", pattern)
 174  		}
 175  	}
 176  
 177  	analyzerNames := make([]string, 0, len(l.analyzers))
 178  	for name := range l.analyzers {
 179  		analyzerNames = append(analyzerNames, name)
 180  	}
 181  	used := map[unusedKey]bool{}
 182  	var unuseds []unusedPair
 183  	for _, res := range results {
 184  		if len(res.Errors) > 0 && !res.Failed {
 185  			panic("package has errors but isn't marked as failed")
 186  		}
 187  		if res.Failed {
 188  			out.Diagnostics = append(out.Diagnostics, failed(res)...)
 189  		} else {
 190  			if res.Skipped {
 191  				out.Warnings = append(out.Warnings, fmt.Sprintf("skipped package %s because it is too large", res.Package))
 192  				continue
 193  			}
 194  
 195  			if !res.Initial {
 196  				continue
 197  			}
 198  
 199  			out.CheckedFiles = append(out.CheckedFiles, res.Package.GoFiles...)
 200  			allowedAnalyzers := filterAnalyzerNames(analyzerNames, res.Config.Checks)
 201  			resd, err := res.Load()
 202  			if err != nil {
 203  				return out, err
 204  			}
 205  			ps := success(allowedAnalyzers, resd)
 206  			filtered, err := filterIgnored(ps, resd, allowedAnalyzers)
 207  			if err != nil {
 208  				return out, err
 209  			}
 210  			// OPT move this code into the 'success' function.
 211  			for i, diag := range filtered {
 212  				a := l.analyzers[diag.Category]
 213  				// Some diag.Category don't map to analyzers, such as "staticcheck"
 214  				if a != nil {
 215  					filtered[i].MergeIf = a.Doc.MergeIf
 216  				}
 217  			}
 218  			out.Diagnostics = append(out.Diagnostics, filtered...)
 219  
 220  			for _, obj := range resd.Unused.Used {
 221  				// Note: a side-effect of this code is that fields in instantiated structs are handled correctly. Even
 222  				// if only an instantiated field is marked as used, we will not flag the generic field, because it has
 223  				// the same position as the instance. At some point this won't be necessary anymore because we'll be
 224  				// able to make use of the Go 1.19+ Origin methods.
 225  
 226  				// FIXME(dh): pick the object whose filename does not include $GOROOT
 227  				key := unusedKey{
 228  					pkgPath: res.Package.PkgPath,
 229  					base:    filepath.Base(obj.Position.Filename),
 230  					line:    obj.Position.Line,
 231  					name:    obj.Name,
 232  				}
 233  				used[key] = true
 234  			}
 235  
 236  			if allowedAnalyzers["U1000"] {
 237  				for _, obj := range resd.Unused.Unused {
 238  					key := unusedKey{
 239  						pkgPath: res.Package.PkgPath,
 240  						base:    filepath.Base(obj.Position.Filename),
 241  						line:    obj.Position.Line,
 242  						name:    obj.Name,
 243  					}
 244  					unuseds = append(unuseds, unusedPair{key, obj})
 245  					if _, ok := used[key]; !ok {
 246  						used[key] = false
 247  					}
 248  				}
 249  			}
 250  		}
 251  	}
 252  
 253  	for _, uo := range unuseds {
 254  		if used[uo.key] {
 255  			continue
 256  		}
 257  		out.Diagnostics = append(out.Diagnostics, diagnostic{
 258  			Diagnostic: runner.Diagnostic{
 259  				Position: uo.obj.DisplayPosition,
 260  				Message:  fmt.Sprintf("%s %s is unused", uo.obj.Kind, uo.obj.Name),
 261  				Category: "U1000",
 262  			},
 263  			MergeIf: lint.MergeIfAll,
 264  		})
 265  	}
 266  
 267  	return out, nil
 268  }
 269  
 270  func filterIgnored(diagnostics []diagnostic, res runner.ResultData, allowedAnalyzers map[string]bool) ([]diagnostic, error) {
 271  	couldHaveMatched := func(ig *lineIgnore) bool {
 272  		for _, c := range ig.Checks {
 273  			if c == "U1000" {
 274  				// We never want to flag ignores for U1000,
 275  				// because U1000 isn't local to a single
 276  				// package. For example, an identifier may
 277  				// only be used by tests, in which case an
 278  				// ignore would only fire when not analyzing
 279  				// tests. To avoid spurious "useless ignore"
 280  				// warnings, just never flag U1000.
 281  				return false
 282  			}
 283  
 284  			// Even though the runner always runs all analyzers, we
 285  			// still only flag unmatched ignores for the set of
 286  			// analyzers the user has expressed interest in. That way,
 287  			// `staticcheck -checks=SA1000` won't complain about an
 288  			// unmatched ignore for an unrelated check.
 289  			if allowedAnalyzers[c] {
 290  				return true
 291  			}
 292  		}
 293  
 294  		return false
 295  	}
 296  
 297  	ignores, moreDiagnostics := parseDirectives(res.Directives)
 298  
 299  	for _, ig := range ignores {
 300  		for i := range diagnostics {
 301  			diag := &diagnostics[i]
 302  			if ig.match(*diag) {
 303  				diag.Severity = severityIgnored
 304  			}
 305  		}
 306  
 307  		if ig, ok := ig.(*lineIgnore); ok && !ig.Matched && couldHaveMatched(ig) {
 308  			diag := diagnostic{
 309  				Diagnostic: runner.Diagnostic{
 310  					Position: ig.Pos,
 311  					Message:  "this linter directive didn't match anything; should it be removed?",
 312  					Category: "staticcheck",
 313  				},
 314  			}
 315  			moreDiagnostics = append(moreDiagnostics, diag)
 316  		}
 317  	}
 318  
 319  	return append(diagnostics, moreDiagnostics...), nil
 320  }
 321  
 322  type ignore interface {
 323  	match(diag diagnostic) bool
 324  }
 325  
 326  type lineIgnore struct {
 327  	File    string
 328  	Line    int
 329  	Checks  []string
 330  	Matched bool
 331  	Pos     token.Position
 332  }
 333  
 334  func (li *lineIgnore) match(p diagnostic) bool {
 335  	pos := p.Position
 336  	if pos.Filename != li.File || pos.Line != li.Line {
 337  		return false
 338  	}
 339  	for _, c := range li.Checks {
 340  		if m, _ := filepath.Match(c, p.Category); m {
 341  			li.Matched = true
 342  			return true
 343  		}
 344  	}
 345  	return false
 346  }
 347  
 348  func (li *lineIgnore) String() string {
 349  	matched := "not matched"
 350  	if li.Matched {
 351  		matched = "matched"
 352  	}
 353  	return fmt.Sprintf("%s:%d %s (%s)", li.File, li.Line, strings.Join(li.Checks, ", "), matched)
 354  }
 355  
 356  type fileIgnore struct {
 357  	File   string
 358  	Checks []string
 359  }
 360  
 361  func (fi *fileIgnore) match(p diagnostic) bool {
 362  	if p.Position.Filename != fi.File {
 363  		return false
 364  	}
 365  	for _, c := range fi.Checks {
 366  		if m, _ := filepath.Match(c, p.Category); m {
 367  			return true
 368  		}
 369  	}
 370  	return false
 371  }
 372  
 373  type severity uint8
 374  
 375  const (
 376  	severityError severity = iota
 377  	severityWarning
 378  	severityIgnored
 379  )
 380  
 381  func (s severity) String() string {
 382  	switch s {
 383  	case severityError:
 384  		return "error"
 385  	case severityWarning:
 386  		return "warning"
 387  	case severityIgnored:
 388  		return "ignored"
 389  	default:
 390  		return fmt.Sprintf("Severity(%d)", s)
 391  	}
 392  }
 393  
 394  // diagnostic represents a diagnostic in some source code.
 395  type diagnostic struct {
 396  	runner.Diagnostic
 397  
 398  	// These fields are exported so that we can gob encode them.
 399  	Severity  severity
 400  	MergeIf   lint.MergeStrategy
 401  	BuildName string
 402  }
 403  
 404  func (p diagnostic) equal(o diagnostic) bool {
 405  	return p.Position == o.Position &&
 406  		p.End == o.End &&
 407  		p.Message == o.Message &&
 408  		p.Category == o.Category &&
 409  		p.Severity == o.Severity &&
 410  		p.MergeIf == o.MergeIf &&
 411  		p.BuildName == o.BuildName
 412  }
 413  
 414  func (p *diagnostic) String() string {
 415  	if p.BuildName != "" {
 416  		return fmt.Sprintf("%s [%s] (%s)", p.Message, p.BuildName, p.Category)
 417  	} else {
 418  		return fmt.Sprintf("%s (%s)", p.Message, p.Category)
 419  	}
 420  }
 421  
 422  func failed(res runner.Result) []diagnostic {
 423  	var diagnostics []diagnostic
 424  
 425  	for _, e := range res.Errors {
 426  		switch e := e.(type) {
 427  		case packages.Error:
 428  			msg := e.Msg
 429  			if len(msg) != 0 && msg[0] == '\n' {
 430  				// TODO(dh): See https://github.com/golang/go/issues/32363
 431  				msg = msg[1:]
 432  			}
 433  
 434  			cat := "compile"
 435  			if e.Kind == packages.ParseError {
 436  				cat = "config"
 437  			}
 438  
 439  			var posn token.Position
 440  			if e.Pos == "" {
 441  				// Under certain conditions (malformed package
 442  				// declarations, multiple packages in the same
 443  				// directory), go list emits an error on stderr
 444  				// instead of JSON. Those errors do not have
 445  				// associated position information in
 446  				// go/packages.Error, even though the output on
 447  				// stderr may contain it.
 448  				if p, n, err := parsePos(msg); err == nil {
 449  					if abs, err := filepath.Abs(p.Filename); err == nil {
 450  						p.Filename = abs
 451  					}
 452  					posn = p
 453  					msg = msg[n+2:]
 454  				}
 455  			} else {
 456  				var err error
 457  				posn, _, err = parsePos(e.Pos)
 458  				if err != nil {
 459  					panic(fmt.Sprintf("internal error: %s", err))
 460  				}
 461  			}
 462  			diag := diagnostic{
 463  				Diagnostic: runner.Diagnostic{
 464  					Position: posn,
 465  					Message:  msg,
 466  					Category: cat,
 467  				},
 468  				Severity: severityError,
 469  			}
 470  			diagnostics = append(diagnostics, diag)
 471  		case error:
 472  			diag := diagnostic{
 473  				Diagnostic: runner.Diagnostic{
 474  					Position: token.Position{},
 475  					Message:  e.Error(),
 476  					Category: "compile",
 477  				},
 478  				Severity: severityError,
 479  			}
 480  			diagnostics = append(diagnostics, diag)
 481  		}
 482  	}
 483  
 484  	return diagnostics
 485  }
 486  
 487  type unusedKey struct {
 488  	pkgPath string
 489  	base    string
 490  	line    int
 491  	name    string
 492  }
 493  
 494  type unusedPair struct {
 495  	key unusedKey
 496  	obj unused.Object
 497  }
 498  
 499  func success(allowedAnalyzers map[string]bool, res runner.ResultData) []diagnostic {
 500  	diags := res.Diagnostics
 501  	var diagnostics []diagnostic
 502  	for _, diag := range diags {
 503  		if !allowedAnalyzers[diag.Category] {
 504  			continue
 505  		}
 506  		diagnostics = append(diagnostics, diagnostic{Diagnostic: diag})
 507  	}
 508  	return diagnostics
 509  }
 510  
 511  func filterAnalyzerNames(analyzers []string, checks []string) map[string]bool {
 512  	allowedChecks := map[string]bool{}
 513  
 514  	for _, check := range checks {
 515  		b := true
 516  		if len(check) > 1 && check[0] == '-' {
 517  			b = false
 518  			check = check[1:]
 519  		}
 520  		if check == "*" || check == "all" {
 521  			// Match all
 522  			for _, c := range analyzers {
 523  				allowedChecks[c] = b
 524  			}
 525  		} else if strings.HasSuffix(check, "*") {
 526  			// Glob
 527  			prefix := check[:len(check)-1]
 528  			isCat := strings.IndexFunc(prefix, func(r rune) bool { return unicode.IsNumber(r) }) == -1
 529  
 530  			for _, a := range analyzers {
 531  				idx := strings.IndexFunc(a, func(r rune) bool { return unicode.IsNumber(r) })
 532  				if isCat {
 533  					// Glob is S*, which should match S1000 but not SA1000
 534  					cat := a[:idx]
 535  					if prefix == cat {
 536  						allowedChecks[a] = b
 537  					}
 538  				} else {
 539  					// Glob is S1*
 540  					if strings.HasPrefix(a, prefix) {
 541  						allowedChecks[a] = b
 542  					}
 543  				}
 544  			}
 545  		} else {
 546  			// Literal check name
 547  			allowedChecks[check] = b
 548  		}
 549  	}
 550  	return allowedChecks
 551  }
 552  
 553  // Note that the file name is optional and can be empty because of //line
 554  // directives of the form "//line :1" (but not "//line :1:1"). See
 555  // https://go.dev/issue/24183 and https://staticcheck.dev/issues/1582.
 556  var posRe = regexp.MustCompile(`^(?:(.+?):)?(\d+)(?::(\d+)?)?`)
 557  
 558  func parsePos(pos string) (token.Position, int, error) {
 559  	if pos == "-" || pos == "" {
 560  		return token.Position{}, 0, nil
 561  	}
 562  	parts := posRe.FindStringSubmatch(pos)
 563  	if parts == nil {
 564  		return token.Position{}, 0, fmt.Errorf("malformed position %q", pos)
 565  	}
 566  	file := parts[1]
 567  	line, _ := strconv.Atoi(parts[2])
 568  	col, _ := strconv.Atoi(parts[3])
 569  	return token.Position{
 570  		Filename: file,
 571  		Line:     line,
 572  		Column:   col,
 573  	}, len(parts[0]), nil
 574  }
 575