Skip to content

Commit

Permalink
Add back "-go" flag
Browse files Browse the repository at this point in the history
Even though Go 1.21 and newer interpret "go" in go.mod files to mean a
minimum requirement, users may still be working with older versions of
Go, or outside of Go modules. For them, the flag is still useful.
  • Loading branch information
dominikh committed Jul 1, 2024
1 parent 1fc8b1b commit b3bd325
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 47 deletions.
108 changes: 66 additions & 42 deletions go/loader/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,24 +140,40 @@ func Graph(c *cache.Cache, cfg *packages.Config, patterns ...string) ([]*Package
type program struct {
fset *token.FileSet
packages map[string]*types.Package
options *Options
}

type Stats struct {
Source time.Duration
Export map[*PackageSpec]time.Duration
}

type Options struct {
// The Go language version to use for the type checker. If unset, or if set
// to "module", it will default to the Go version specified in the module;
// if there is no module, it will default to the version of Go the
// executable was built with.
GoVersion string
}

// Load loads the package described in spec. Imports will be loaded
// from export data, while the package itself will be loaded from
// source.
//
// An error will only be returned for system failures, such as failure
// to read export data from disk. Syntax and type errors, among
// others, will only populate the returned package's Errors field.
func Load(spec *PackageSpec) (*Package, Stats, error) {
func Load(spec *PackageSpec, opts *Options) (*Package, Stats, error) {
if opts == nil {
opts = &Options{}
}
if opts.GoVersion == "" {
opts.GoVersion = "module"
}
prog := &program{
fset: token.NewFileSet(),
packages: map[string]*types.Package{},
options: opts,
}

stats := Stats{
Expand Down Expand Up @@ -292,54 +308,62 @@ func (prog *program) loadFromSource(spec *PackageSpec) (*Package, error) {
pkg.Errors = append(pkg.Errors, convertError(err)...)
},
}
if spec.Module != nil && spec.Module.GoVersion != "" {
var our string
if version.IsValid(runtime.Version()) {
// Staticcheck was built with a released version of Go.
// runtime.Version() returns something like "go1.22.4" or
// "go1.23rc1".
our = runtime.Version()
if prog.options.GoVersion == "module" {
if spec.Module != nil && spec.Module.GoVersion != "" {
var our string
if version.IsValid(runtime.Version()) {
// Staticcheck was built with a released version of Go.
// runtime.Version() returns something like "go1.22.4" or
// "go1.23rc1".
our = runtime.Version()
} else {
// Staticcheck was built with a development version of Go.
// runtime.Version() returns something like "devel go1.23-e8ee1dc4f9
// Sun Jun 23 00:52:20 2024 +0000". Fall back to using ReleaseTags,
// where the last one will contain the language version of the
// development version of Go.
tags := build.Default.ReleaseTags
our = tags[len(tags)-1]
}
if version.Compare("go"+spec.Module.GoVersion, our) == 1 {
// We don't need this check for correctness, as go/types rejects
// a GoVersion that's too new. But we can produce a better error
// message. In Go 1.22, go/types simply says "package requires
// newer Go version go1.23", without any information about the
// file, or what version Staticcheck was built with. Starting
// with Go 1.23, the error seems to be better:
// "/home/dominikh/prj/src/example.com/foo.go:3:1: package
// requires newer Go version go1.24 (application built with
// go1.23)" and we may be able to remove this custom logic once
// we depend on Go 1.23.
//
// Note that if Staticcheck was built with a development version of
// Go, e.g. "devel go1.23-82c371a307", then we'll say that
// Staticcheck was built with go1.23, which is the language version
// of the development build. This matches the behavior of the Go
// toolchain, which says "go.mod requires go >= 1.23rc1 (running go
// 1.23; GOTOOLCHAIN=local)".
//
// Note that this prevents Go master from working with go1.23rc1,
// even if master is further ahead. This is currently unavoidable,
// and matches the behavior of the Go toolchain (see above.)
return nil, fmt.Errorf(
"module requires at least go%s, but Staticcheck was built with %s",
spec.Module.GoVersion, our,
)
}
tc.GoVersion = "go" + spec.Module.GoVersion
} else {
// Staticcheck was built with a development version of Go.
// runtime.Version() returns something like "devel go1.23-e8ee1dc4f9
// Sun Jun 23 00:52:20 2024 +0000". Fall back to using ReleaseTags,
// where the last one will contain the language version of the
// development version of Go.
tags := build.Default.ReleaseTags
our = tags[len(tags)-1]
}
if version.Compare("go"+spec.Module.GoVersion, our) == 1 {
// We don't need this check for correctness, as go/types rejects a
// GoVersion that's too new. But we can produce a better error
// message.
//
// Note that if Staticcheck was built with a development version of
// Go, e.g. "devel go1.23-82c371a307", then we'll say that
// Staticcheck was built with go1.23, which is the language version
// of the development build. This matches the behavior of the Go
// toolchain, which says "go.mod requires go >= 1.23rc1 (running go
// 1.23; GOTOOLCHAIN=local)".
//
// Note that this prevents Go master from working with go1.23rc1,
// even if master is further ahead. This is currently unavoidable,
// and matches the behavior of the Go toolchain (see above.)
return nil, fmt.Errorf(
"module requires at least go%s, but Staticcheck was built with %s",
spec.Module.GoVersion, our,
)
tc.GoVersion = tags[len(tags)-1]
}
tc.GoVersion = "go" + spec.Module.GoVersion
} else {
tags := build.Default.ReleaseTags
tc.GoVersion = tags[len(tags)-1]
tc.GoVersion = prog.options.GoVersion
}
// Note that the type-checker can return a non-nil error even though the Go
// compiler has already successfully built this package (which is an
// invariant of getting to this point.) For example, for a module that
// requires Go 1.23, if Staticcheck was built with Go 1.22, but the user's
// toolchain is Go 1.23, then 'go list' (as invoked by go/packages) will
// build the package fine, but go/types will complain that the version is
// too new.
// invariant of getting to this point), for example because of the Go
// version passed to the type checker.
err := types.NewChecker(tc, pkg.Fset, pkg.Types, pkg.TypesInfo).Files(pkg.Syntax)
return pkg, err
}
Expand Down
2 changes: 1 addition & 1 deletion internal/cmd/unused/unused.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func main() {
// XXX priunt errors
continue
}
lpkg, _, err := loader.Load(spec)
lpkg, _, err := loader.Load(spec, nil)
if err != nil {
continue
}
Expand Down
32 changes: 30 additions & 2 deletions lintcmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"flag"
"fmt"
"go/token"
stdversion "go/version"
"io"
"log"
"os"
Expand Down Expand Up @@ -66,8 +67,9 @@ type Command struct {
debugMeasureAnalyzers string
debugTrace string

checks list
fail list
checks list
fail list
goVersion versionFlag
}
}

Expand Down Expand Up @@ -156,8 +158,10 @@ func (cmd *Command) initFlagSet(name string) {

cmd.flags.checks = list{"inherit"}
cmd.flags.fail = list{"all"}
cmd.flags.goVersion = versionFlag("module")
flags.Var(&cmd.flags.checks, "checks", "Comma-separated list of `checks` to enable.")
flags.Var(&cmd.flags.fail, "fail", "Comma-separated list of `checks` that can cause a non-zero exit status.")
flags.Var(&cmd.flags.goVersion, "go", "Target Go `version` in the format '1.x', or the literal 'module' to use the module's Go version")
}

type list []string
Expand All @@ -180,6 +184,29 @@ func (list *list) Set(s string) error {
return nil
}

type versionFlag string

func (v *versionFlag) String() string {
return fmt.Sprintf("%q", string(*v))
}

func (v *versionFlag) Set(s string) error {
if s == "module" {
*v = "module"
} else {
orig := s
if !strings.HasPrefix(s, "go") {
s = "go" + s
}
if stdversion.IsValid(s) {
*v = versionFlag(s)
} else {
return fmt.Errorf("%q is not a valid Go version", orig)
}
}
return nil
}

// ParseFlags parses command line flags.
// It must be called before calling Run.
// After calling ParseFlags, the values of flags can be accessed.
Expand Down Expand Up @@ -457,6 +484,7 @@ func (cmd *Command) lint() int {
analyzers: cs,
patterns: cmd.flags.fs.Args(),
lintTests: cmd.flags.tests,
goVersion: string(cmd.flags.goVersion),
config: config.Config{
Checks: cmd.flags.checks,
},
Expand Down
2 changes: 2 additions & 0 deletions lintcmd/lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ type options struct {
analyzers []*lint.Analyzer
patterns []string
lintTests bool
goVersion string
printAnalyzerMeasurement func(analysis *analysis.Analyzer, pkg *loader.PackageSpec, d time.Duration)
}

Expand All @@ -109,6 +110,7 @@ func (l *linter) run(bconf buildConfig) (lintResult, error) {
if err != nil {
return lintResult{}, err
}
r.GoVersion = l.opts.goVersion
r.Stats.PrintAnalyzerMeasurement = l.opts.printAnalyzerMeasurement

printStats := func() {
Expand Down
6 changes: 4 additions & 2 deletions lintcmd/runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,8 @@ func (act *analyzerAction) String() string {

// A Runner executes analyzers on packages.
type Runner struct {
Stats Stats
Stats Stats
GoVersion string

// If set to true, Runner will populate results with data relevant to testing analyzers
TestMode bool
Expand Down Expand Up @@ -543,6 +544,7 @@ func (r *subrunner) do(act action) error {
fmt.Fprintf(h, "cfg %#v\n", hashCfg)
fmt.Fprintf(h, "pkg %x\n", a.Package.Hash)
fmt.Fprintf(h, "analyzers %s\n", r.analyzerNames)
fmt.Fprintf(h, "go %s\n", r.GoVersion)
fmt.Fprintf(h, "env godebug %q\n", os.Getenv("GODEBUG"))

// OPT(dh): do we actually need to hash vetx? can we not assume
Expand Down Expand Up @@ -685,7 +687,7 @@ func (r *subrunner) doUncached(a *packageAction) (packageActionResult, error) {
// processed concurrently, we shouldn't load b's export data
// twice.

pkg, _, err := loader.Load(a.Package)
pkg, _, err := loader.Load(a.Package, &loader.Options{GoVersion: r.GoVersion})
if err != nil {
return packageActionResult{}, err
}
Expand Down
5 changes: 5 additions & 0 deletions website/content/docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,8 @@ Some checks, particularly those in the `ST` (stylecheck) category, may not be ap
[`checks` option]({{< relref "/docs/configuration/options#checks" >}})
in your [configuration]({{< relref "/docs/configuration/#configuration-files" >}}).
{{% /faq/question %}}

{{% faq/question id="go-version" question="Staticcheck's suggestions don't apply to my version of Go" %}}
You can [specify the version of Go your code should work with.]({{< relref "/docs/configuration/#targeting-go-versions" >}})
{{% /faq/question %}}
{{% /faq/list %}}
15 changes: 15 additions & 0 deletions website/content/docs/running-staticcheck/cli/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,21 @@ See this [list of formatters]({{< relref "/docs/running-staticcheck/cli/formatte
<!-- TODO -->
<!-- ## Controlling the exit status {#fail} -->

## Targeting Go versions {#go}

Some of Staticcheck's analyses adjust their behavior based on the targeted Go version.
For example, the suggestion that one use `for range xs` instead of `for _ = range xs` only applies to Go 1.4 and later, as it won't compile with versions of Go older than that.

By default, Staticcheck targets the Go version declared in `go.mod` via the `go` directive.
For Go 1.21 and never, that directive specifies the minimum required version of Go.

For older versions of Go, the directive technically specifies the maximum version of language features that the module
can use, which means it might be higher than the minimum required version. In those cases, you can manually overwrite
the targeted Go version by using the `-go` command line flag. For example, `staticcheck -go 1.0 ./...` will only make
suggestions that work with Go 1.0.

The targeted Go version limits both language features and parts of the standard library that will be recommended.

## Excluding tests {#tests}

By default, Staticcheck analyses packages as well as their tests.
Expand Down

0 comments on commit b3bd325

Please sign in to comment.