mirror of
https://github.com/golang/go.git
synced 2025-12-08 06:10:04 +00:00
Currently we rely on the type-checker to do some basic data-flow analysis to help decide whether function literals should capture variables by value or reference. However, this analysis isn't done by go/types, and escape analysis already has a better framework for doing this more precisely. This CL extends escape analysis to recalculate the same "byval" as CaptureVars and check that it matches. A future CL will remove CaptureVars in favor of escape analysis's calculation. Notably, escape analysis happens after deadcode removes obviously unreachable code, so it sees the AST without any unreachable assignments. (Also without unreachable addrtakens, but ComputeAddrtaken already happens after deadcode too.) There are two test cases where a variable is only reassigned on certain CPUs. This CL changes them to reassign the variables unconditionally (as no-op reassignments that avoid triggering cmd/vet's self-assignment check), at least until we remove CaptureVars. Passes toolstash -cmp. Change-Id: I7162619739fedaf861b478fb8d506f96a6ac21f3 Reviewed-on: https://go-review.googlesource.com/c/go/+/281535 Trust: Matthew Dempsky <mdempsky@google.com> Run-TryBot: Matthew Dempsky <mdempsky@google.com> TryBot-Result: Go Bot <gobot@golang.org> Reviewed-by: Cuong Manh Le <cuong.manhle.vn@gmail.com>
259 lines
10 KiB
Go
259 lines
10 KiB
Go
// Copyright 2019 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 logopt
|
|
|
|
import (
|
|
"internal/testenv"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
const srcCode = `package x
|
|
type pair struct {a,b int}
|
|
func bar(y *pair) *int {
|
|
return &y.b
|
|
}
|
|
var a []int
|
|
func foo(w, z *pair) *int {
|
|
if *bar(w) > 0 {
|
|
return bar(z)
|
|
}
|
|
if a[1] > 0 {
|
|
a = a[:2]
|
|
}
|
|
return &a[0]
|
|
}
|
|
|
|
// address taking prevents closure inlining
|
|
func n() int {
|
|
foo := func() int { return 1 }
|
|
bar := &foo
|
|
x := (*bar)() + foo()
|
|
return x
|
|
}
|
|
`
|
|
|
|
func want(t *testing.T, out string, desired string) {
|
|
// On Windows, Unicode escapes in the JSON output end up "normalized" elsewhere to /u....,
|
|
// so "normalize" what we're looking for to match that.
|
|
s := strings.ReplaceAll(desired, string(os.PathSeparator), "/")
|
|
if !strings.Contains(out, s) {
|
|
t.Errorf("did not see phrase %s in \n%s", s, out)
|
|
}
|
|
}
|
|
|
|
func wantN(t *testing.T, out string, desired string, n int) {
|
|
if strings.Count(out, desired) != n {
|
|
t.Errorf("expected exactly %d occurrences of %s in \n%s", n, desired, out)
|
|
}
|
|
}
|
|
|
|
func TestPathStuff(t *testing.T) {
|
|
sep := string(filepath.Separator)
|
|
if path, whine := parseLogPath("file:///c:foo"); path != "c:foo" || whine != "" { // good path
|
|
t.Errorf("path='%s', whine='%s'", path, whine)
|
|
}
|
|
if path, whine := parseLogPath("file:///foo"); path != sep+"foo" || whine != "" { // good path
|
|
t.Errorf("path='%s', whine='%s'", path, whine)
|
|
}
|
|
if path, whine := parseLogPath("foo"); path != "" || whine == "" { // BAD path
|
|
t.Errorf("path='%s', whine='%s'", path, whine)
|
|
}
|
|
if sep == "\\" { // On WINDOWS ONLY
|
|
if path, whine := parseLogPath("C:/foo"); path != "C:\\foo" || whine != "" { // good path
|
|
t.Errorf("path='%s', whine='%s'", path, whine)
|
|
}
|
|
if path, whine := parseLogPath("c:foo"); path != "" || whine == "" { // BAD path
|
|
t.Errorf("path='%s', whine='%s'", path, whine)
|
|
}
|
|
if path, whine := parseLogPath("/foo"); path != "" || whine == "" { // BAD path
|
|
t.Errorf("path='%s', whine='%s'", path, whine)
|
|
}
|
|
} else { // ON UNIX ONLY
|
|
if path, whine := parseLogPath("/foo"); path != sep+"foo" || whine != "" { // good path
|
|
t.Errorf("path='%s', whine='%s'", path, whine)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLogOpt(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testenv.MustHaveGoBuild(t)
|
|
|
|
dir, err := ioutil.TempDir("", "TestLogOpt")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(dir)
|
|
|
|
dir = fixSlash(dir) // Normalize the directory name as much as possible, for Windows testing
|
|
src := filepath.Join(dir, "file.go")
|
|
if err := ioutil.WriteFile(src, []byte(srcCode), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
outfile := filepath.Join(dir, "file.o")
|
|
|
|
t.Run("JSON_fails", func(t *testing.T) {
|
|
// Test malformed flag
|
|
out, err := testLogOpt(t, "-json=foo", src, outfile)
|
|
if err == nil {
|
|
t.Error("-json=foo succeeded unexpectedly")
|
|
}
|
|
want(t, out, "option should be")
|
|
want(t, out, "number")
|
|
|
|
// Test a version number that is currently unsupported (and should remain unsupported for a while)
|
|
out, err = testLogOpt(t, "-json=9,foo", src, outfile)
|
|
if err == nil {
|
|
t.Error("-json=0,foo succeeded unexpectedly")
|
|
}
|
|
want(t, out, "version must be")
|
|
|
|
})
|
|
|
|
// replace d (dir) with t ("tmpdir") and convert path separators to '/'
|
|
normalize := func(out []byte, d, t string) string {
|
|
s := string(out)
|
|
s = strings.ReplaceAll(s, d, t)
|
|
s = strings.ReplaceAll(s, string(os.PathSeparator), "/")
|
|
return s
|
|
}
|
|
|
|
// Ensure that <128 byte copies are not reported and that 128-byte copies are.
|
|
// Check at both 1 and 8-byte alignments.
|
|
t.Run("Copy", func(t *testing.T) {
|
|
const copyCode = `package x
|
|
func s128a1(x *[128]int8) [128]int8 {
|
|
return *x
|
|
}
|
|
func s127a1(x *[127]int8) [127]int8 {
|
|
return *x
|
|
}
|
|
func s16a8(x *[16]int64) [16]int64 {
|
|
return *x
|
|
}
|
|
func s15a8(x *[15]int64) [15]int64 {
|
|
return *x
|
|
}
|
|
`
|
|
copy := filepath.Join(dir, "copy.go")
|
|
if err := ioutil.WriteFile(copy, []byte(copyCode), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
outcopy := filepath.Join(dir, "copy.o")
|
|
|
|
// On not-amd64, test the host architecture and os
|
|
arches := []string{runtime.GOARCH}
|
|
goos0 := runtime.GOOS
|
|
goos0 = "" + goos0 // TODO(mdempsky): Remove once CaptureVars is gone.
|
|
if runtime.GOARCH == "amd64" { // Test many things with "linux" (wasm will get "js")
|
|
arches = []string{"arm", "arm64", "386", "amd64", "mips", "mips64", "ppc64le", "riscv64", "s390x", "wasm"}
|
|
goos0 = "linux"
|
|
}
|
|
|
|
for _, arch := range arches {
|
|
t.Run(arch, func(t *testing.T) {
|
|
goos := goos0
|
|
if arch == "wasm" {
|
|
goos = "js"
|
|
}
|
|
_, err := testCopy(t, dir, arch, goos, copy, outcopy)
|
|
if err != nil {
|
|
t.Error("-json=0,file://log/opt should have succeeded")
|
|
}
|
|
logged, err := ioutil.ReadFile(filepath.Join(dir, "log", "opt", "x", "copy.json"))
|
|
if err != nil {
|
|
t.Error("-json=0,file://log/opt missing expected log file")
|
|
}
|
|
slogged := normalize(logged, string(uriIfy(dir)), string(uriIfy("tmpdir")))
|
|
t.Logf("%s", slogged)
|
|
want(t, slogged, `{"range":{"start":{"line":3,"character":2},"end":{"line":3,"character":2}},"severity":3,"code":"copy","source":"go compiler","message":"128 bytes"}`)
|
|
want(t, slogged, `{"range":{"start":{"line":9,"character":2},"end":{"line":9,"character":2}},"severity":3,"code":"copy","source":"go compiler","message":"128 bytes"}`)
|
|
wantN(t, slogged, `"code":"copy"`, 2)
|
|
})
|
|
}
|
|
})
|
|
|
|
// Some architectures don't fault on nil dereference, so nilchecks are eliminated differently.
|
|
// The N-way copy test also doesn't need to run N-ways N times.
|
|
if runtime.GOARCH != "amd64" {
|
|
return
|
|
}
|
|
|
|
t.Run("Success", func(t *testing.T) {
|
|
// This test is supposed to succeed
|
|
|
|
// Note 'file://' is the I-Know-What-I-Am-Doing way of specifying a file, also to deal with corner cases for Windows.
|
|
_, err := testLogOptDir(t, dir, "-json=0,file://log/opt", src, outfile)
|
|
if err != nil {
|
|
t.Error("-json=0,file://log/opt should have succeeded")
|
|
}
|
|
logged, err := ioutil.ReadFile(filepath.Join(dir, "log", "opt", "x", "file.json"))
|
|
if err != nil {
|
|
t.Error("-json=0,file://log/opt missing expected log file")
|
|
}
|
|
// All this delicacy with uriIfy and filepath.Join is to get this test to work right on Windows.
|
|
slogged := normalize(logged, string(uriIfy(dir)), string(uriIfy("tmpdir")))
|
|
t.Logf("%s", slogged)
|
|
// below shows proper nilcheck
|
|
want(t, slogged, `{"range":{"start":{"line":9,"character":13},"end":{"line":9,"character":13}},"severity":3,"code":"nilcheck","source":"go compiler","message":"",`+
|
|
`"relatedInformation":[{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":4,"character":11},"end":{"line":4,"character":11}}},"message":"inlineLoc"}]}`)
|
|
want(t, slogged, `{"range":{"start":{"line":11,"character":6},"end":{"line":11,"character":6}},"severity":3,"code":"isInBounds","source":"go compiler","message":""}`)
|
|
want(t, slogged, `{"range":{"start":{"line":7,"character":6},"end":{"line":7,"character":6}},"severity":3,"code":"canInlineFunction","source":"go compiler","message":"cost: 35"}`)
|
|
// escape analysis explanation
|
|
want(t, slogged, `{"range":{"start":{"line":7,"character":13},"end":{"line":7,"character":13}},"severity":3,"code":"leak","source":"go compiler","message":"parameter z leaks to ~r2 with derefs=0",`+
|
|
`"relatedInformation":[`+
|
|
`{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":9,"character":13},"end":{"line":9,"character":13}}},"message":"escflow: flow: y = z:"},`+
|
|
`{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":9,"character":13},"end":{"line":9,"character":13}}},"message":"escflow: from y := z (assign-pair)"},`+
|
|
`{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":9,"character":13},"end":{"line":9,"character":13}}},"message":"escflow: flow: ~R0 = y:"},`+
|
|
`{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":4,"character":11},"end":{"line":4,"character":11}}},"message":"inlineLoc"},`+
|
|
`{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":9,"character":13},"end":{"line":9,"character":13}}},"message":"escflow: from y.b (dot of pointer)"},`+
|
|
`{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":4,"character":11},"end":{"line":4,"character":11}}},"message":"inlineLoc"},`+
|
|
`{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":9,"character":13},"end":{"line":9,"character":13}}},"message":"escflow: from \u0026y.b (address-of)"},`+
|
|
`{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":4,"character":9},"end":{"line":4,"character":9}}},"message":"inlineLoc"},`+
|
|
`{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":9,"character":13},"end":{"line":9,"character":13}}},"message":"escflow: from ~R0 = \u0026y.b (assign-pair)"},`+
|
|
`{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":9,"character":3},"end":{"line":9,"character":3}}},"message":"escflow: flow: ~r2 = ~R0:"},`+
|
|
`{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":9,"character":3},"end":{"line":9,"character":3}}},"message":"escflow: from return (*int)(~R0) (return)"}]}`)
|
|
})
|
|
}
|
|
|
|
func testLogOpt(t *testing.T, flag, src, outfile string) (string, error) {
|
|
run := []string{testenv.GoToolPath(t), "tool", "compile", flag, "-o", outfile, src}
|
|
t.Log(run)
|
|
cmd := exec.Command(run[0], run[1:]...)
|
|
out, err := cmd.CombinedOutput()
|
|
t.Logf("%s", out)
|
|
return string(out), err
|
|
}
|
|
|
|
func testLogOptDir(t *testing.T, dir, flag, src, outfile string) (string, error) {
|
|
// Notice the specified import path "x"
|
|
run := []string{testenv.GoToolPath(t), "tool", "compile", "-p", "x", flag, "-o", outfile, src}
|
|
t.Log(run)
|
|
cmd := exec.Command(run[0], run[1:]...)
|
|
cmd.Dir = dir
|
|
out, err := cmd.CombinedOutput()
|
|
t.Logf("%s", out)
|
|
return string(out), err
|
|
}
|
|
|
|
func testCopy(t *testing.T, dir, goarch, goos, src, outfile string) (string, error) {
|
|
// Notice the specified import path "x"
|
|
run := []string{testenv.GoToolPath(t), "tool", "compile", "-p", "x", "-json=0,file://log/opt", "-o", outfile, src}
|
|
t.Log(run)
|
|
cmd := exec.Command(run[0], run[1:]...)
|
|
cmd.Dir = dir
|
|
cmd.Env = append(os.Environ(), "GOARCH="+goarch, "GOOS="+goos)
|
|
out, err := cmd.CombinedOutput()
|
|
t.Logf("%s", out)
|
|
return string(out), err
|
|
}
|