src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

lint.go (14476B)


      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[caseFoldedString]*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 	cache.SetSalt(salt)
     70 
     71 	analyzers := make(map[caseFoldedString]*lint.Analyzer, len(opts.analyzers))
     72 	for _, a := range opts.analyzers {
     73 		analyzers[makeCaseFoldedString(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([]caseFoldedString, 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 			resChecks := makeCaseFoldedStrings(res.Config.Checks)
    201 			allowedAnalyzers := filterAnalyzerNames(analyzerNames, resChecks)
    202 			resd, err := res.Load()
    203 			if err != nil {
    204 				return out, err
    205 			}
    206 			ps := success(allowedAnalyzers, resd)
    207 			filtered, err := filterIgnored(ps, resd, allowedAnalyzers)
    208 			if err != nil {
    209 				return out, err
    210 			}
    211 			// OPT move this code into the 'success' function.
    212 			for i, diag := range filtered {
    213 				a := l.analyzers[makeCaseFoldedString(diag.Category)]
    214 				// Some diag.Category don't map to analyzers, such as "staticcheck"
    215 				if a != nil {
    216 					filtered[i].MergeIf = a.Doc.MergeIf
    217 				}
    218 			}
    219 			out.Diagnostics = append(out.Diagnostics, filtered...)
    220 
    221 			for _, obj := range resd.Unused.Used {
    222 				// Note: a side-effect of this code is that fields in instantiated structs are handled correctly. Even
    223 				// if only an instantiated field is marked as used, we will not flag the generic field, because it has
    224 				// the same position as the instance. At some point this won't be necessary anymore because we'll be
    225 				// able to make use of the Go 1.19+ Origin methods.
    226 
    227 				// FIXME(dh): pick the object whose filename does not include $GOROOT
    228 				key := unusedKey{
    229 					pkgPath: res.Package.PkgPath,
    230 					base:    filepath.Base(obj.Position.Filename),
    231 					line:    obj.Position.Line,
    232 					name:    obj.Name,
    233 				}
    234 				used[key] = true
    235 			}
    236 
    237 			if allowedAnalyzers[makeCaseFoldedString("U1000")] {
    238 				for _, obj := range resd.Unused.Unused {
    239 					key := unusedKey{
    240 						pkgPath: res.Package.PkgPath,
    241 						base:    filepath.Base(obj.Position.Filename),
    242 						line:    obj.Position.Line,
    243 						name:    obj.Name,
    244 					}
    245 					unuseds = append(unuseds, unusedPair{key, obj})
    246 					if _, ok := used[key]; !ok {
    247 						used[key] = false
    248 					}
    249 				}
    250 			}
    251 		}
    252 	}
    253 
    254 	for _, uo := range unuseds {
    255 		if used[uo.key] {
    256 			continue
    257 		}
    258 		out.Diagnostics = append(out.Diagnostics, diagnostic{
    259 			Diagnostic: runner.Diagnostic{
    260 				Position: uo.obj.DisplayPosition,
    261 				Message:  fmt.Sprintf("%s %s is unused", uo.obj.Kind, uo.obj.Name),
    262 				Category: "U1000",
    263 			},
    264 			MergeIf: lint.MergeIfAll,
    265 		})
    266 	}
    267 
    268 	return out, nil
    269 }
    270 
    271 func filterIgnored(
    272 	diagnostics []diagnostic,
    273 	res runner.ResultData,
    274 	allowedAnalyzers map[caseFoldedString]bool,
    275 ) ([]diagnostic, error) {
    276 	couldHaveMatched := func(ig *lineIgnore) bool {
    277 		for _, c := range ig.Checks {
    278 			if c.String() == "u1000" {
    279 				// We never want to flag ignores for U1000,
    280 				// because U1000 isn't local to a single
    281 				// package. For example, an identifier may
    282 				// only be used by tests, in which case an
    283 				// ignore would only fire when not analyzing
    284 				// tests. To avoid spurious "useless ignore"
    285 				// warnings, just never flag U1000.
    286 				return false
    287 			}
    288 
    289 			// Even though the runner always runs all analyzers, we
    290 			// still only flag unmatched ignores for the set of
    291 			// analyzers the user has expressed interest in. That way,
    292 			// `staticcheck -checks=SA1000` won't complain about an
    293 			// unmatched ignore for an unrelated check.
    294 			if allowedAnalyzers[c] {
    295 				return true
    296 			}
    297 		}
    298 
    299 		return false
    300 	}
    301 
    302 	ignores, moreDiagnostics := parseDirectives(res.Directives)
    303 
    304 	for _, ig := range ignores {
    305 		for i := range diagnostics {
    306 			diag := &diagnostics[i]
    307 			if ig.match(*diag) {
    308 				diag.Severity = severityIgnored
    309 			}
    310 		}
    311 
    312 		if ig, ok := ig.(*lineIgnore); ok && !ig.Matched && couldHaveMatched(ig) {
    313 			diag := diagnostic{
    314 				Diagnostic: runner.Diagnostic{
    315 					Position: ig.Pos,
    316 					Message:  "this linter directive didn't match anything; should it be removed?",
    317 					Category: "staticcheck",
    318 				},
    319 			}
    320 			moreDiagnostics = append(moreDiagnostics, diag)
    321 		}
    322 	}
    323 
    324 	return append(diagnostics, moreDiagnostics...), nil
    325 }
    326 
    327 type ignore interface {
    328 	match(diag diagnostic) bool
    329 }
    330 
    331 type lineIgnore struct {
    332 	File    string
    333 	Line    int
    334 	Checks  []caseFoldedString
    335 	Matched bool
    336 	Pos     token.Position
    337 }
    338 
    339 func (li *lineIgnore) match(p diagnostic) bool {
    340 	pos := p.Position
    341 	if pos.Filename != li.File || pos.Line != li.Line {
    342 		return false
    343 	}
    344 	for _, c := range li.Checks {
    345 		if m, _ := filepath.Match(c.String(), makeCaseFoldedString(p.Category).String()); m {
    346 			li.Matched = true
    347 			return true
    348 		}
    349 	}
    350 	return false
    351 }
    352 
    353 type fileIgnore struct {
    354 	File   string
    355 	Checks []caseFoldedString
    356 }
    357 
    358 func (fi *fileIgnore) match(p diagnostic) bool {
    359 	if p.Position.Filename != fi.File {
    360 		return false
    361 	}
    362 	for _, c := range fi.Checks {
    363 		if m, _ := filepath.Match(c.String(), makeCaseFoldedString(p.Category).String()); m {
    364 			return true
    365 		}
    366 	}
    367 	return false
    368 }
    369 
    370 type severity uint8
    371 
    372 const (
    373 	severityError severity = iota
    374 	severityWarning
    375 	severityIgnored
    376 )
    377 
    378 func (s severity) String() string {
    379 	switch s {
    380 	case severityError:
    381 		return "error"
    382 	case severityWarning:
    383 		return "warning"
    384 	case severityIgnored:
    385 		return "ignored"
    386 	default:
    387 		return fmt.Sprintf("Severity(%d)", s)
    388 	}
    389 }
    390 
    391 // diagnostic represents a diagnostic in some source code.
    392 type diagnostic struct {
    393 	runner.Diagnostic
    394 
    395 	// These fields are exported so that we can gob encode them.
    396 	Severity  severity
    397 	MergeIf   lint.MergeStrategy
    398 	BuildName string
    399 }
    400 
    401 func (p diagnostic) equal(o diagnostic) bool {
    402 	return p.Position == o.Position &&
    403 		p.End == o.End &&
    404 		p.Message == o.Message &&
    405 		makeCaseFoldedString(p.Category) == makeCaseFoldedString(o.Category) &&
    406 		p.Severity == o.Severity &&
    407 		p.MergeIf == o.MergeIf &&
    408 		p.BuildName == o.BuildName
    409 }
    410 
    411 func (p *diagnostic) String() string {
    412 	if p.BuildName != "" {
    413 		return fmt.Sprintf("%s [%s] (%s)", p.Message, p.BuildName, p.Category)
    414 	} else {
    415 		return fmt.Sprintf("%s (%s)", p.Message, p.Category)
    416 	}
    417 }
    418 
    419 func failed(res runner.Result) []diagnostic {
    420 	var diagnostics []diagnostic
    421 
    422 	for _, e := range res.Errors {
    423 		switch e := e.(type) {
    424 		case packages.Error:
    425 			msg := e.Msg
    426 			if len(msg) != 0 && msg[0] == '\n' {
    427 				// TODO(dh): See https://github.com/golang/go/issues/32363
    428 				msg = msg[1:]
    429 			}
    430 
    431 			cat := "compile"
    432 			if e.Kind == packages.ParseError {
    433 				cat = "config"
    434 			}
    435 
    436 			var posn token.Position
    437 			if e.Pos == "" {
    438 				// Under certain conditions (malformed package
    439 				// declarations, multiple packages in the same
    440 				// directory), go list emits an error on stderr
    441 				// instead of JSON. Those errors do not have
    442 				// associated position information in
    443 				// go/packages.Error, even though the output on
    444 				// stderr may contain it.
    445 				if p, n, err := parsePos(msg); err == nil {
    446 					if abs, err := filepath.Abs(p.Filename); err == nil {
    447 						p.Filename = abs
    448 					}
    449 					posn = p
    450 					msg = msg[n+2:]
    451 				}
    452 			} else {
    453 				var err error
    454 				posn, _, err = parsePos(e.Pos)
    455 				if err != nil {
    456 					panic(fmt.Sprintf("internal error: %s", err))
    457 				}
    458 			}
    459 			diag := diagnostic{
    460 				Diagnostic: runner.Diagnostic{
    461 					Position: posn,
    462 					Message:  msg,
    463 					Category: cat,
    464 				},
    465 				Severity: severityError,
    466 			}
    467 			diagnostics = append(diagnostics, diag)
    468 		case error:
    469 			diag := diagnostic{
    470 				Diagnostic: runner.Diagnostic{
    471 					Position: token.Position{},
    472 					Message:  e.Error(),
    473 					Category: "compile",
    474 				},
    475 				Severity: severityError,
    476 			}
    477 			diagnostics = append(diagnostics, diag)
    478 		}
    479 	}
    480 
    481 	return diagnostics
    482 }
    483 
    484 type unusedKey struct {
    485 	pkgPath string
    486 	base    string
    487 	line    int
    488 	name    string
    489 }
    490 
    491 type unusedPair struct {
    492 	key unusedKey
    493 	obj unused.Object
    494 }
    495 
    496 func success(allowedAnalyzers map[caseFoldedString]bool, res runner.ResultData) []diagnostic {
    497 	diags := res.Diagnostics
    498 	var diagnostics []diagnostic
    499 	for _, diag := range diags {
    500 		if !allowedAnalyzers[makeCaseFoldedString(diag.Category)] {
    501 			continue
    502 		}
    503 		diagnostics = append(diagnostics, diagnostic{Diagnostic: diag})
    504 	}
    505 	return diagnostics
    506 }
    507 
    508 func filterAnalyzerNames(allAnalyzers []caseFoldedString, selection []caseFoldedString) map[caseFoldedString]bool {
    509 	allowedChecks := map[caseFoldedString]bool{}
    510 
    511 	for _, check := range selection {
    512 		b := true
    513 		if check.Length() > 1 && check.Index(0) == '-' {
    514 			b = false
    515 			check = check.Slice(1, -1)
    516 		}
    517 		if check.String() == "*" || check.String() == "all" {
    518 			// Match all
    519 			for _, a := range allAnalyzers {
    520 				allowedChecks[a] = b
    521 			}
    522 		} else if strings.HasSuffix(check.String(), "*") {
    523 			// Glob
    524 			prefix := check.Slice(0, check.Length()-1)
    525 			isCat := strings.IndexFunc(prefix.String(), unicode.IsNumber) == -1
    526 
    527 			for _, a := range allAnalyzers {
    528 				idx := strings.IndexFunc(a.String(), unicode.IsNumber)
    529 				if isCat {
    530 					// Glob is S*, which should match S1000 but not SA1000
    531 					cat := a.Slice(0, idx)
    532 					if prefix == cat {
    533 						allowedChecks[a] = b
    534 					}
    535 				} else {
    536 					// Glob is S1*
    537 					if strings.HasPrefix(a.String(), prefix.String()) {
    538 						allowedChecks[a] = b
    539 					}
    540 				}
    541 			}
    542 		} else {
    543 			// Literal check name
    544 			allowedChecks[check] = b
    545 		}
    546 	}
    547 	return allowedChecks
    548 }
    549 
    550 // Note that the file name is optional and can be empty because of //line
    551 // directives of the form "//line :1" (but not "//line :1:1"). See
    552 // https://go.dev/issue/24183 and https://staticcheck.dev/issues/1582.
    553 var posRe = regexp.MustCompile(`^(?:(.+?):)?(\d+)(?::(\d+)?)?`)
    554 
    555 func parsePos(pos string) (token.Position, int, error) {
    556 	if pos == "-" || pos == "" {
    557 		return token.Position{}, 0, nil
    558 	}
    559 	parts := posRe.FindStringSubmatch(pos)
    560 	if parts == nil {
    561 		return token.Position{}, 0, fmt.Errorf("malformed position %q", pos)
    562 	}
    563 	file := parts[1]
    564 	line, _ := strconv.Atoi(parts[2])
    565 	col, _ := strconv.Atoi(parts[3])
    566 	return token.Position{
    567 		Filename: file,
    568 		Line:     line,
    569 		Column:   col,
    570 	}, len(parts[0]), nil
    571 }