@@ -8,11 +8,13 @@ import (
8
8
"go/constant"
9
9
"go/token"
10
10
"go/types"
11
+ "strconv"
11
12
"strings"
12
13
13
14
"honnef.co/go/tools/analysis/facts/generated"
14
15
"honnef.co/go/tools/analysis/facts/purity"
15
16
"honnef.co/go/tools/analysis/facts/tokenfile"
17
+ "honnef.co/go/tools/analysis/lint"
16
18
"honnef.co/go/tools/go/ast/astutil"
17
19
"honnef.co/go/tools/go/types/typeutil"
18
20
"honnef.co/go/tools/pattern"
@@ -315,13 +317,136 @@ func MayHaveSideEffects(pass *analysis.Pass, expr ast.Expr, purity purity.Result
315
317
}
316
318
}
317
319
318
- func IsGoVersion (pass * analysis.Pass , minor int ) bool {
319
- f , ok := pass .Analyzer .Flags .Lookup ("go" ).Value .(flag.Getter )
320
- if ! ok {
321
- panic ("requested Go version, but analyzer has no version flag" )
320
+ func LanguageVersion (pass * analysis.Pass , node Positioner ) int {
321
+ // As of Go 1.21, two places can specify the minimum Go version:
322
+ // - 'go' directives in go.mod and go.work files
323
+ // - individual files by using '//go:build'
324
+ //
325
+ // Individual files can upgrade to a higher version than the module version. Individual files
326
+ // can also downgrade to a lower version, but only if the module version is at least Go 1.21.
327
+ //
328
+ // The restriction on downgrading doesn't matter to us. All language changes before Go 1.22 will
329
+ // not type-check on versions that are too old, and thus never reach our analyzes. In practice,
330
+ // such ineffective downgrading will always be useless, as the compiler will not restrict the
331
+ // language features used, and doesn't ever rely on minimum versions to restrict the use of the
332
+ // standard library. However, for us, both choices (respecting or ignoring ineffective
333
+ // downgrading) have equal complexity, but only respecting it has a non-zero chance of reducing
334
+ // noisy positives.
335
+ //
336
+ // The minimum Go versions are exposed via go/ast.File.GoVersion and go/types.Package.GoVersion.
337
+ // ast.File's version is populated by the parser, whereas types.Package's version is populated
338
+ // from the Go version specified in the types.Config, which is set by our package loader, based
339
+ // on the module information provided by go/packages, via 'go list -json'.
340
+ //
341
+ // As of Go 1.21, standard library packages do not present themselves as modules, and thus do
342
+ // not have a version set on their types.Package. In this case, we fall back to the version
343
+ // provided by our '-go' flag. In most cases, '-go' defaults to 'module', which falls back to
344
+ // the Go version that Staticcheck was built with when no module information exists. In the
345
+ // future, the standard library will hopefully be a proper module (see
346
+ // https://github.com/golang/go/issues/61174#issuecomment-1622471317). In that case, the version
347
+ // of standard library packages will match that of the used Go version. At that point,
348
+ // Staticcheck will refuse to work with Go versions that are too new, to avoid misinterpreting
349
+ // code due to language changes.
350
+ //
351
+ // We also lack module information when building in GOPATH mode. In this case, the implied
352
+ // language version is at most Go 1.21, as per https://github.com/golang/go/issues/60915. We
353
+ // don't handle this yet, and it will not matter until Go 1.22.
354
+ //
355
+ // It is not clear how per-file downgrading behaves in GOPATH mode. On the one hand, no module
356
+ // version at all is provided, which should preclude per-file downgrading. On the other hand,
357
+ // https://github.com/golang/go/issues/60915 suggests that the language version is at most 1.21
358
+ // in GOPATH mode, which would allow per-file downgrading. Again it doesn't affect us, as all
359
+ // relevant language changes before Go 1.22 will lead to type-checking failures and never reach
360
+ // us.
361
+ //
362
+ // It is not clear if per-file upgrading is possible in GOPATH mode. This needs clarification.
363
+
364
+ f := File (pass , node )
365
+ var n int
366
+ if v := fileGoVersion (f ); v != "" {
367
+ var ok bool
368
+ n , ok = lint .ParseGoVersion (v )
369
+ if ! ok {
370
+ panic (fmt .Sprintf ("unexpected failure parsing version %q" , v ))
371
+ }
372
+ } else if v := packageGoVersion (pass .Pkg ); v != "" {
373
+ var ok bool
374
+ n , ok = lint .ParseGoVersion (v )
375
+ if ! ok {
376
+ panic (fmt .Sprintf ("unexpected failure parsing version %q" , v ))
377
+ }
378
+ } else {
379
+ v , ok := pass .Analyzer .Flags .Lookup ("go" ).Value .(flag.Getter )
380
+ if ! ok {
381
+ panic ("requested Go version, but analyzer has no version flag" )
382
+ }
383
+ n = v .Get ().(int )
384
+ }
385
+
386
+ return n
387
+ }
388
+
389
+ func StdlibVersion (pass * analysis.Pass , node Positioner ) int {
390
+ var n int
391
+ if v := packageGoVersion (pass .Pkg ); v != "" {
392
+ var ok bool
393
+ n , ok = lint .ParseGoVersion (v )
394
+ if ! ok {
395
+ panic (fmt .Sprintf ("unexpected failure parsing version %q" , v ))
396
+ }
397
+ } else {
398
+ v , ok := pass .Analyzer .Flags .Lookup ("go" ).Value .(flag.Getter )
399
+ if ! ok {
400
+ panic ("requested Go version, but analyzer has no version flag" )
401
+ }
402
+ n = v .Get ().(int )
403
+ }
404
+
405
+ f := File (pass , node )
406
+ if f == nil {
407
+ panic (fmt .Sprintf ("no file found for node with position %s" , pass .Fset .PositionFor (node .Pos (), false )))
408
+ }
409
+
410
+ if v := fileGoVersion (f ); v != "" {
411
+ nf , err := strconv .Atoi (strings .TrimPrefix (v , "go1." ))
412
+ if err != nil {
413
+ panic (fmt .Sprintf ("unexpected error: %s" , err ))
414
+ }
415
+
416
+ if n < 21 {
417
+ // Before Go 1.21, the Go version set in go.mod specified the maximum language version
418
+ // available to the module. It wasn't uncommon to set the version to Go 1.20 but only
419
+ // use 1.20 functionality (both language and stdlib) in files tagged for 1.20, and
420
+ // supporting a lower version overall. As such, a file tagged lower than the module
421
+ // version couldn't expect to have access to the standard library of the version set in
422
+ // go.mod.
423
+ //
424
+ // While Go 1.21's behavior has been backported to 1.19.11 and 1.20.6, users'
425
+ // expectations have not.
426
+ n = nf
427
+ } else {
428
+ // Go 1.21 and newer refuse to build modules that depend on versions newer than the Go
429
+ // version. This means that in a 1.22 module with a file tagged as 1.17, the file can
430
+ // expect to have access to 1.22's standard library.
431
+ //
432
+ // Do note that strictly speaking we're conflating the Go version and the module version in
433
+ // our check. Nothing is stopping a user from using Go 1.17 to build a Go 1.22 module, in
434
+ // which case the 1.17 file will not have acces to the 1.22 standard library. However, we
435
+ // believe that if a module requires 1.21 or newer, then the author clearly expects the new
436
+ // behavior, and doesn't care for the old one. Otherwise they would've specified an older
437
+ // version.
438
+ //
439
+ // In other words, the module version also specifies what it itself actually means, with
440
+ // >=1.21 being a minimum version for the toolchain, and <1.21 being a maximum version for
441
+ // the language.
442
+
443
+ if nf > n {
444
+ n = nf
445
+ }
446
+ }
322
447
}
323
- version := f . Get ().( int )
324
- return version >= minor
448
+
449
+ return n
325
450
}
326
451
327
452
var integerLiteralQ = pattern .MustParse (`(IntegerLiteral tv)` )
0 commit comments