diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 13:14:23 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 13:14:23 +0000 |
commit | 73df946d56c74384511a194dd01dbe099584fd1a (patch) | |
tree | fd0bcea490dd81327ddfbb31e215439672c9a068 /src/cmd/compile/internal/logopt | |
parent | Initial commit. (diff) | |
download | golang-1.16-73df946d56c74384511a194dd01dbe099584fd1a.tar.xz golang-1.16-73df946d56c74384511a194dd01dbe099584fd1a.zip |
Adding upstream version 1.16.10.upstream/1.16.10upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/cmd/compile/internal/logopt')
-rw-r--r-- | src/cmd/compile/internal/logopt/escape.go | 13 | ||||
-rw-r--r-- | src/cmd/compile/internal/logopt/escape_bootstrap.go | 12 | ||||
-rw-r--r-- | src/cmd/compile/internal/logopt/log_opts.go | 523 | ||||
-rw-r--r-- | src/cmd/compile/internal/logopt/logopt_test.go | 258 |
4 files changed, 806 insertions, 0 deletions
diff --git a/src/cmd/compile/internal/logopt/escape.go b/src/cmd/compile/internal/logopt/escape.go new file mode 100644 index 0000000..802f967 --- /dev/null +++ b/src/cmd/compile/internal/logopt/escape.go @@ -0,0 +1,13 @@ +// 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. + +// +build go1.8 + +package logopt + +import "net/url" + +func pathEscape(s string) string { + return url.PathEscape(s) +} diff --git a/src/cmd/compile/internal/logopt/escape_bootstrap.go b/src/cmd/compile/internal/logopt/escape_bootstrap.go new file mode 100644 index 0000000..66ff0b8 --- /dev/null +++ b/src/cmd/compile/internal/logopt/escape_bootstrap.go @@ -0,0 +1,12 @@ +// 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. + +// +build !go1.8 + +package logopt + +// For bootstrapping with an early version of Go +func pathEscape(s string) string { + panic("This should never be called; the compiler is not fully bootstrapped if it is.") +} diff --git a/src/cmd/compile/internal/logopt/log_opts.go b/src/cmd/compile/internal/logopt/log_opts.go new file mode 100644 index 0000000..37a049d --- /dev/null +++ b/src/cmd/compile/internal/logopt/log_opts.go @@ -0,0 +1,523 @@ +// 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 ( + "cmd/internal/obj" + "cmd/internal/objabi" + "cmd/internal/src" + "encoding/json" + "fmt" + "io" + "log" + "net/url" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + "sync" + "unicode" +) + +// This implements (non)optimization logging for -json option to the Go compiler +// The option is -json 0,<destination>. +// +// 0 is the version number; to avoid the need for synchronized updates, if +// new versions of the logging appear, the compiler will support both, for a while, +// and clients will specify what they need. +// +// <destination> is a directory. +// Directories are specified with a leading / or os.PathSeparator, +// or more explicitly with file://directory. The second form is intended to +// deal with corner cases on Windows, and to allow specification of a relative +// directory path (which is normally a bad idea, because the local directory +// varies a lot in a build, especially with modules and/or vendoring, and may +// not be writeable). +// +// For each package pkg compiled, a url.PathEscape(pkg)-named subdirectory +// is created. For each source file.go in that package that generates +// diagnostics (no diagnostics means no file), +// a url.PathEscape(file)+".json"-named file is created and contains the +// logged diagnostics. +// +// For example, "cmd%2Finternal%2Fdwarf/%3Cautogenerated%3E.json" +// for "cmd/internal/dwarf" and <autogenerated> (which is not really a file, but the compiler sees it) +// +// If the package string is empty, it is replaced internally with string(0) which encodes to %00. +// +// Each log file begins with a JSON record identifying version, +// platform, and other context, followed by optimization-relevant +// LSP Diagnostic records, one per line (LSP version 3.15, no difference from 3.14 on the subset used here +// see https://microsoft.github.io/language-server-protocol/specifications/specification-3-15/ ) +// +// The fields of a Diagnostic are used in the following way: +// Range: the outermost source position, for now begin and end are equal. +// Severity: (always) SeverityInformation (3) +// Source: (always) "go compiler" +// Code: a string describing the missed optimization, e.g., "nilcheck", "cannotInline", "isInBounds", "escape" +// Message: depending on code, additional information, e.g., the reason a function cannot be inlined. +// RelatedInformation: if the missed optimization actually occurred at a function inlined at Range, +// then the sequence of inlined locations appears here, from (second) outermost to innermost, +// each with message="inlineLoc". +// +// In the case of escape analysis explanations, after any outer inlining locations, +// the lines of the explanation appear, each potentially followed with its own inlining +// location if the escape flow occurred within an inlined function. +// +// For example <destination>/cmd%2Fcompile%2Finternal%2Fssa/prove.json +// might begin with the following line (wrapped for legibility): +// +// {"version":0,"package":"cmd/compile/internal/ssa","goos":"darwin","goarch":"amd64", +// "gc_version":"devel +e1b9a57852 Fri Nov 1 15:07:00 2019 -0400", +// "file":"/Users/drchase/work/go/src/cmd/compile/internal/ssa/prove.go"} +// +// and later contain (also wrapped for legibility): +// +// {"range":{"start":{"line":191,"character":24},"end":{"line":191,"character":24}}, +// "severity":3,"code":"nilcheck","source":"go compiler","message":"", +// "relatedInformation":[ +// {"location":{"uri":"file:///Users/drchase/work/go/src/cmd/compile/internal/ssa/func.go", +// "range":{"start":{"line":153,"character":16},"end":{"line":153,"character":16}}}, +// "message":"inlineLoc"}]} +// +// That is, at prove.go (implicit from context, provided in both filename and header line), +// line 191, column 24, a nilcheck occurred in the generated code. +// The relatedInformation indicates that this code actually came from +// an inlined call to func.go, line 153, character 16. +// +// prove.go:191: +// ft.orderS = f.newPoset() +// func.go:152 and 153: +// func (f *Func) newPoset() *poset { +// if len(f.Cache.scrPoset) > 0 { +// +// In the case that the package is empty, the string(0) package name is also used in the header record, for example +// +// go tool compile -json=0,file://logopt x.go # no -p option to set the package +// head -1 logopt/%00/x.json +// {"version":0,"package":"\u0000","goos":"darwin","goarch":"amd64","gc_version":"devel +86487adf6a Thu Nov 7 19:34:56 2019 -0500","file":"x.go"} + +type VersionHeader struct { + Version int `json:"version"` + Package string `json:"package"` + Goos string `json:"goos"` + Goarch string `json:"goarch"` + GcVersion string `json:"gc_version"` + File string `json:"file,omitempty"` // LSP requires an enclosing resource, i.e., a file +} + +// DocumentURI, Position, Range, Location, Diagnostic, DiagnosticRelatedInformation all reuse json definitions from gopls. +// See https://github.com/golang/tools/blob/22afafe3322a860fcd3d88448768f9db36f8bc5f/internal/lsp/protocol/tsprotocol.go + +type DocumentURI string + +type Position struct { + Line uint `json:"line"` // gopls uses float64, but json output is the same for integers + Character uint `json:"character"` // gopls uses float64, but json output is the same for integers +} + +// A Range in a text document expressed as (zero-based) start and end positions. +// A range is comparable to a selection in an editor. Therefore the end position is exclusive. +// If you want to specify a range that contains a line including the line ending character(s) +// then use an end position denoting the start of the next line. +type Range struct { + /*Start defined: + * The range's start position + */ + Start Position `json:"start"` + + /*End defined: + * The range's end position + */ + End Position `json:"end"` // exclusive +} + +// A Location represents a location inside a resource, such as a line inside a text file. +type Location struct { + // URI is + URI DocumentURI `json:"uri"` + + // Range is + Range Range `json:"range"` +} + +/* DiagnosticRelatedInformation defined: + * Represents a related message and source code location for a diagnostic. This should be + * used to point to code locations that cause or related to a diagnostics, e.g when duplicating + * a symbol in a scope. + */ +type DiagnosticRelatedInformation struct { + + /*Location defined: + * The location of this related diagnostic information. + */ + Location Location `json:"location"` + + /*Message defined: + * The message of this related diagnostic information. + */ + Message string `json:"message"` +} + +// DiagnosticSeverity defines constants +type DiagnosticSeverity uint + +const ( + /*SeverityInformation defined: + * Reports an information. + */ + SeverityInformation DiagnosticSeverity = 3 +) + +// DiagnosticTag defines constants +type DiagnosticTag uint + +/*Diagnostic defined: + * Represents a diagnostic, such as a compiler error or warning. Diagnostic objects + * are only valid in the scope of a resource. + */ +type Diagnostic struct { + + /*Range defined: + * The range at which the message applies + */ + Range Range `json:"range"` + + /*Severity defined: + * The diagnostic's severity. Can be omitted. If omitted it is up to the + * client to interpret diagnostics as error, warning, info or hint. + */ + Severity DiagnosticSeverity `json:"severity,omitempty"` // always SeverityInformation for optimizer logging. + + /*Code defined: + * The diagnostic's code, which usually appear in the user interface. + */ + Code string `json:"code,omitempty"` // LSP uses 'number | string' = gopls interface{}, but only string here, e.g. "boundsCheck", "nilcheck", etc. + + /*Source defined: + * A human-readable string describing the source of this + * diagnostic, e.g. 'typescript' or 'super lint'. It usually + * appears in the user interface. + */ + Source string `json:"source,omitempty"` // "go compiler" + + /*Message defined: + * The diagnostic's message. It usually appears in the user interface + */ + Message string `json:"message"` // sometimes used, provides additional information. + + /*Tags defined: + * Additional metadata about the diagnostic. + */ + Tags []DiagnosticTag `json:"tags,omitempty"` // always empty for logging optimizations. + + /*RelatedInformation defined: + * An array of related diagnostic information, e.g. when symbol-names within + * a scope collide all definitions can be marked via this property. + */ + RelatedInformation []DiagnosticRelatedInformation `json:"relatedInformation,omitempty"` +} + +// A LoggedOpt is what the compiler produces and accumulates, +// to be converted to JSON for human or IDE consumption. +type LoggedOpt struct { + pos src.XPos // Source code position at which the event occurred. If it is inlined, outer and all inlined locations will appear in JSON. + compilerPass string // Compiler pass. For human/adhoc consumption; does not appear in JSON (yet) + functionName string // Function name. For human/adhoc consumption; does not appear in JSON (yet) + what string // The (non) optimization; "nilcheck", "boundsCheck", "inline", "noInline" + target []interface{} // Optional target(s) or parameter(s) of "what" -- what was inlined, why it was not, size of copy, etc. 1st is most important/relevant. +} + +type logFormat uint8 + +const ( + None logFormat = iota + Json0 // version 0 for LSP 3.14, 3.15; future versions of LSP may change the format and the compiler may need to support both as clients are updated. +) + +var Format = None +var dest string + +// LogJsonOption parses and validates the version,directory value attached to the -json compiler flag. +func LogJsonOption(flagValue string) { + version, directory := parseLogFlag("json", flagValue) + if version != 0 { + log.Fatal("-json version must be 0") + } + dest = checkLogPath(directory) + Format = Json0 +} + +// parseLogFlag checks the flag passed to -json +// for version,destination format and returns the two parts. +func parseLogFlag(flag, value string) (version int, directory string) { + if Format != None { + log.Fatal("Cannot repeat -json flag") + } + commaAt := strings.Index(value, ",") + if commaAt <= 0 { + log.Fatalf("-%s option should be '<version>,<destination>' where <version> is a number", flag) + } + v, err := strconv.Atoi(value[:commaAt]) + if err != nil { + log.Fatalf("-%s option should be '<version>,<destination>' where <version> is a number: err=%v", flag, err) + } + version = v + directory = value[commaAt+1:] + return +} + +// isWindowsDriveURI returns true if the file URI is of the format used by +// Windows URIs. The url.Parse package does not specially handle Windows paths +// (see golang/go#6027), so we check if the URI path has a drive prefix (e.g. "/C:"). +// (copied from tools/internal/span/uri.go) +// this is less comprehensive that the processing in filepath.IsAbs on Windows. +func isWindowsDriveURIPath(uri string) bool { + if len(uri) < 4 { + return false + } + return uri[0] == '/' && unicode.IsLetter(rune(uri[1])) && uri[2] == ':' +} + +func parseLogPath(destination string) (string, string) { + if filepath.IsAbs(destination) { + return filepath.Clean(destination), "" + } + if strings.HasPrefix(destination, "file://") { // IKWIAD, or Windows C:\foo\bar\baz + uri, err := url.Parse(destination) + if err != nil { + return "", fmt.Sprintf("optimizer logging destination looked like file:// URI but failed to parse: err=%v", err) + } + destination = uri.Host + uri.Path + if isWindowsDriveURIPath(destination) { + // strip leading / from /C: + // unlike tools/internal/span/uri.go, do not uppercase the drive letter -- let filepath.Clean do what it does. + destination = destination[1:] + } + return filepath.Clean(destination), "" + } + return "", fmt.Sprintf("optimizer logging destination %s was neither %s-prefixed directory nor file://-prefixed file URI", destination, string(filepath.Separator)) +} + +// checkLogPath does superficial early checking of the string specifying +// the directory to which optimizer logging is directed, and if +// it passes the test, stores the string in LO_dir +func checkLogPath(destination string) string { + path, complaint := parseLogPath(destination) + if complaint != "" { + log.Fatalf(complaint) + } + err := os.MkdirAll(path, 0755) + if err != nil { + log.Fatalf("optimizer logging destination '<version>,<directory>' but could not create <directory>: err=%v", err) + } + return path +} + +var loggedOpts []*LoggedOpt +var mu = sync.Mutex{} // mu protects loggedOpts. + +// NewLoggedOpt allocates a new LoggedOpt, to later be passed to either NewLoggedOpt or LogOpt as "args". +// Pos is the source position (including inlining), what is the message, pass is which pass created the message, +// funcName is the name of the function +// A typical use for this to accumulate an explanation for a missed optimization, for example, why did something escape? +func NewLoggedOpt(pos src.XPos, what, pass, funcName string, args ...interface{}) *LoggedOpt { + pass = strings.Replace(pass, " ", "_", -1) + return &LoggedOpt{pos, pass, funcName, what, args} +} + +// Logopt logs information about a (usually missed) optimization performed by the compiler. +// Pos is the source position (including inlining), what is the message, pass is which pass created the message, +// funcName is the name of the function +func LogOpt(pos src.XPos, what, pass, funcName string, args ...interface{}) { + if Format == None { + return + } + lo := NewLoggedOpt(pos, what, pass, funcName, args...) + mu.Lock() + defer mu.Unlock() + // Because of concurrent calls from back end, no telling what the order will be, but is stable-sorted by outer Pos before use. + loggedOpts = append(loggedOpts, lo) +} + +// Enabled returns whether optimization logging is enabled. +func Enabled() bool { + switch Format { + case None: + return false + case Json0: + return true + } + panic("Unexpected optimizer-logging level") +} + +// byPos sorts diagnostics by source position. +type byPos struct { + ctxt *obj.Link + a []*LoggedOpt +} + +func (x byPos) Len() int { return len(x.a) } +func (x byPos) Less(i, j int) bool { + return x.ctxt.OutermostPos(x.a[i].pos).Before(x.ctxt.OutermostPos(x.a[j].pos)) +} +func (x byPos) Swap(i, j int) { x.a[i], x.a[j] = x.a[j], x.a[i] } + +func writerForLSP(subdirpath, file string) io.WriteCloser { + basename := file + lastslash := strings.LastIndexAny(basename, "\\/") + if lastslash != -1 { + basename = basename[lastslash+1:] + } + lastdot := strings.LastIndex(basename, ".go") + if lastdot != -1 { + basename = basename[:lastdot] + } + basename = pathEscape(basename) + + // Assume a directory, make a file + p := filepath.Join(subdirpath, basename+".json") + w, err := os.Create(p) + if err != nil { + log.Fatalf("Could not create file %s for logging optimizer actions, %v", p, err) + } + return w +} + +func fixSlash(f string) string { + if os.PathSeparator == '/' { + return f + } + return strings.Replace(f, string(os.PathSeparator), "/", -1) +} + +func uriIfy(f string) DocumentURI { + url := url.URL{ + Scheme: "file", + Path: fixSlash(f), + } + return DocumentURI(url.String()) +} + +// Return filename, replacing a first occurrence of $GOROOT with the +// actual value of the GOROOT (because LSP does not speak "$GOROOT"). +func uprootedPath(filename string) string { + if !strings.HasPrefix(filename, "$GOROOT/") { + return filename + } + return objabi.GOROOT + filename[len("$GOROOT"):] +} + +// FlushLoggedOpts flushes all the accumulated optimization log entries. +func FlushLoggedOpts(ctxt *obj.Link, slashPkgPath string) { + if Format == None { + return + } + + sort.Stable(byPos{ctxt, loggedOpts}) // Stable is necessary to preserve the per-function order, which is repeatable. + switch Format { + + case Json0: // LSP 3.15 + var posTmp []src.Pos + var encoder *json.Encoder + var w io.WriteCloser + + if slashPkgPath == "" { + slashPkgPath = "\000" + } + subdirpath := filepath.Join(dest, pathEscape(slashPkgPath)) + err := os.MkdirAll(subdirpath, 0755) + if err != nil { + log.Fatalf("Could not create directory %s for logging optimizer actions, %v", subdirpath, err) + } + diagnostic := Diagnostic{Source: "go compiler", Severity: SeverityInformation} + + // For LSP, make a subdirectory for the package, and for each file foo.go, create foo.json in that subdirectory. + currentFile := "" + for _, x := range loggedOpts { + posTmp, p0 := x.parsePos(ctxt, posTmp) + p0f := uprootedPath(p0.Filename()) + + if currentFile != p0f { + if w != nil { + w.Close() + } + currentFile = p0f + w = writerForLSP(subdirpath, currentFile) + encoder = json.NewEncoder(w) + encoder.Encode(VersionHeader{Version: 0, Package: slashPkgPath, Goos: objabi.GOOS, Goarch: objabi.GOARCH, GcVersion: objabi.Version, File: currentFile}) + } + + // The first "target" is the most important one. + var target string + if len(x.target) > 0 { + target = fmt.Sprint(x.target[0]) + } + + diagnostic.Code = x.what + diagnostic.Message = target + diagnostic.Range = newPointRange(p0) + diagnostic.RelatedInformation = diagnostic.RelatedInformation[:0] + + appendInlinedPos(posTmp, &diagnostic) + + // Diagnostic explanation is stored in RelatedInformation after inlining info + if len(x.target) > 1 { + switch y := x.target[1].(type) { + case []*LoggedOpt: + for _, z := range y { + posTmp, p0 := z.parsePos(ctxt, posTmp) + loc := newLocation(p0) + msg := z.what + if len(z.target) > 0 { + msg = msg + ": " + fmt.Sprint(z.target[0]) + } + + diagnostic.RelatedInformation = append(diagnostic.RelatedInformation, DiagnosticRelatedInformation{Location: loc, Message: msg}) + appendInlinedPos(posTmp, &diagnostic) + } + } + } + + encoder.Encode(diagnostic) + } + if w != nil { + w.Close() + } + } +} + +// newPointRange returns a single-position Range for the compiler source location p. +func newPointRange(p src.Pos) Range { + return Range{Start: Position{p.Line(), p.Col()}, + End: Position{p.Line(), p.Col()}} +} + +// newLocation returns the Location for the compiler source location p +func newLocation(p src.Pos) Location { + loc := Location{URI: uriIfy(uprootedPath(p.Filename())), Range: newPointRange(p)} + return loc +} + +// appendInlinedPos extracts inlining information from posTmp and append it to diagnostic +func appendInlinedPos(posTmp []src.Pos, diagnostic *Diagnostic) { + for i := 1; i < len(posTmp); i++ { + p := posTmp[i] + loc := newLocation(p) + diagnostic.RelatedInformation = append(diagnostic.RelatedInformation, DiagnosticRelatedInformation{Location: loc, Message: "inlineLoc"}) + } +} + +func (x *LoggedOpt) parsePos(ctxt *obj.Link, posTmp []src.Pos) ([]src.Pos, src.Pos) { + posTmp = ctxt.AllPos(x.pos, posTmp) + // Reverse posTmp to put outermost first. + l := len(posTmp) + for i := 0; i < l/2; i++ { + posTmp[i], posTmp[l-i-1] = posTmp[l-i-1], posTmp[i] + } + p0 := posTmp[0] + return posTmp, p0 +} diff --git a/src/cmd/compile/internal/logopt/logopt_test.go b/src/cmd/compile/internal/logopt/logopt_test.go new file mode 100644 index 0000000..e121c1a --- /dev/null +++ b/src/cmd/compile/internal/logopt/logopt_test.go @@ -0,0 +1,258 @@ +// 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 + 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 = \u003cN\u003e (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 +} |