all: merge master (9b63f3d) into gopls-release-branch.0.14
Also reinstate the x/tools replace directive.
For golang/go#63220
Conflicts:
- gopls/go.sum
Merge List:
+ 2023-10-11 9b63f3d1d gopls: upgrade x/telemetry dependency
+ 2023-10-11 f38ff07b8 internal/refactor/inline: T{} is duplicable for struct/array
+ 2023-10-11 ecbfa885b go/analysis/passes/timeformat: simplify isTimeDotFormat
+ 2023-10-11 187911b87 internal/refactor/inline: more precise SelectorExpr effects
+ 2023-10-10 dbf6f4218 go/analysis/passes/httpmux: add command
+ 2023-10-10 7e7568c76 go/analysis/passes/httpmux: check for enhanced ServeMux patterns
+ 2023-10-10 b26815635 gopls: allow all drive letters in cache/filemap_test.go
+ 2023-10-10 be4e4d62f go/analysis/passes/internal/analysisutil: account for nil Func.Pkg
+ 2023-10-10 0e4fc907c internal/refactor/inline: add missing spread context (return)
+ 2023-10-10 8954aa7a5 go/types/internal/play: fix slice OOB when *ast.File is selected
+ 2023-10-10 fda3fe3aa gopls/internal/lsp: use the correct options for semantic tokens legend
+ 2023-10-10 f6d8589ec cmd/compilebench: pass linker flags to prebuild
+ 2023-10-09 5874869c2 go/analysis/passes/internal/analysisutil: add IsFunctionNamed
+ 2023-10-09 0b06fd80c cmd/gonew: skip Test if exec is unsupported
+ 2023-10-09 f5fd4c914 go/analysis/passes/bools: use astutil.Unparen
+ 2023-10-09 59ac17fcd go/analysis/passes/internal/analysisutil: remove Unparen
+ 2023-10-08 a3b5082fb go/analysis/passes/appends: improve check for append builtin
+ 2023-10-08 395d39326 go/analysis/passes/internal/analysisutil: add IsNamedType
+ 2023-10-08 22abcd608 oogo/analysis/passes/bools: remove duplicate functions
+ 2023-10-06 3f4194ee2 go.mod: update golang.org/x dependencies
+ 2023-10-05 1e4ce7c30 internal/refactor/inline: yet more tweaks to everything test
Change-Id: Iaf86891ab5af9ee05ada507c75234e750633e0ef
diff --git a/cmd/compilebench/main.go b/cmd/compilebench/main.go
index 681ffd3..15323c2 100644
--- a/cmd/compilebench/main.go
+++ b/cmd/compilebench/main.go
@@ -350,7 +350,7 @@
return err
}
- importcfg, err := genImportcfgFile(c.dir, false)
+ importcfg, err := genImportcfgFile(c.dir, "", false) // TODO: pass compiler flags?
if err != nil {
return err
}
@@ -418,12 +418,19 @@
}
// Build dependencies.
- out, err := exec.Command(*flagGoCmd, "build", "-o", "/dev/null", r.dir).CombinedOutput()
+ ldflags := *flagLinkerFlags
+ if r.flags != "" {
+ if ldflags != "" {
+ ldflags += " "
+ }
+ ldflags += r.flags
+ }
+ out, err := exec.Command(*flagGoCmd, "build", "-o", "/dev/null", "-ldflags="+ldflags, r.dir).CombinedOutput()
if err != nil {
return fmt.Errorf("go build -a %s: %v\n%s", r.dir, err, out)
}
- importcfg, err := genImportcfgFile(r.dir, true)
+ importcfg, err := genImportcfgFile(r.dir, "-ldflags="+ldflags, true)
if err != nil {
return err
}
@@ -643,15 +650,19 @@
// genImportcfgFile generates an importcfg file for building package
// dir. Returns the generated importcfg file path (or empty string
// if the package has no dependency).
-func genImportcfgFile(dir string, full bool) (string, error) {
+func genImportcfgFile(dir string, flags string, full bool) (string, error) {
need := "{{.Imports}}"
if full {
// for linking, we need transitive dependencies
need = "{{.Deps}}"
}
+ if flags == "" {
+ flags = "--" // passing "" to go list, it will match to the current directory
+ }
+
// find imported/dependent packages
- cmd := exec.Command(*flagGoCmd, "list", "-f", need, dir)
+ cmd := exec.Command(*flagGoCmd, "list", "-f", need, flags, dir)
cmd.Stderr = os.Stderr
out, err := cmd.Output()
if err != nil {
@@ -667,7 +678,7 @@
}
// build importcfg for imported packages
- cmd = exec.Command(*flagGoCmd, "list", "-export", "-f", "{{if .Export}}packagefile {{.ImportPath}}={{.Export}}{{end}}")
+ cmd = exec.Command(*flagGoCmd, "list", "-export", "-f", "{{if .Export}}packagefile {{.ImportPath}}={{.Export}}{{end}}", flags)
cmd.Args = append(cmd.Args, strings.Fields(string(out))...)
cmd.Stderr = os.Stderr
out, err = cmd.Output()
diff --git a/cmd/godoc/godoc_test.go b/cmd/godoc/godoc_test.go
index 42582c4..b7b0e1b 100644
--- a/cmd/godoc/godoc_test.go
+++ b/cmd/godoc/godoc_test.go
@@ -47,7 +47,7 @@
func godocPath(t *testing.T) string {
if !testenv.HasExec() {
- t.Skipf("skipping test that requires exec")
+ t.Skipf("skipping test: exec not supported on %s/%s", runtime.GOOS, runtime.GOARCH)
}
exe.once.Do(func() {
diff --git a/cmd/gonew/main_test.go b/cmd/gonew/main_test.go
index 590bda0..142788b 100644
--- a/cmd/gonew/main_test.go
+++ b/cmd/gonew/main_test.go
@@ -17,6 +17,7 @@
"testing"
"golang.org/x/tools/internal/diffp"
+ "golang.org/x/tools/internal/testenv"
"golang.org/x/tools/txtar"
)
@@ -28,6 +29,9 @@
}
func Test(t *testing.T) {
+ if !testenv.HasExec() {
+ t.Skipf("skipping test: exec not supported on %s/%s", runtime.GOOS, runtime.GOARCH)
+ }
exe, err := os.Executable()
if err != nil {
t.Fatal(err)
diff --git a/go.mod b/go.mod
index 4a01af3..9688b9d 100644
--- a/go.mod
+++ b/go.mod
@@ -4,9 +4,9 @@
require (
github.com/yuin/goldmark v1.4.13
- golang.org/x/mod v0.12.0
- golang.org/x/net v0.15.0
- golang.org/x/sys v0.12.0
+ golang.org/x/mod v0.13.0
+ golang.org/x/net v0.16.0
+ golang.org/x/sys v0.13.0
)
-require golang.org/x/sync v0.3.0
+require golang.org/x/sync v0.4.0
diff --git a/go.sum b/go.sum
index 3f717ed..78a350f 100644
--- a/go.sum
+++ b/go.sum
@@ -1,10 +1,10 @@
github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
-golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
-golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
-golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
-golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
-golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
-golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
-golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY=
+golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos=
+golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
+golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
+golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
+golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
+golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
diff --git a/go/analysis/passes/appends/appends.go b/go/analysis/passes/appends/appends.go
index f0b90a4..6976f0d 100644
--- a/go/analysis/passes/appends/appends.go
+++ b/go/analysis/passes/appends/appends.go
@@ -15,6 +15,7 @@
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/analysis/passes/internal/analysisutil"
"golang.org/x/tools/go/ast/inspector"
+ "golang.org/x/tools/go/types/typeutil"
)
//go:embed doc.go
@@ -36,12 +37,9 @@
}
inspect.Preorder(nodeFilter, func(n ast.Node) {
call := n.(*ast.CallExpr)
- if ident, ok := call.Fun.(*ast.Ident); ok && ident.Name == "append" {
- if _, ok := pass.TypesInfo.Uses[ident].(*types.Builtin); ok {
- if len(call.Args) == 1 {
- pass.ReportRangef(call, "append with no values")
- }
- }
+ b, ok := typeutil.Callee(pass.TypesInfo, call).(*types.Builtin)
+ if ok && b.Name() == "append" && len(call.Args) == 1 {
+ pass.ReportRangef(call, "append with no values")
}
})
diff --git a/go/analysis/passes/appends/testdata/src/b/b.go b/go/analysis/passes/appends/testdata/src/b/b.go
index 87a04c4..b4e99d4 100644
--- a/go/analysis/passes/appends/testdata/src/b/b.go
+++ b/go/analysis/passes/appends/testdata/src/b/b.go
@@ -16,3 +16,9 @@
sli = append(sli, 4, 5, 6)
sli = append(sli)
}
+
+func localvar() {
+ append := func(int) int { return 0 }
+ a := append(1)
+ _ = a
+}
diff --git a/go/analysis/passes/assign/assign.go b/go/analysis/passes/assign/assign.go
index 10489be..3bfd501 100644
--- a/go/analysis/passes/assign/assign.go
+++ b/go/analysis/passes/assign/assign.go
@@ -18,6 +18,7 @@
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/analysis/passes/internal/analysisutil"
+ "golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/go/ast/inspector"
)
@@ -77,7 +78,7 @@
// isMapIndex returns true if e is a map index expression.
func isMapIndex(info *types.Info, e ast.Expr) bool {
- if idx, ok := analysisutil.Unparen(e).(*ast.IndexExpr); ok {
+ if idx, ok := astutil.Unparen(e).(*ast.IndexExpr); ok {
if typ := info.Types[idx.X].Type; typ != nil {
_, ok := typ.Underlying().(*types.Map)
return ok
diff --git a/go/analysis/passes/atomic/atomic.go b/go/analysis/passes/atomic/atomic.go
index b40e081..931f9ca 100644
--- a/go/analysis/passes/atomic/atomic.go
+++ b/go/analysis/passes/atomic/atomic.go
@@ -8,12 +8,12 @@
_ "embed"
"go/ast"
"go/token"
- "go/types"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/analysis/passes/internal/analysisutil"
"golang.org/x/tools/go/ast/inspector"
+ "golang.org/x/tools/go/types/typeutil"
)
//go:embed doc.go
@@ -52,18 +52,8 @@
if !ok {
continue
}
- sel, ok := call.Fun.(*ast.SelectorExpr)
- if !ok {
- continue
- }
- pkgIdent, _ := sel.X.(*ast.Ident)
- pkgName, ok := pass.TypesInfo.Uses[pkgIdent].(*types.PkgName)
- if !ok || pkgName.Imported().Path() != "sync/atomic" {
- continue
- }
-
- switch sel.Sel.Name {
- case "AddInt32", "AddInt64", "AddUint32", "AddUint64", "AddUintptr":
+ fn := typeutil.StaticCallee(pass.TypesInfo, call)
+ if analysisutil.IsFunctionNamed(fn, "sync/atomic", "AddInt32", "AddInt64", "AddUint32", "AddUint64", "AddUintptr") {
checkAtomicAddAssignment(pass, n.Lhs[i], call)
}
}
diff --git a/go/analysis/passes/atomic/testdata/src/a/a.go b/go/analysis/passes/atomic/testdata/src/a/a.go
index dc12bd0..e784605 100644
--- a/go/analysis/passes/atomic/testdata/src/a/a.go
+++ b/go/analysis/passes/atomic/testdata/src/a/a.go
@@ -14,9 +14,10 @@
func AtomicTests() {
x := uint64(1)
- x = atomic.AddUint64(&x, 1) // want "direct assignment to atomic value"
- _, x = 10, atomic.AddUint64(&x, 1) // want "direct assignment to atomic value"
- x, _ = atomic.AddUint64(&x, 1), 10 // want "direct assignment to atomic value"
+ x = atomic.AddUint64(&x, 1) // want "direct assignment to atomic value"
+ _, x = 10, atomic.AddUint64(&x, 1) // want "direct assignment to atomic value"
+ x, _ = atomic.AddUint64(&x, 1), 10 // want "direct assignment to atomic value"
+ x, _ = (atomic.AddUint64)(&x, 1), 10 // want "direct assignment to atomic value"
y := &x
*y = atomic.AddUint64(y, 1) // want "direct assignment to atomic value"
diff --git a/go/analysis/passes/atomicalign/atomicalign.go b/go/analysis/passes/atomicalign/atomicalign.go
index 01683e4..aff6d25 100644
--- a/go/analysis/passes/atomicalign/atomicalign.go
+++ b/go/analysis/passes/atomicalign/atomicalign.go
@@ -18,6 +18,7 @@
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/analysis/passes/internal/analysisutil"
"golang.org/x/tools/go/ast/inspector"
+ "golang.org/x/tools/go/types/typeutil"
)
const Doc = "check for non-64-bits-aligned arguments to sync/atomic functions"
@@ -42,31 +43,20 @@
nodeFilter := []ast.Node{
(*ast.CallExpr)(nil),
}
+ funcNames := []string{
+ "AddInt64", "AddUint64",
+ "LoadInt64", "LoadUint64",
+ "StoreInt64", "StoreUint64",
+ "SwapInt64", "SwapUint64",
+ "CompareAndSwapInt64", "CompareAndSwapUint64",
+ }
inspect.Preorder(nodeFilter, func(node ast.Node) {
call := node.(*ast.CallExpr)
- sel, ok := call.Fun.(*ast.SelectorExpr)
- if !ok {
- return
- }
- pkgIdent, ok := sel.X.(*ast.Ident)
- if !ok {
- return
- }
- pkgName, ok := pass.TypesInfo.Uses[pkgIdent].(*types.PkgName)
- if !ok || pkgName.Imported().Path() != "sync/atomic" {
- return
- }
-
- switch sel.Sel.Name {
- case "AddInt64", "AddUint64",
- "LoadInt64", "LoadUint64",
- "StoreInt64", "StoreUint64",
- "SwapInt64", "SwapUint64",
- "CompareAndSwapInt64", "CompareAndSwapUint64":
-
+ fn := typeutil.StaticCallee(pass.TypesInfo, call)
+ if analysisutil.IsFunctionNamed(fn, "sync/atomic", funcNames...) {
// For all the listed functions, the expression to check is always the first function argument.
- check64BitAlignment(pass, sel.Sel.Name, call.Args[0])
+ check64BitAlignment(pass, fn.Name(), call.Args[0])
}
})
diff --git a/go/analysis/passes/atomicalign/testdata/src/a/a.go b/go/analysis/passes/atomicalign/testdata/src/a/a.go
index 45dd73d..deebc30 100644
--- a/go/analysis/passes/atomicalign/testdata/src/a/a.go
+++ b/go/analysis/passes/atomicalign/testdata/src/a/a.go
@@ -4,6 +4,7 @@
// This file contains tests for the atomic alignment checker.
+//go:build arm || 386
// +build arm 386
package testdata
@@ -102,7 +103,8 @@
atomic.LoadInt64(&a.b) // want "address of non 64-bit aligned field .b passed to atomic.LoadInt64"
atomic.LoadInt64(&a.c)
- atomic.LoadUint64(&a.e) // want "address of non 64-bit aligned field .e passed to atomic.LoadUint64"
+ atomic.LoadUint64(&a.e) // want "address of non 64-bit aligned field .e passed to atomic.LoadUint64"
+ (atomic.LoadUint64)(&a.e) // want "address of non 64-bit aligned field .e passed to atomic.LoadUint64"
}
func anonymousFieldAlignment() {
diff --git a/go/analysis/passes/bools/bools.go b/go/analysis/passes/bools/bools.go
index 4219f08..5643297 100644
--- a/go/analysis/passes/bools/bools.go
+++ b/go/analysis/passes/bools/bools.go
@@ -14,6 +14,7 @@
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/analysis/passes/internal/analysisutil"
+ "golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/go/ast/inspector"
)
@@ -83,7 +84,7 @@
i := 0
var sets [][]ast.Expr
for j := 0; j <= len(exprs); j++ {
- if j == len(exprs) || hasSideEffects(info, exprs[j]) {
+ if j == len(exprs) || analysisutil.HasSideEffects(info, exprs[j]) {
if i < j {
sets = append(sets, exprs[i:j])
}
@@ -162,46 +163,13 @@
}
}
-// hasSideEffects reports whether evaluation of e has side effects.
-func hasSideEffects(info *types.Info, e ast.Expr) bool {
- safe := true
- ast.Inspect(e, func(node ast.Node) bool {
- switch n := node.(type) {
- case *ast.CallExpr:
- typVal := info.Types[n.Fun]
- switch {
- case typVal.IsType():
- // Type conversion, which is safe.
- case typVal.IsBuiltin():
- // Builtin func, conservatively assumed to not
- // be safe for now.
- safe = false
- return false
- default:
- // A non-builtin func or method call.
- // Conservatively assume that all of them have
- // side effects for now.
- safe = false
- return false
- }
- case *ast.UnaryExpr:
- if n.Op == token.ARROW {
- safe = false
- return false
- }
- }
- return true
- })
- return !safe
-}
-
// split returns a slice of all subexpressions in e that are connected by op.
// For example, given 'a || (b || c) || d' with the or op,
// split returns []{d, c, b, a}.
// seen[e] is already true; any newly processed exprs are added to seen.
func (op boolOp) split(e ast.Expr, seen map[*ast.BinaryExpr]bool) (exprs []ast.Expr) {
for {
- e = unparen(e)
+ e = astutil.Unparen(e)
if b, ok := e.(*ast.BinaryExpr); ok && b.Op == op.tok {
seen[b] = true
exprs = append(exprs, op.split(b.Y, seen)...)
@@ -213,14 +181,3 @@
}
return
}
-
-// unparen returns e with any enclosing parentheses stripped.
-func unparen(e ast.Expr) ast.Expr {
- for {
- p, ok := e.(*ast.ParenExpr)
- if !ok {
- return e
- }
- e = p.X
- }
-}
diff --git a/go/analysis/passes/cgocall/cgocall.go b/go/analysis/passes/cgocall/cgocall.go
index 98d9a77..4e86439 100644
--- a/go/analysis/passes/cgocall/cgocall.go
+++ b/go/analysis/passes/cgocall/cgocall.go
@@ -19,6 +19,7 @@
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/internal/analysisutil"
+ "golang.org/x/tools/go/ast/astutil"
)
const debug = false
@@ -64,7 +65,7 @@
// Is this a C.f() call?
var name string
- if sel, ok := analysisutil.Unparen(call.Fun).(*ast.SelectorExpr); ok {
+ if sel, ok := astutil.Unparen(call.Fun).(*ast.SelectorExpr); ok {
if id, ok := sel.X.(*ast.Ident); ok && id.Name == "C" {
name = sel.Sel.Name
}
diff --git a/go/analysis/passes/copylock/copylock.go b/go/analysis/passes/copylock/copylock.go
index ec7727d..2eeb0a3 100644
--- a/go/analysis/passes/copylock/copylock.go
+++ b/go/analysis/passes/copylock/copylock.go
@@ -16,6 +16,7 @@
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/analysis/passes/internal/analysisutil"
+ "golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/internal/typeparams"
)
@@ -223,7 +224,7 @@
}
func lockPathRhs(pass *analysis.Pass, x ast.Expr) typePath {
- x = analysisutil.Unparen(x) // ignore parens on rhs
+ x = astutil.Unparen(x) // ignore parens on rhs
if _, ok := x.(*ast.CompositeLit); ok {
return nil
@@ -233,7 +234,7 @@
return nil
}
if star, ok := x.(*ast.StarExpr); ok {
- if _, ok := analysisutil.Unparen(star.X).(*ast.CallExpr); ok {
+ if _, ok := astutil.Unparen(star.X).(*ast.CallExpr); ok {
// A call may return a pointer to a zero value.
return nil
}
@@ -319,9 +320,7 @@
// In go1.10, sync.noCopy did not implement Locker.
// (The Unlock method was added only in CL 121876.)
// TODO(adonovan): remove workaround when we drop go1.10.
- if named, ok := typ.(*types.Named); ok &&
- named.Obj().Name() == "noCopy" &&
- named.Obj().Pkg().Path() == "sync" {
+ if analysisutil.IsNamedType(typ, "sync", "noCopy") {
return []string{typ.String()}
}
diff --git a/go/analysis/passes/deepequalerrors/deepequalerrors.go b/go/analysis/passes/deepequalerrors/deepequalerrors.go
index 3a18187..1a83bdd 100644
--- a/go/analysis/passes/deepequalerrors/deepequalerrors.go
+++ b/go/analysis/passes/deepequalerrors/deepequalerrors.go
@@ -46,11 +46,8 @@
}
inspect.Preorder(nodeFilter, func(n ast.Node) {
call := n.(*ast.CallExpr)
- fn, ok := typeutil.Callee(pass.TypesInfo, call).(*types.Func)
- if !ok {
- return
- }
- if fn.FullName() == "reflect.DeepEqual" && hasError(pass, call.Args[0]) && hasError(pass, call.Args[1]) {
+ fn, _ := typeutil.Callee(pass.TypesInfo, call).(*types.Func)
+ if analysisutil.IsFunctionNamed(fn, "reflect", "DeepEqual") && hasError(pass, call.Args[0]) && hasError(pass, call.Args[1]) {
pass.ReportRangef(call, "avoid using reflect.DeepEqual with errors")
}
})
diff --git a/go/analysis/passes/defers/defers.go b/go/analysis/passes/defers/defers.go
index ed2a122..5e8e80a 100644
--- a/go/analysis/passes/defers/defers.go
+++ b/go/analysis/passes/defers/defers.go
@@ -7,7 +7,6 @@
import (
_ "embed"
"go/ast"
- "go/types"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
@@ -36,8 +35,7 @@
checkDeferCall := func(node ast.Node) bool {
switch v := node.(type) {
case *ast.CallExpr:
- fn, ok := typeutil.Callee(pass.TypesInfo, v).(*types.Func)
- if ok && fn.Name() == "Since" && fn.Pkg().Path() == "time" {
+ if analysisutil.IsFunctionNamed(typeutil.StaticCallee(pass.TypesInfo, v), "time", "Since") {
pass.Reportf(v.Pos(), "call to time.Since is not deferred")
}
case *ast.FuncLit:
diff --git a/go/analysis/passes/defers/testdata/src/a/a.go b/go/analysis/passes/defers/testdata/src/a/a.go
index e8bc8cd..6f0118a 100644
--- a/go/analysis/passes/defers/testdata/src/a/a.go
+++ b/go/analysis/passes/defers/testdata/src/a/a.go
@@ -26,8 +26,8 @@
defer fmt.Println(evalBefore)
do := func(f func()) {}
defer do(func() { time.Since(now) })
- defer fmt.Println(Since()) // OK because Since function is not in module time
-
+ defer fmt.Println(Since()) // OK because Since function is not in module time
+ defer copy([]int(nil), []int{1}) // check that a builtin doesn't cause a panic
}
type y struct{}
diff --git a/go/analysis/passes/errorsas/errorsas.go b/go/analysis/passes/errorsas/errorsas.go
index 2fcbdfa..43996b8 100644
--- a/go/analysis/passes/errorsas/errorsas.go
+++ b/go/analysis/passes/errorsas/errorsas.go
@@ -51,15 +51,12 @@
inspect.Preorder(nodeFilter, func(n ast.Node) {
call := n.(*ast.CallExpr)
fn := typeutil.StaticCallee(pass.TypesInfo, call)
- if fn == nil {
- return // not a static call
+ if !analysisutil.IsFunctionNamed(fn, "errors", "As") {
+ return
}
if len(call.Args) < 2 {
return // not enough arguments, e.g. called with return values of another function
}
- if fn.FullName() != "errors.As" {
- return
- }
if err := checkAsTarget(pass, call.Args[1]); err != nil {
pass.ReportRangef(call, "%v", err)
}
diff --git a/go/analysis/passes/httpmux/cmd/httpmux/main.go b/go/analysis/passes/httpmux/cmd/httpmux/main.go
new file mode 100644
index 0000000..e8a6311
--- /dev/null
+++ b/go/analysis/passes/httpmux/cmd/httpmux/main.go
@@ -0,0 +1,13 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// The httpmux command runs the httpmux analyzer.
+package main
+
+import (
+ "golang.org/x/tools/go/analysis/passes/httpmux"
+ "golang.org/x/tools/go/analysis/singlechecker"
+)
+
+func main() { singlechecker.Main(httpmux.Analyzer) }
diff --git a/go/analysis/passes/httpmux/httpmux.go b/go/analysis/passes/httpmux/httpmux.go
new file mode 100644
index 0000000..fa99296
--- /dev/null
+++ b/go/analysis/passes/httpmux/httpmux.go
@@ -0,0 +1,186 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package httpmux
+
+import (
+ "go/ast"
+ "go/constant"
+ "go/types"
+ "regexp"
+ "strings"
+
+ "golang.org/x/mod/semver"
+ "golang.org/x/tools/go/analysis"
+ "golang.org/x/tools/go/analysis/passes/inspect"
+ "golang.org/x/tools/go/analysis/passes/internal/analysisutil"
+ "golang.org/x/tools/go/ast/inspector"
+ "golang.org/x/tools/go/types/typeutil"
+)
+
+const Doc = `report using Go 1.22 enhanced ServeMux patterns in older Go versions
+
+The httpmux analysis is active for Go modules configured to run with Go 1.21 or
+earlier versions. It reports calls to net/http.ServeMux.Handle and HandleFunc
+methods whose patterns use features added in Go 1.22, like HTTP methods (such as
+"GET") and wildcards. (See https://pkg.go.dev/net/http#ServeMux for details.)
+Such patterns can be registered in older versions of Go, but will not behave as expected.`
+
+var Analyzer = &analysis.Analyzer{
+ Name: "httpmux",
+ Doc: Doc,
+ URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/httpmux",
+ Requires: []*analysis.Analyzer{inspect.Analyzer},
+ Run: run,
+}
+
+var inTest bool // So Go version checks can be skipped during testing.
+
+func run(pass *analysis.Pass) (any, error) {
+ if !inTest {
+ // Check that Go version is 1.21 or earlier.
+ if goVersionAfter121(goVersion(pass.Pkg)) {
+ return nil, nil
+ }
+ }
+ if !analysisutil.Imports(pass.Pkg, "net/http") {
+ return nil, nil
+ }
+ // Look for calls to ServeMux.Handle or ServeMux.HandleFunc.
+ inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
+
+ nodeFilter := []ast.Node{
+ (*ast.CallExpr)(nil),
+ }
+
+ inspect.Preorder(nodeFilter, func(n ast.Node) {
+ call := n.(*ast.CallExpr)
+ if isServeMuxRegisterCall(pass, call) {
+ pat, ok := stringConstantExpr(pass, call.Args[0])
+ if ok && likelyEnhancedPattern(pat) {
+ pass.ReportRangef(call.Args[0], "possible enhanced ServeMux pattern used with Go version before 1.22 (update go.mod file?)")
+ }
+ }
+ })
+ return nil, nil
+}
+
+// isServeMuxRegisterCall reports whether call is a static call to one of:
+// - net/http.Handle
+// - net/http.HandleFunc
+// - net/http.ServeMux.Handle
+// - net/http.ServeMux.HandleFunc
+// TODO(jba): consider expanding this to accommodate wrappers around these functions.
+func isServeMuxRegisterCall(pass *analysis.Pass, call *ast.CallExpr) bool {
+ fn := typeutil.StaticCallee(pass.TypesInfo, call)
+ if fn == nil {
+ return false
+ }
+ if analysisutil.IsFunctionNamed(fn, "net/http", "Handle", "HandleFunc") {
+ return true
+ }
+ if !isMethodNamed(fn, "net/http", "Handle", "HandleFunc") {
+ return false
+ }
+ t, ok := fn.Type().(*types.Signature).Recv().Type().(*types.Pointer)
+ if !ok {
+ return false
+ }
+ return analysisutil.IsNamedType(t.Elem(), "net/http", "ServeMux")
+}
+
+func isMethodNamed(f *types.Func, pkgPath string, names ...string) bool {
+ if f == nil {
+ return false
+ }
+ if f.Pkg() == nil || f.Pkg().Path() != pkgPath {
+ return false
+ }
+ if f.Type().(*types.Signature).Recv() == nil {
+ return false
+ }
+ for _, n := range names {
+ if f.Name() == n {
+ return true
+ }
+ }
+ return false
+}
+
+// stringConstantExpr returns expression's string constant value.
+//
+// ("", false) is returned if expression isn't a string
+// constant.
+func stringConstantExpr(pass *analysis.Pass, expr ast.Expr) (string, bool) {
+ lit := pass.TypesInfo.Types[expr].Value
+ if lit != nil && lit.Kind() == constant.String {
+ return constant.StringVal(lit), true
+ }
+ return "", false
+}
+
+// A valid wildcard must start a segment, and its name must be valid Go
+// identifier.
+var wildcardRegexp = regexp.MustCompile(`/\{[_\pL][_\pL\p{Nd}]*(\.\.\.)?\}`)
+
+// likelyEnhancedPattern reports whether the ServeMux pattern pat probably
+// contains either an HTTP method name or a wildcard, extensions added in Go 1.22.
+func likelyEnhancedPattern(pat string) bool {
+ if strings.Contains(pat, " ") {
+ // A space in the pattern suggests that it begins with an HTTP method.
+ return true
+ }
+ return wildcardRegexp.MatchString(pat)
+}
+
+func goVersionAfter121(goVersion string) bool {
+ if goVersion == "" { // Maybe the stdlib?
+ return true
+ }
+ version := versionFromGoVersion(goVersion)
+ return semver.Compare(version, "v1.21") > 0
+}
+
+func goVersion(pkg *types.Package) string {
+ // types.Package.GoVersion did not exist before Go 1.21.
+ if p, ok := any(pkg).(interface{ GoVersion() string }); ok {
+ return p.GoVersion()
+ }
+ return ""
+}
+
+var (
+ // Regexp for matching go tags. The groups are:
+ // 1 the major.minor version
+ // 2 the patch version, or empty if none
+ // 3 the entire prerelease, if present
+ // 4 the prerelease type ("beta" or "rc")
+ // 5 the prerelease number
+ tagRegexp = regexp.MustCompile(`^go(\d+\.\d+)(\.\d+|)((beta|rc)(\d+))?$`)
+)
+
+// Copied from pkgsite/internal/stdlib.VersionForTag.
+func versionFromGoVersion(goVersion string) string {
+ // Special cases for go1.
+ if goVersion == "go1" {
+ return "v1.0.0"
+ }
+ if goVersion == "go1.0" {
+ return ""
+ }
+ m := tagRegexp.FindStringSubmatch(goVersion)
+ if m == nil {
+ return ""
+ }
+ version := "v" + m[1]
+ if m[2] != "" {
+ version += m[2]
+ } else {
+ version += ".0"
+ }
+ if m[3] != "" {
+ version += "-" + m[4] + "." + m[5]
+ }
+ return version
+}
diff --git a/go/analysis/passes/httpmux/httpmux_test.go b/go/analysis/passes/httpmux/httpmux_test.go
new file mode 100644
index 0000000..f2cb9c7
--- /dev/null
+++ b/go/analysis/passes/httpmux/httpmux_test.go
@@ -0,0 +1,37 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package httpmux
+
+import (
+ "testing"
+
+ "golang.org/x/tools/go/analysis/analysistest"
+)
+
+func Test(t *testing.T) {
+ testdata := analysistest.TestData()
+ tests := []string{"a"}
+ inTest = true
+ analysistest.Run(t, testdata, Analyzer, tests...)
+}
+
+func TestGoVersion(t *testing.T) {
+ for _, test := range []struct {
+ in string
+ want bool
+ }{
+ {"", true},
+ {"go1", false},
+ {"go1.21", false},
+ {"go1.21rc3", false},
+ {"go1.22", true},
+ {"go1.22rc1", true},
+ } {
+ got := goVersionAfter121(test.in)
+ if got != test.want {
+ t.Errorf("%q: got %t, want %t", test.in, got, test.want)
+ }
+ }
+}
diff --git a/go/analysis/passes/httpmux/testdata/src/a/a.go b/go/analysis/passes/httpmux/testdata/src/a/a.go
new file mode 100644
index 0000000..ad5b3ba
--- /dev/null
+++ b/go/analysis/passes/httpmux/testdata/src/a/a.go
@@ -0,0 +1,47 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This file contains tests for the httpmux checker.
+
+package a
+
+import "net/http"
+
+func _() {
+ http.HandleFunc("GET /x", nil) // want "enhanced ServeMux pattern"
+ http.HandleFunc("/{a}/b/", nil) // want "enhanced ServeMux pattern"
+ mux := http.NewServeMux()
+ mux.Handle("example.com/c/{d}", nil) // want "enhanced ServeMux pattern"
+ mux.HandleFunc("/{x...}", nil) // want "enhanced ServeMux pattern"
+
+ // Should not match.
+
+ // not an enhanced pattern
+ http.Handle("/", nil)
+
+ // invalid wildcard; will panic in 1.22
+ http.HandleFunc("/{/a/}", nil)
+ mux.Handle("/{1}", nil)
+ mux.Handle("/x{a}", nil)
+
+ // right package, wrong method
+ http.ParseTime("GET /")
+
+ // right function name, wrong package
+ Handle("GET /", nil)
+ HandleFunc("GET /", nil)
+
+ // right method name, wrong type
+ var sm ServeMux
+ sm.Handle("example.com/c/{d}", nil)
+ sm.HandleFunc("method /{x...}", nil)
+}
+
+func Handle(pat string, x any) {}
+func HandleFunc(pat string, x any) {}
+
+type ServeMux struct{}
+
+func (*ServeMux) Handle(pat string, x any) {}
+func (*ServeMux) HandleFunc(pat string, x any) {}
diff --git a/go/analysis/passes/httpresponse/httpresponse.go b/go/analysis/passes/httpresponse/httpresponse.go
index 61c3b76..c6b6c81 100644
--- a/go/analysis/passes/httpresponse/httpresponse.go
+++ b/go/analysis/passes/httpresponse/httpresponse.go
@@ -116,7 +116,7 @@
if res.Len() != 2 {
return false // the function called does not return two values.
}
- if ptr, ok := res.At(0).Type().(*types.Pointer); !ok || !isNamedType(ptr.Elem(), "net/http", "Response") {
+ if ptr, ok := res.At(0).Type().(*types.Pointer); !ok || !analysisutil.IsNamedType(ptr.Elem(), "net/http", "Response") {
return false // the first return type is not *http.Response.
}
@@ -131,11 +131,11 @@
return ok && id.Name == "http" // function in net/http package.
}
- if isNamedType(typ, "net/http", "Client") {
+ if analysisutil.IsNamedType(typ, "net/http", "Client") {
return true // method on http.Client.
}
ptr, ok := typ.(*types.Pointer)
- return ok && isNamedType(ptr.Elem(), "net/http", "Client") // method on *http.Client.
+ return ok && analysisutil.IsNamedType(ptr.Elem(), "net/http", "Client") // method on *http.Client.
}
// restOfBlock, given a traversal stack, finds the innermost containing
@@ -171,13 +171,3 @@
return nil
}
}
-
-// isNamedType reports whether t is the named type path.name.
-func isNamedType(t types.Type, path, name string) bool {
- n, ok := t.(*types.Named)
- if !ok {
- return false
- }
- obj := n.Obj()
- return obj.Name() == name && obj.Pkg() != nil && obj.Pkg().Path() == path
-}
diff --git a/go/analysis/passes/internal/analysisutil/util.go b/go/analysis/passes/internal/analysisutil/util.go
index a8d8403..c006075 100644
--- a/go/analysis/passes/internal/analysisutil/util.go
+++ b/go/analysis/passes/internal/analysisutil/util.go
@@ -55,17 +55,6 @@
return !safe
}
-// Unparen returns e with any enclosing parentheses stripped.
-func Unparen(e ast.Expr) ast.Expr {
- for {
- p, ok := e.(*ast.ParenExpr)
- if !ok {
- return e
- }
- e = p.X
- }
-}
-
// ReadFile reads a file and adds it to the FileSet
// so that we can report errors against it using lineStart.
func ReadFile(fset *token.FileSet, filename string) ([]byte, *token.File, error) {
@@ -118,3 +107,46 @@
}
return false
}
+
+// IsNamedType reports whether t is the named type with the given package path
+// and one of the given names.
+// This function avoids allocating the concatenation of "pkg.Name",
+// which is important for the performance of syntax matching.
+func IsNamedType(t types.Type, pkgPath string, names ...string) bool {
+ n, ok := t.(*types.Named)
+ if !ok {
+ return false
+ }
+ obj := n.Obj()
+ if obj == nil || obj.Pkg() == nil || obj.Pkg().Path() != pkgPath {
+ return false
+ }
+ name := obj.Name()
+ for _, n := range names {
+ if name == n {
+ return true
+ }
+ }
+ return false
+}
+
+// IsFunctionNamed reports whether f is a top-level function defined in the
+// given package and has one of the given names.
+// It returns false if f is nil or a method.
+func IsFunctionNamed(f *types.Func, pkgPath string, names ...string) bool {
+ if f == nil {
+ return false
+ }
+ if f.Pkg() == nil || f.Pkg().Path() != pkgPath {
+ return false
+ }
+ if f.Type().(*types.Signature).Recv() != nil {
+ return false
+ }
+ for _, n := range names {
+ if f.Name() == n {
+ return true
+ }
+ }
+ return false
+}
diff --git a/go/analysis/passes/loopclosure/loopclosure.go b/go/analysis/passes/loopclosure/loopclosure.go
index 5620c35..fbcdc22 100644
--- a/go/analysis/passes/loopclosure/loopclosure.go
+++ b/go/analysis/passes/loopclosure/loopclosure.go
@@ -359,20 +359,5 @@
if ptr, ok := recv.Type().(*types.Pointer); ok {
rtype = ptr.Elem()
}
- named, ok := rtype.(*types.Named)
- if !ok {
- return false
- }
- if named.Obj().Name() != typeName {
- return false
- }
- pkg := f.Pkg()
- if pkg == nil {
- return false
- }
- if pkg.Path() != pkgPath {
- return false
- }
-
- return true
+ return analysisutil.IsNamedType(rtype, pkgPath, typeName)
}
diff --git a/go/analysis/passes/printf/printf.go b/go/analysis/passes/printf/printf.go
index b2b8c67..070654f 100644
--- a/go/analysis/passes/printf/printf.go
+++ b/go/analysis/passes/printf/printf.go
@@ -511,15 +511,10 @@
sig := fn.Type().(*types.Signature)
return sig.Params().Len() == 2 &&
sig.Results().Len() == 0 &&
- isNamed(sig.Params().At(0).Type(), "fmt", "State") &&
+ analysisutil.IsNamedType(sig.Params().At(0).Type(), "fmt", "State") &&
types.Identical(sig.Params().At(1).Type(), types.Typ[types.Rune])
}
-func isNamed(T types.Type, pkgpath, name string) bool {
- named, ok := T.(*types.Named)
- return ok && named.Obj().Pkg().Path() == pkgpath && named.Obj().Name() == name
-}
-
// formatState holds the parsed representation of a printf directive such as "%3.*[4]d".
// It is constructed by parsePrintfVerb.
type formatState struct {
diff --git a/go/analysis/passes/reflectvaluecompare/reflectvaluecompare.go b/go/analysis/passes/reflectvaluecompare/reflectvaluecompare.go
index 2767713..6789d73 100644
--- a/go/analysis/passes/reflectvaluecompare/reflectvaluecompare.go
+++ b/go/analysis/passes/reflectvaluecompare/reflectvaluecompare.go
@@ -49,11 +49,8 @@
}
}
case *ast.CallExpr:
- fn, ok := typeutil.Callee(pass.TypesInfo, n).(*types.Func)
- if !ok {
- return
- }
- if fn.FullName() == "reflect.DeepEqual" && (isReflectValue(pass, n.Args[0]) || isReflectValue(pass, n.Args[1])) {
+ fn, _ := typeutil.Callee(pass.TypesInfo, n).(*types.Func)
+ if analysisutil.IsFunctionNamed(fn, "reflect", "DeepEqual") && (isReflectValue(pass, n.Args[0]) || isReflectValue(pass, n.Args[1])) {
pass.ReportRangef(n, "avoid using reflect.DeepEqual with reflect.Value")
}
}
@@ -68,11 +65,7 @@
return false
}
// See if the type is reflect.Value
- named, ok := tv.Type.(*types.Named)
- if !ok {
- return false
- }
- if obj := named.Obj(); obj == nil || obj.Pkg() == nil || obj.Pkg().Path() != "reflect" || obj.Name() != "Value" {
+ if !analysisutil.IsNamedType(tv.Type, "reflect", "Value") {
return false
}
if _, ok := e.(*ast.CompositeLit); ok {
diff --git a/go/analysis/passes/slog/slog.go b/go/analysis/passes/slog/slog.go
index 92c1da8..a1323c3 100644
--- a/go/analysis/passes/slog/slog.go
+++ b/go/analysis/passes/slog/slog.go
@@ -139,7 +139,7 @@
}
func isAttr(t types.Type) bool {
- return isNamed(t, "log/slog", "Attr")
+ return analysisutil.IsNamedType(t, "log/slog", "Attr")
}
// shortName returns a name for the function that is shorter than FullName.
@@ -232,12 +232,3 @@
sel := info.Selections[s]
return sel != nil && sel.Kind() == types.MethodExpr
}
-
-// isNamed reports whether t is exactly a named type in a package with a given path.
-func isNamed(t types.Type, path, name string) bool {
- if n, ok := t.(*types.Named); ok {
- obj := n.Obj()
- return obj.Pkg() != nil && obj.Pkg().Path() == path && obj.Name() == name
- }
- return false
-}
diff --git a/go/analysis/passes/sortslice/analyzer.go b/go/analysis/passes/sortslice/analyzer.go
index 1fe206b..6c151a0 100644
--- a/go/analysis/passes/sortslice/analyzer.go
+++ b/go/analysis/passes/sortslice/analyzer.go
@@ -47,12 +47,7 @@
inspect.Preorder(nodeFilter, func(n ast.Node) {
call := n.(*ast.CallExpr)
fn, _ := typeutil.Callee(pass.TypesInfo, call).(*types.Func)
- if fn == nil {
- return
- }
-
- fnName := fn.FullName()
- if fnName != "sort.Slice" && fnName != "sort.SliceStable" && fnName != "sort.SliceIsSorted" {
+ if !analysisutil.IsFunctionNamed(fn, "sort", "Slice", "SliceStable", "SliceIsSorted") {
return
}
@@ -131,7 +126,7 @@
pass.Report(analysis.Diagnostic{
Pos: call.Pos(),
End: call.End(),
- Message: fmt.Sprintf("%s's argument must be a slice; is called with %s", fnName, typ.String()),
+ Message: fmt.Sprintf("%s's argument must be a slice; is called with %s", fn.FullName(), typ.String()),
SuggestedFixes: fixes,
})
})
diff --git a/go/analysis/passes/tests/tests.go b/go/analysis/passes/tests/tests.go
index 9589a46..d0b0ebb 100644
--- a/go/analysis/passes/tests/tests.go
+++ b/go/analysis/passes/tests/tests.go
@@ -257,13 +257,7 @@
if !ok {
return false
}
- named, ok := ptr.Elem().(*types.Named)
- if !ok {
- return false
- }
- obj := named.Obj()
- // obj.Pkg is nil for the error type.
- return obj != nil && obj.Pkg() != nil && obj.Pkg().Path() == "testing" && obj.Name() == testingType
+ return analysisutil.IsNamedType(ptr.Elem(), "testing", testingType)
}
// Validate that fuzz target function's arguments are of accepted types.
diff --git a/go/analysis/passes/timeformat/timeformat.go b/go/analysis/passes/timeformat/timeformat.go
index c45b9fa..eb84502 100644
--- a/go/analysis/passes/timeformat/timeformat.go
+++ b/go/analysis/passes/timeformat/timeformat.go
@@ -88,29 +88,16 @@
}
func isTimeDotFormat(f *types.Func) bool {
- if f.Name() != "Format" || f.Pkg().Path() != "time" {
- return false
- }
- sig, ok := f.Type().(*types.Signature)
- if !ok {
+ if f.Name() != "Format" || f.Pkg() == nil || f.Pkg().Path() != "time" {
return false
}
// Verify that the receiver is time.Time.
- recv := sig.Recv()
- if recv == nil {
- return false
- }
- named, ok := recv.Type().(*types.Named)
- return ok && named.Obj().Name() == "Time"
+ recv := f.Type().(*types.Signature).Recv()
+ return recv != nil && analysisutil.IsNamedType(recv.Type(), "time", "Time")
}
func isTimeDotParse(f *types.Func) bool {
- if f.Name() != "Parse" || f.Pkg().Path() != "time" {
- return false
- }
- // Verify that there is no receiver.
- sig, ok := f.Type().(*types.Signature)
- return ok && sig.Recv() == nil
+ return analysisutil.IsFunctionNamed(f, "time", "Parse")
}
// badFormatAt return the start of a bad format in e or -1 if no bad format is found.
diff --git a/go/analysis/passes/unsafeptr/unsafeptr.go b/go/analysis/passes/unsafeptr/unsafeptr.go
index e43ac20..32e71ef 100644
--- a/go/analysis/passes/unsafeptr/unsafeptr.go
+++ b/go/analysis/passes/unsafeptr/unsafeptr.go
@@ -15,6 +15,7 @@
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/analysis/passes/internal/analysisutil"
+ "golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/go/ast/inspector"
)
@@ -68,7 +69,7 @@
// Check unsafe.Pointer safety rules according to
// https://golang.org/pkg/unsafe/#Pointer.
- switch x := analysisutil.Unparen(x).(type) {
+ switch x := astutil.Unparen(x).(type) {
case *ast.SelectorExpr:
// "(6) Conversion of a reflect.SliceHeader or
// reflect.StringHeader Data field to or from Pointer."
@@ -104,8 +105,7 @@
}
switch sel.Sel.Name {
case "Pointer", "UnsafeAddr":
- t, ok := info.Types[sel.X].Type.(*types.Named)
- if ok && t.Obj().Pkg().Path() == "reflect" && t.Obj().Name() == "Value" {
+ if analysisutil.IsNamedType(info.Types[sel.X].Type, "reflect", "Value") {
return true
}
}
@@ -118,7 +118,7 @@
// isSafeArith reports whether x is a pointer arithmetic expression that is safe
// to convert to unsafe.Pointer.
func isSafeArith(info *types.Info, x ast.Expr) bool {
- switch x := analysisutil.Unparen(x).(type) {
+ switch x := astutil.Unparen(x).(type) {
case *ast.CallExpr:
// Base case: initial conversion from unsafe.Pointer to uintptr.
return len(x.Args) == 1 &&
@@ -153,13 +153,5 @@
// isReflectHeader reports whether t is reflect.SliceHeader or reflect.StringHeader.
func isReflectHeader(t types.Type) bool {
- if named, ok := t.(*types.Named); ok {
- if obj := named.Obj(); obj.Pkg() != nil && obj.Pkg().Path() == "reflect" {
- switch obj.Name() {
- case "SliceHeader", "StringHeader":
- return true
- }
- }
- }
- return false
+ return analysisutil.IsNamedType(t, "reflect", "SliceHeader", "StringHeader")
}
diff --git a/go/analysis/passes/unusedresult/unusedresult.go b/go/analysis/passes/unusedresult/unusedresult.go
index cb487a2..7f79b4a 100644
--- a/go/analysis/passes/unusedresult/unusedresult.go
+++ b/go/analysis/passes/unusedresult/unusedresult.go
@@ -24,6 +24,7 @@
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/analysis/passes/internal/analysisutil"
+ "golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/go/types/typeutil"
)
@@ -82,7 +83,7 @@
(*ast.ExprStmt)(nil),
}
inspect.Preorder(nodeFilter, func(n ast.Node) {
- call, ok := analysisutil.Unparen(n.(*ast.ExprStmt).X).(*ast.CallExpr)
+ call, ok := astutil.Unparen(n.(*ast.ExprStmt).X).(*ast.CallExpr)
if !ok {
return // not a call statement
}
@@ -92,7 +93,6 @@
if !ok {
return // e.g. var or builtin
}
-
if sig := fn.Type().(*types.Signature); sig.Recv() != nil {
// method (e.g. foo.String())
if types.Identical(sig, sigNoArgsStringResult) {
diff --git a/go/types/internal/play/play.go b/go/types/internal/play/play.go
index 79cdc5a..7a760e2 100644
--- a/go/types/internal/play/play.go
+++ b/go/types/internal/play/play.go
@@ -174,7 +174,7 @@
}
// selection x.f information (if cursor is over .f)
- for _, n := range path[:2] {
+ for _, n := range path[:min(2, len(path))] {
if sel, ok := n.(*ast.SelectorExpr); ok {
seln, ok := pkg.TypesInfo.Selections[sel]
if ok {
@@ -334,3 +334,12 @@
body { color: gray; }
div#out { font-family: monospace; font-size: 80%; }
`
+
+// TODO(adonovan): use go1.21 built-in.
+func min(x, y int) int {
+ if x < y {
+ return x
+ } else {
+ return y
+ }
+}
diff --git a/gopls/go.mod b/gopls/go.mod
index 80ce516..b4f027b 100644
--- a/gopls/go.mod
+++ b/gopls/go.mod
@@ -7,10 +7,10 @@
github.com/jba/printsrc v0.2.2
github.com/jba/templatecheck v0.6.0
github.com/sergi/go-diff v1.1.0
- golang.org/x/mod v0.12.0
- golang.org/x/sync v0.3.0
- golang.org/x/sys v0.12.0
- golang.org/x/telemetry v0.0.0-20231003223302-0168ef4ebbd3
+ golang.org/x/mod v0.13.0
+ golang.org/x/sync v0.4.0
+ golang.org/x/sys v0.13.0
+ golang.org/x/telemetry v0.0.0-20231011160506-788d5629a052
golang.org/x/text v0.13.0
golang.org/x/tools v0.13.1-0.20231005170247-117a486e6b4a
golang.org/x/vuln v1.0.1
@@ -26,3 +26,5 @@
golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338 // indirect
)
+
+replace golang.org/x/tools => ../
diff --git a/gopls/go.sum b/gopls/go.sum
index dbf0717..ea1d12e 100644
--- a/gopls/go.sum
+++ b/gopls/go.sum
@@ -30,15 +30,15 @@
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338 h1:2O2DON6y3XMJiQRAS1UWU+54aec2uopH3x7MAiqGW6Y=
golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
-golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
-golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY=
+golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
-golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
-golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
-golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/telemetry v0.0.0-20231003223302-0168ef4ebbd3 h1:vxxQvncMbcRAtqHV5HsHGJkbya+BIOYIY+y6cdPZhzk=
-golang.org/x/telemetry v0.0.0-20231003223302-0168ef4ebbd3/go.mod h1:ppZ76JTkRgJC2GQEgtVY3fiuJR+N8FU2MAlp+gfN1E4=
+golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
+golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
+golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
+golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/telemetry v0.0.0-20231011160506-788d5629a052 h1:1baVNneD/IRxmu8JQdBuki78zUqBtZxq8smZXQj0X2Y=
+golang.org/x/telemetry v0.0.0-20231011160506-788d5629a052/go.mod h1:6p4ScoNeC2dhpQ1nSSMmkZ7mEj5JQUSCyc0uExBp5T4=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
diff --git a/gopls/internal/lsp/cache/filemap_test.go b/gopls/internal/lsp/cache/filemap_test.go
index 3d5bab4..a1d10af 100644
--- a/gopls/internal/lsp/cache/filemap_test.go
+++ b/gopls/internal/lsp/cache/filemap_test.go
@@ -7,7 +7,6 @@
import (
"path/filepath"
"sort"
- "strings"
"testing"
"github.com/google/go-cmp/cmp"
@@ -56,7 +55,12 @@
// Normalize paths for windows compatibility.
normalize := func(path string) string {
- return strings.TrimPrefix(filepath.ToSlash(path), "C:") // the span packages adds 'C:'
+ y := filepath.ToSlash(path)
+ // Windows paths may start with a drive letter
+ if len(y) > 2 && y[1] == ':' && y[0] >= 'A' && y[0] <= 'Z' {
+ y = y[2:]
+ }
+ return y
}
for _, test := range tests {
diff --git a/gopls/internal/lsp/fake/editor.go b/gopls/internal/lsp/fake/editor.go
index ccee51e..b8e6901 100644
--- a/gopls/internal/lsp/fake/editor.go
+++ b/gopls/internal/lsp/fake/editor.go
@@ -44,7 +44,8 @@
config EditorConfig // editor configuration
buffers map[string]buffer // open buffers (relative path -> buffer content)
serverCapabilities protocol.ServerCapabilities // capabilities / options
- watchPatterns []*glob.Glob // glob patterns to watch
+ semTokOpts protocol.SemanticTokensOptions
+ watchPatterns []*glob.Glob // glob patterns to watch
// Call metrics for the purpose of expectations. This is done in an ad-hoc
// manner for now. Perhaps in the future we should do something more
@@ -306,8 +307,13 @@
if err != nil {
return fmt.Errorf("initialize: %w", err)
}
+ semTokOpts, err := marshalUnmarshal[protocol.SemanticTokensOptions](resp.Capabilities.SemanticTokensProvider)
+ if err != nil {
+ return fmt.Errorf("unmarshalling semantic tokens options: %v", err)
+ }
e.mu.Lock()
e.serverCapabilities = resp.Capabilities
+ e.semTokOpts = semTokOpts
e.mu.Unlock()
if err := e.Server.Initialized(ctx, &protocol.InitializedParams{}); err != nil {
@@ -318,6 +324,19 @@
return nil
}
+// marshalUnmarshal is a helper to json Marshal and then Unmarshal as a
+// different type. Used to work around cases where our protocol types are not
+// specific.
+func marshalUnmarshal[T any](v any) (T, error) {
+ var t T
+ data, err := json.Marshal(v)
+ if err != nil {
+ return t, err
+ }
+ err = json.Unmarshal(data, &t)
+ return t, err
+}
+
// HasCommand reports whether the connected server supports the command with the given ID.
func (e *Editor) HasCommand(id string) bool {
for _, command := range e.serverCapabilities.ExecuteCommandProvider.Commands {
@@ -1492,3 +1511,61 @@
return e.Server.DocumentHighlight(ctx, params)
}
+
+// SemanticTokens invokes textDocument/semanticTokens/full, and interprets its
+// result.
+func (e *Editor) SemanticTokens(ctx context.Context, path string) ([]SemanticToken, error) {
+ p := &protocol.SemanticTokensParams{
+ TextDocument: protocol.TextDocumentIdentifier{
+ URI: e.sandbox.Workdir.URI(path),
+ },
+ }
+ resp, err := e.Server.SemanticTokensFull(ctx, p)
+ if err != nil {
+ return nil, err
+ }
+ content, ok := e.BufferText(path)
+ if !ok {
+ return nil, fmt.Errorf("buffer %s is not open", path)
+ }
+ return e.interpretTokens(resp.Data, content), nil
+}
+
+// A SemanticToken is an interpreted semantic token value.
+type SemanticToken struct {
+ Token string
+ TokenType string
+ Mod string
+}
+
+// Note: previously this function elided comment, string, and number tokens.
+// Instead, filtering of token types should be done by the caller.
+func (e *Editor) interpretTokens(x []uint32, contents string) []SemanticToken {
+ e.mu.Lock()
+ legend := e.semTokOpts.Legend
+ e.mu.Unlock()
+ lines := strings.Split(contents, "\n")
+ ans := []SemanticToken{}
+ line, col := 1, 1
+ for i := 0; i < len(x); i += 5 {
+ line += int(x[i])
+ col += int(x[i+1])
+ if x[i] != 0 { // new line
+ col = int(x[i+1]) + 1 // 1-based column numbers
+ }
+ sz := x[i+2]
+ t := legend.TokenTypes[x[i+3]]
+ l := x[i+4]
+ var mods []string
+ for i, mod := range legend.TokenModifiers {
+ if l&(1<<i) != 0 {
+ mods = append(mods, mod)
+ }
+ }
+ // Preexisting note: "col is a utf-8 offset"
+ // TODO(rfindley): is that true? Or is it UTF-16, like other columns in the LSP?
+ tok := lines[line-1][col-1 : col-1+int(sz)]
+ ans = append(ans, SemanticToken{tok, t, strings.Join(mods, " ")})
+ }
+ return ans
+}
diff --git a/gopls/internal/lsp/general.go b/gopls/internal/lsp/general.go
index 7d9ed35..02e9f70 100644
--- a/gopls/internal/lsp/general.go
+++ b/gopls/internal/lsp/general.go
@@ -172,8 +172,8 @@
Range: &protocol.Or_SemanticTokensOptions_range{Value: true},
Full: &protocol.Or_SemanticTokensOptions_full{Value: true},
Legend: protocol.SemanticTokensLegend{
- TokenTypes: nonNilSliceString(s.Options().SemanticTypes),
- TokenModifiers: nonNilSliceString(s.Options().SemanticMods),
+ TokenTypes: nonNilSliceString(options.SemanticTypes),
+ TokenModifiers: nonNilSliceString(options.SemanticMods),
},
},
SignatureHelpProvider: &protocol.SignatureHelpOptions{
@@ -217,7 +217,6 @@
}
s.notifications = nil
- options := s.Options()
if err := s.addFolders(ctx, s.pendingFolders); err != nil {
return err
}
@@ -225,6 +224,7 @@
s.checkViewGoVersions()
var registrations []protocol.Registration
+ options := s.Options()
if options.ConfigurationSupported && options.DynamicConfigurationSupported {
registrations = append(registrations, protocol.Registration{
ID: "workspace/didChangeConfiguration",
diff --git a/gopls/internal/lsp/regtest/regtest.go b/gopls/internal/lsp/regtest/regtest.go
index 7def1d7..6e14b91 100644
--- a/gopls/internal/lsp/regtest/regtest.go
+++ b/gopls/internal/lsp/regtest/regtest.go
@@ -104,7 +104,7 @@
}
if !testenv.HasExec() {
- fmt.Printf("skipping all tests: exec not supported on %s\n", runtime.GOOS)
+ fmt.Printf("skipping all tests: exec not supported on %s/%s\n", runtime.GOOS, runtime.GOARCH)
os.Exit(0)
}
testenv.ExitIfSmallMachine()
diff --git a/gopls/internal/regtest/misc/semantictokens_test.go b/gopls/internal/regtest/misc/semantictokens_test.go
index a96024b..d50ceb0 100644
--- a/gopls/internal/regtest/misc/semantictokens_test.go
+++ b/gopls/internal/regtest/misc/semantictokens_test.go
@@ -5,11 +5,10 @@
package misc
import (
- "strings"
"testing"
"github.com/google/go-cmp/cmp"
- "golang.org/x/tools/gopls/internal/lsp"
+ "golang.org/x/tools/gopls/internal/lsp/fake"
"golang.org/x/tools/gopls/internal/lsp/protocol"
. "golang.org/x/tools/gopls/internal/lsp/regtest"
"golang.org/x/tools/internal/typeparams"
@@ -48,31 +47,30 @@
// fix bug involving type parameters and regular parameters
// (golang/vscode-go#2527)
func TestSemantic_2527(t *testing.T) {
- if !typeparams.Enabled {
- t.Skip("type parameters are needed for this test")
- }
// these are the expected types of identifiers in text order
- want := []result{
- {"package", "keyword", ""},
- {"foo", "namespace", ""},
- {"func", "keyword", ""},
- {"Add", "function", "definition deprecated"},
- {"T", "typeParameter", "definition"},
- {"int", "type", "defaultLibrary"},
- {"target", "parameter", "definition"},
- {"T", "typeParameter", ""},
- {"l", "parameter", "definition"},
- {"T", "typeParameter", ""},
- {"T", "typeParameter", ""},
- {"return", "keyword", ""},
- {"append", "function", "defaultLibrary"},
- {"l", "parameter", ""},
- {"target", "parameter", ""},
- {"for", "keyword", ""},
- {"range", "keyword", ""},
- {"l", "parameter", ""},
- {"return", "keyword", ""},
- {"nil", "variable", "readonly defaultLibrary"},
+ want := []fake.SemanticToken{
+ {Token: "package", TokenType: "keyword"},
+ {Token: "foo", TokenType: "namespace"},
+ {Token: "// Deprecated (for testing)", TokenType: "comment"},
+ {Token: "func", TokenType: "keyword"},
+ {Token: "Add", TokenType: "function", Mod: "definition deprecated"},
+ {Token: "T", TokenType: "typeParameter", Mod: "definition"},
+ {Token: "int", TokenType: "type", Mod: "defaultLibrary"},
+ {Token: "target", TokenType: "parameter", Mod: "definition"},
+ {Token: "T", TokenType: "typeParameter"},
+ {Token: "l", TokenType: "parameter", Mod: "definition"},
+ {Token: "T", TokenType: "typeParameter"},
+ {Token: "T", TokenType: "typeParameter"},
+ {Token: "return", TokenType: "keyword"},
+ {Token: "append", TokenType: "function", Mod: "defaultLibrary"},
+ {Token: "l", TokenType: "parameter"},
+ {Token: "target", TokenType: "parameter"},
+ {Token: "for", TokenType: "keyword"},
+ {Token: "range", TokenType: "keyword"},
+ {Token: "l", TokenType: "parameter"},
+ {Token: "// test coverage", TokenType: "comment"},
+ {Token: "return", TokenType: "keyword"},
+ {Token: "nil", TokenType: "variable", Mod: "readonly defaultLibrary"},
}
src := `
-- go.mod --
@@ -96,16 +94,10 @@
env.AfterChange(
Diagnostics(env.AtRegexp("main.go", "for range")),
)
- p := &protocol.SemanticTokensParams{
- TextDocument: protocol.TextDocumentIdentifier{
- URI: env.Sandbox.Workdir.URI("main.go"),
- },
- }
- v, err := env.Editor.Server.SemanticTokensFull(env.Ctx, p)
+ seen, err := env.Editor.SemanticTokens(env.Ctx, "main.go")
if err != nil {
t.Fatal(err)
}
- seen := interpret(v.Data, env.BufferText("main.go"))
if x := cmp.Diff(want, seen); x != "" {
t.Errorf("Semantic tokens do not match (-want +got):\n%s", x)
}
@@ -142,16 +134,10 @@
Settings{"semanticTokens": true},
).Run(t, src, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
- p := &protocol.SemanticTokensParams{
- TextDocument: protocol.TextDocumentIdentifier{
- URI: env.Sandbox.Workdir.URI("main.go"),
- },
- }
- v, err := env.Editor.Server.SemanticTokensFull(env.Ctx, p)
+ seen, err := env.Editor.SemanticTokens(env.Ctx, "main.go")
if err != nil {
t.Fatal(err)
}
- seen := interpret(v.Data, env.BufferText("main.go"))
for i, s := range seen {
if (s.Token == "K" || s.Token == "V") && s.TokenType != "typeParameter" {
t.Errorf("%d: expected K and V to be type parameters, but got %v", i, s)
@@ -159,46 +145,3 @@
}
})
}
-
-type result struct {
- Token string
- TokenType string
- Mod string
-}
-
-// human-readable version of the semantic tokens
-// comment, string, number are elided
-// (and in the future, maybe elide other things, like operators)
-func interpret(x []uint32, contents string) []result {
- lines := strings.Split(contents, "\n")
- ans := []result{}
- line, col := 1, 1
- for i := 0; i < len(x); i += 5 {
- line += int(x[i])
- col += int(x[i+1])
- if x[i] != 0 { // new line
- col = int(x[i+1]) + 1 // 1-based column numbers
- }
- sz := x[i+2]
- t := semanticTypes[x[i+3]]
- if t == "comment" || t == "string" || t == "number" {
- continue
- }
- l := x[i+4]
- var mods []string
- for i, mod := range semanticModifiers {
- if l&(1<<i) != 0 {
- mods = append(mods, mod)
- }
- }
- // col is a utf-8 offset
- tok := lines[line-1][col-1 : col-1+int(sz)]
- ans = append(ans, result{tok, t, strings.Join(mods, " ")})
- }
- return ans
-}
-
-var (
- semanticTypes = lsp.SemanticTypes()
- semanticModifiers = lsp.SemanticModifiers()
-)
diff --git a/internal/refactor/inline/calleefx.go b/internal/refactor/inline/calleefx.go
index 6e3dc79..11246e5 100644
--- a/internal/refactor/inline/calleefx.go
+++ b/internal/refactor/inline/calleefx.go
@@ -116,10 +116,23 @@
visitExpr(n.X)
case *ast.SelectorExpr:
- if sel, ok := info.Selections[n]; ok {
+ if seln, ok := info.Selections[n]; ok {
visitExpr(n.X)
- if sel.Indirect() {
- effect(rinf) // indirect read x.f of heap variable
+
+ // See types.SelectionKind for background.
+ switch seln.Kind() {
+ case types.MethodExpr:
+ // A method expression T.f acts like a
+ // reference to a func decl,
+ // so it doesn't read x until called.
+
+ case types.MethodVal, types.FieldVal:
+ // A field or method value selection x.f
+ // reads x if the selection indirects a pointer.
+
+ if indirectSelection(seln) {
+ effect(rinf)
+ }
}
} else {
// qualified identifier: treat like unqualified
diff --git a/internal/refactor/inline/escape.go b/internal/refactor/inline/escape.go
index d05d2b9..795ad4f 100644
--- a/internal/refactor/inline/escape.go
+++ b/internal/refactor/inline/escape.go
@@ -72,9 +72,11 @@
if sel, ok := n.Fun.(*ast.SelectorExpr); ok {
if seln, ok := info.Selections[sel]; ok &&
seln.Kind() == types.MethodVal &&
- !seln.Indirect() &&
- is[*types.Pointer](seln.Obj().Type().(*types.Signature).Recv().Type()) {
- lvalue(sel.X, true) // &x.f
+ isPointer(seln.Obj().Type().(*types.Signature).Recv().Type()) {
+ tArg, indirect := effectiveReceiver(seln)
+ if !indirect && !isPointer(tArg) {
+ lvalue(sel.X, true) // &x.f
+ }
}
}
diff --git a/internal/refactor/inline/everything_test.go b/internal/refactor/inline/everything_test.go
index a1c5448..f611149 100644
--- a/internal/refactor/inline/everything_test.go
+++ b/internal/refactor/inline/everything_test.go
@@ -10,6 +10,7 @@
"go/ast"
"go/parser"
"go/types"
+ "log"
"os"
"path/filepath"
"strings"
@@ -37,7 +38,7 @@
//
// And these commands to inline everything in the kubernetes repository:
//
-// $ go build -c -o /tmp/everything ./internal/refactor/inline/
+// $ go test -c -o /tmp/everything ./internal/refactor/inline/
// $ (cd kubernetes && /tmp/everything -test.run=Everything -packages=./...)
//
// TODO(adonovan):
@@ -226,7 +227,7 @@
noMutCheck()
}
}
- t.Errorf("Analyzed %d packages", len(pkgs))
+ log.Printf("Analyzed %d packages", len(pkgs))
}
type importerFunc func(path string) (*types.Package, error)
diff --git a/internal/refactor/inline/inline.go b/internal/refactor/inline/inline.go
index 9615ab4..618b179 100644
--- a/internal/refactor/inline/inline.go
+++ b/internal/refactor/inline/inline.go
@@ -844,23 +844,30 @@
// x, y = f()
// or the sole argument to a spread call:
// printf(f())
+ // or spread return statement:
+ // return f()
res.old = context
switch context := context.(type) {
case *ast.AssignStmt:
- // Inv: the call is in Rhs[0], not Lhs.
+ // Inv: the call must be in Rhs[0], not Lhs.
assign := shallowCopy(context)
assign.Rhs = results
res.new = assign
case *ast.ValueSpec:
- // Inv: the call is in Values[0], not Names.
+ // Inv: the call must be in Values[0], not Names.
spec := shallowCopy(context)
spec.Values = results
res.new = spec
case *ast.CallExpr:
- // Inv: the Call is Args[0], not Fun.
+ // Inv: the call must be in Args[0], not Fun.
call := shallowCopy(context)
call.Args = results
res.new = call
+ case *ast.ReturnStmt:
+ // Inv: the call must be Results[0].
+ ret := shallowCopy(context)
+ ret.Results = results
+ res.new = ret
default:
return nil, fmt.Errorf("internal error: unexpected context %T for spread call", context)
}
@@ -1086,7 +1093,7 @@
debugFormatNode(caller.Fset, caller.Call.Fun),
fld.Name())
}
- if is[*types.Pointer](arg.typ.Underlying()) {
+ if isPointer(arg.typ) {
arg.pure = false // implicit *ptr operation => impure
}
arg.expr = &ast.SelectorExpr{
@@ -1098,8 +1105,8 @@
}
// Make * or & explicit.
- argIsPtr := arg.typ != deref(arg.typ)
- paramIsPtr := is[*types.Pointer](seln.Obj().Type().(*types.Signature).Recv().Type())
+ argIsPtr := isPointer(arg.typ)
+ paramIsPtr := isPointer(seln.Obj().Type().(*types.Signature).Recv().Type())
if !argIsPtr && paramIsPtr {
// &recv
arg.expr = &ast.UnaryExpr{Op: token.AND, X: arg.expr}
@@ -1903,27 +1910,23 @@
return true
case *ast.SelectorExpr:
- if sel, ok := info.Selections[e]; ok {
- switch sel.Kind() {
+ if seln, ok := info.Selections[e]; ok {
+
+ // See types.SelectionKind for background.
+ switch seln.Kind() {
case types.MethodExpr:
// A method expression T.f acts like a
// reference to a func decl, so it is pure.
return true
- case types.MethodVal:
- // A method value x.f acts like a
- // closure around a T.f(x, ...) call,
- // so it is as pure as x.
- return pure(e.X)
-
- case types.FieldVal:
- // A field selection x.f is pure if
- // x is pure and the selection does
+ case types.MethodVal, types.FieldVal:
+ // A field or method selection x.f is pure
+ // if x is pure and the selection does
// not indirect a pointer.
- return !sel.Indirect() && pure(e.X)
+ return !indirectSelection(seln) && pure(e.X)
default:
- panic(sel)
+ panic(seln)
}
} else {
// A qualified identifier is
@@ -1971,7 +1974,7 @@
// integer literals, unary negation, and selectors x.f where x is not
// a pointer. But we would not wish to duplicate expressions that:
// - have side effects (e.g. nearly all calls),
-// - are not referentially transparent (e.g. &T{}, ptr.field), or
+// - are not referentially transparent (e.g. &T{}, ptr.field, *ptr), or
// - are long (e.g. "huge string literal").
func duplicable(info *types.Info, e ast.Expr) bool {
switch e := e.(type) {
@@ -1995,17 +1998,35 @@
case *ast.UnaryExpr: // e.g. +1, -1
return (e.Op == token.ADD || e.Op == token.SUB) && duplicable(info, e.X)
+ case *ast.CompositeLit:
+ // Empty struct or array literals T{} are duplicable.
+ // (Non-empty literals are too verbose, and slice/map
+ // literals allocate indirect variables.)
+ if len(e.Elts) == 0 {
+ switch info.TypeOf(e).Underlying().(type) {
+ case *types.Struct, *types.Array:
+ return true
+ }
+ }
+ return false
+
case *ast.CallExpr:
// Don't treat a conversion T(x) as duplicable even
// if x is duplicable because it could duplicate
- // allocations. There may be cases to tease apart here.
+ // allocations.
+ //
+ // TODO(adonovan): there are cases to tease apart here:
+ // duplicating string([]byte) conversions increases
+ // allocation but doesn't change behavior, but the
+ // reverse, []byte(string), allocates a distinct array,
+ // which is observable
return false
case *ast.SelectorExpr:
- if sel, ok := info.Selections[e]; ok {
+ if seln, ok := info.Selections[e]; ok {
// A field or method selection x.f is referentially
// transparent if it does not indirect a pointer.
- return !sel.Indirect()
+ return !indirectSelection(seln)
}
// A qualified identifier pkg.Name is referentially transparent.
return true
diff --git a/internal/refactor/inline/inline_test.go b/internal/refactor/inline/inline_test.go
index 8362e44..189ac3f 100644
--- a/internal/refactor/inline/inline_test.go
+++ b/internal/refactor/inline/inline_test.go
@@ -391,6 +391,56 @@
})
}
+func TestDuplicable(t *testing.T) {
+ runTests(t, []testcase{
+ {
+ "Empty strings are duplicable.",
+ `func f(s string) { print(s, s) }`,
+ `func _() { f("") }`,
+ `func _() { print("", "") }`,
+ },
+ {
+ "Non-empty string literals are not duplicable.",
+ `func f(s string) { print(s, s) }`,
+ `func _() { f("hi") }`,
+ `func _() {
+ var s string = "hi"
+ print(s, s)
+}`,
+ },
+ {
+ "Empty array literals are duplicable.",
+ `func f(a [2]int) { print(a, a) }`,
+ `func _() { f([2]int{}) }`,
+ `func _() { print([2]int{}, [2]int{}) }`,
+ },
+ {
+ "Non-empty array literals are not duplicable.",
+ `func f(a [2]int) { print(a, a) }`,
+ `func _() { f([2]int{1, 2}) }`,
+ `func _() {
+ var a [2]int = [2]int{1, 2}
+ print(a, a)
+}`,
+ },
+ {
+ "Empty struct literals are duplicable.",
+ `func f(s S) { print(s, s) }; type S struct { x int }`,
+ `func _() { f(S{}) }`,
+ `func _() { print(S{}, S{}) }`,
+ },
+ {
+ "Non-empty struct literals are not duplicable.",
+ `func f(s S) { print(s, s) }; type S struct { x int }`,
+ `func _() { f(S{x: 1}) }`,
+ `func _() {
+ var s S = S{x: 1}
+ print(s, s)
+}`,
+ },
+ })
+}
+
func TestExprStmtReduction(t *testing.T) {
runTests(t, []testcase{
{
@@ -617,6 +667,12 @@
)
}`,
},
+ {
+ "Spread call in return (#63398).",
+ `func f() (int, error) { return 0, nil }`,
+ `func _() (int, error) { return f() }`,
+ `func _() (int, error) { return 0, nil }`,
+ },
})
}
@@ -715,6 +771,21 @@
`func _() { f(g(1), g(2), g(3)) }`,
`func _() { func(int, y any, z int) { defer g(0); println(int, y, z) }(g(1), g(2), g(3)) }`,
},
+ {
+ "An indirect method selection (*x).g acts as a read.",
+ `func f(x *T, y any) any { return x.g(y) }; type T struct{}; func (T) g(x any) any { return x }`,
+ `func _(x *T) { f(x, recover()) }`,
+ `func _(x *T) {
+ var y any = recover()
+ x.g(y)
+}`,
+ },
+ {
+ "A direct method selection x.g is pure.",
+ `func f(x *T, y any) any { return x.g(y) }; type T struct{}; func (*T) g(x any) any { return x }`,
+ `func _(x *T) { f(x, recover()) }`,
+ `func _(x *T) { x.g(recover()) }`,
+ },
})
}
diff --git a/internal/refactor/inline/util.go b/internal/refactor/inline/util.go
index 98d654e..267ef74 100644
--- a/internal/refactor/inline/util.go
+++ b/internal/refactor/inline/util.go
@@ -12,6 +12,8 @@
"go/types"
"reflect"
"strings"
+
+ "golang.org/x/tools/internal/typeparams"
)
func is[T any](x any) bool {
@@ -117,3 +119,42 @@
Args: []ast.Expr{x},
}
}
+
+// isPointer reports whether t is a pointer type.
+func isPointer(t types.Type) bool { return t != deref(t) }
+
+// indirectSelection is like seln.Indirect() without bug #8353.
+func indirectSelection(seln *types.Selection) bool {
+ // Work around bug #8353 in Selection.Indirect when Kind=MethodVal.
+ if seln.Kind() == types.MethodVal {
+ tArg, indirect := effectiveReceiver(seln)
+ if indirect {
+ return true
+ }
+
+ tParam := seln.Obj().Type().(*types.Signature).Recv().Type()
+ return isPointer(tArg) && !isPointer(tParam) // implicit *
+ }
+
+ return seln.Indirect()
+}
+
+// effectiveReceiver returns the effective type of the method
+// receiver after all implicit field selections (but not implicit * or
+// & operations) have been applied.
+//
+// The boolean indicates whether any implicit field selection was indirect.
+func effectiveReceiver(seln *types.Selection) (types.Type, bool) {
+ assert(seln.Kind() == types.MethodVal, "not MethodVal")
+ t := seln.Recv()
+ indices := seln.Index()
+ indirect := false
+ for _, index := range indices[:len(indices)-1] {
+ if tElem := deref(t); tElem != t {
+ indirect = true
+ t = tElem
+ }
+ t = typeparams.CoreType(t).(*types.Struct).Field(index).Type()
+ }
+ return t, indirect
+}