whatcanGOwrong
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
// 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.
|
||||
|
||||
// authtest is a diagnostic tool for implementations of the GOAUTH protocol
|
||||
// described in https://golang.org/issue/26232.
|
||||
//
|
||||
// It accepts a single URL as an argument, and executes the GOAUTH protocol to
|
||||
// fetch and display the headers for that URL.
|
||||
//
|
||||
// CAUTION: authtest logs the GOAUTH responses, which may include user
|
||||
// credentials, to stderr. Do not post its output unless you are certain that
|
||||
// all of the credentials involved are fake!
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var v = flag.Bool("v", false, "if true, log GOAUTH responses to stderr")
|
||||
|
||||
func main() {
|
||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||
flag.Parse()
|
||||
args := flag.Args()
|
||||
if len(args) != 1 {
|
||||
log.Fatalf("usage: [GOAUTH=CMD...] %s URL", filepath.Base(os.Args[0]))
|
||||
}
|
||||
|
||||
resp := try(args[0], nil)
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return
|
||||
}
|
||||
|
||||
resp = try(args[0], resp)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func try(url string, prev *http.Response) *http.Response {
|
||||
req := new(http.Request)
|
||||
if prev != nil {
|
||||
*req = *prev.Request
|
||||
} else {
|
||||
var err error
|
||||
req, err = http.NewRequest("HEAD", os.Args[1], nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
goauth:
|
||||
for _, argList := range strings.Split(os.Getenv("GOAUTH"), ";") {
|
||||
// TODO(golang.org/issue/26849): If we escape quoted strings in GOFLAGS, use
|
||||
// the same quoting here.
|
||||
args := strings.Split(argList, " ")
|
||||
if len(args) == 0 || args[0] == "" {
|
||||
log.Fatalf("invalid or empty command in GOAUTH")
|
||||
}
|
||||
|
||||
creds, err := getCreds(args, prev)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, c := range creds {
|
||||
if c.Apply(req) {
|
||||
fmt.Fprintf(os.Stderr, "# request to %s\n", req.URL)
|
||||
fmt.Fprintf(os.Stderr, "%s %s %s\n", req.Method, req.URL, req.Proto)
|
||||
req.Header.Write(os.Stderr)
|
||||
fmt.Fprintln(os.Stderr)
|
||||
break goauth
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode < 400 || resp.StatusCode > 500 {
|
||||
log.Fatalf("unexpected status: %v", resp.Status)
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "# response from %s\n", resp.Request.URL)
|
||||
formatHead(os.Stderr, resp)
|
||||
return resp
|
||||
}
|
||||
|
||||
func formatHead(out io.Writer, resp *http.Response) {
|
||||
fmt.Fprintf(out, "%s %s\n", resp.Proto, resp.Status)
|
||||
if err := resp.Header.Write(out); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Fprintln(out)
|
||||
}
|
||||
|
||||
type Cred struct {
|
||||
URLPrefixes []*url.URL
|
||||
Header http.Header
|
||||
}
|
||||
|
||||
func (c Cred) Apply(req *http.Request) bool {
|
||||
if req.URL == nil {
|
||||
return false
|
||||
}
|
||||
ok := false
|
||||
for _, prefix := range c.URLPrefixes {
|
||||
if prefix.Host == req.URL.Host &&
|
||||
(req.URL.Path == prefix.Path ||
|
||||
(strings.HasPrefix(req.URL.Path, prefix.Path) &&
|
||||
(strings.HasSuffix(prefix.Path, "/") ||
|
||||
req.URL.Path[len(prefix.Path)] == '/'))) {
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
for k, vs := range c.Header {
|
||||
req.Header.Del(k)
|
||||
for _, v := range vs {
|
||||
req.Header.Add(k, v)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (c Cred) String() string {
|
||||
var buf strings.Builder
|
||||
for _, u := range c.URLPrefixes {
|
||||
fmt.Fprintln(&buf, u)
|
||||
}
|
||||
buf.WriteString("\n")
|
||||
c.Header.Write(&buf)
|
||||
buf.WriteString("\n")
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func getCreds(args []string, resp *http.Response) ([]Cred, error) {
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if resp != nil {
|
||||
u := *resp.Request.URL
|
||||
u.RawQuery = ""
|
||||
cmd.Args = append(cmd.Args, u.String())
|
||||
}
|
||||
|
||||
var head strings.Builder
|
||||
if resp != nil {
|
||||
formatHead(&head, resp)
|
||||
}
|
||||
cmd.Stdin = strings.NewReader(head.String())
|
||||
|
||||
fmt.Fprintf(os.Stderr, "# %s\n", strings.Join(cmd.Args, " "))
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %v", strings.Join(cmd.Args, " "), err)
|
||||
}
|
||||
os.Stderr.Write(out)
|
||||
os.Stderr.WriteString("\n")
|
||||
|
||||
var creds []Cred
|
||||
r := textproto.NewReader(bufio.NewReader(bytes.NewReader(out)))
|
||||
line := 0
|
||||
readLoop:
|
||||
for {
|
||||
var prefixes []*url.URL
|
||||
for {
|
||||
prefix, err := r.ReadLine()
|
||||
if err == io.EOF {
|
||||
if len(prefixes) > 0 {
|
||||
return nil, fmt.Errorf("line %d: %v", line, io.ErrUnexpectedEOF)
|
||||
}
|
||||
break readLoop
|
||||
}
|
||||
line++
|
||||
|
||||
if prefix == "" {
|
||||
if len(prefixes) == 0 {
|
||||
return nil, fmt.Errorf("line %d: unexpected newline", line)
|
||||
}
|
||||
break
|
||||
}
|
||||
u, err := url.Parse(prefix)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("line %d: malformed URL: %v", line, err)
|
||||
}
|
||||
if u.Scheme != "https" {
|
||||
return nil, fmt.Errorf("line %d: non-HTTPS URL %q", line, prefix)
|
||||
}
|
||||
if len(u.RawQuery) > 0 {
|
||||
return nil, fmt.Errorf("line %d: unexpected query string in URL %q", line, prefix)
|
||||
}
|
||||
if len(u.Fragment) > 0 {
|
||||
return nil, fmt.Errorf("line %d: unexpected fragment in URL %q", line, prefix)
|
||||
}
|
||||
prefixes = append(prefixes, u)
|
||||
}
|
||||
|
||||
header, err := r.ReadMIMEHeader()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("headers at line %d: %v", line, err)
|
||||
}
|
||||
if len(header) > 0 {
|
||||
creds = append(creds, Cred{
|
||||
URLPrefixes: prefixes,
|
||||
Header: http.Header(header),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return creds, nil
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
// 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.
|
||||
|
||||
// cookieauth uses a “Netscape cookie file” to implement the GOAUTH protocol
|
||||
// described in https://golang.org/issue/26232.
|
||||
// It expects the location of the file as the first command-line argument.
|
||||
//
|
||||
// Example GOAUTH usage:
|
||||
//
|
||||
// export GOAUTH="cookieauth $(git config --get http.cookieFile)"
|
||||
//
|
||||
// See http://www.cookiecentral.com/faq/#3.5 for a description of the Netscape
|
||||
// cookie file format.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Fprintf(os.Stderr, "usage: %s COOKIEFILE [URL]\n", os.Args[0])
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
log.SetPrefix("cookieauth: ")
|
||||
|
||||
f, err := os.Open(os.Args[1])
|
||||
if err != nil {
|
||||
log.Fatalf("failed to read cookie file: %v\n", os.Args[1])
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var (
|
||||
targetURL *url.URL
|
||||
targetURLs = map[string]*url.URL{}
|
||||
)
|
||||
if len(os.Args) == 3 {
|
||||
targetURL, err = url.ParseRequestURI(os.Args[2])
|
||||
if err != nil {
|
||||
log.Fatalf("invalid request URI (%v): %q\n", err, os.Args[2])
|
||||
}
|
||||
targetURLs[targetURL.String()] = targetURL
|
||||
} else if len(os.Args) > 3 {
|
||||
// Extra arguments were passed: maybe the protocol was expanded?
|
||||
// We don't know how to interpret the request, so ignore it.
|
||||
return
|
||||
}
|
||||
|
||||
entries, err := parseCookieFile(f.Name(), f)
|
||||
if err != nil {
|
||||
log.Fatalf("error reading cookie file: %v\n", f.Name())
|
||||
}
|
||||
|
||||
jar, err := cookiejar.New(nil)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to initialize cookie jar: %v\n", err)
|
||||
}
|
||||
|
||||
for _, e := range entries {
|
||||
u := &url.URL{
|
||||
Scheme: "https",
|
||||
Host: e.Host,
|
||||
Path: e.Cookie.Path,
|
||||
}
|
||||
|
||||
if targetURL == nil {
|
||||
targetURLs[u.String()] = u
|
||||
}
|
||||
|
||||
jar.SetCookies(u, []*http.Cookie{&e.Cookie})
|
||||
}
|
||||
|
||||
for _, u := range targetURLs {
|
||||
req := &http.Request{URL: u, Header: make(http.Header)}
|
||||
for _, c := range jar.Cookies(req.URL) {
|
||||
req.AddCookie(c)
|
||||
}
|
||||
fmt.Printf("%s\n\n", u)
|
||||
req.Header.Write(os.Stdout)
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
|
||||
type Entry struct {
|
||||
Host string
|
||||
Cookie http.Cookie
|
||||
}
|
||||
|
||||
// parseCookieFile parses a Netscape cookie file as described in
|
||||
// http://www.cookiecentral.com/faq/#3.5.
|
||||
func parseCookieFile(name string, r io.Reader) ([]*Entry, error) {
|
||||
var entries []*Entry
|
||||
s := bufio.NewScanner(r)
|
||||
line := 0
|
||||
for s.Scan() {
|
||||
line++
|
||||
text := strings.TrimSpace(s.Text())
|
||||
if len(text) < 2 || (text[0] == '#' && unicode.IsSpace(rune(text[1]))) {
|
||||
continue
|
||||
}
|
||||
|
||||
e, err := parseCookieLine(text)
|
||||
if err != nil {
|
||||
log.Printf("%s:%d: %v\n", name, line, err)
|
||||
continue
|
||||
}
|
||||
entries = append(entries, e)
|
||||
}
|
||||
return entries, s.Err()
|
||||
}
|
||||
|
||||
func parseCookieLine(line string) (*Entry, error) {
|
||||
f := strings.Fields(line)
|
||||
if len(f) < 7 {
|
||||
return nil, fmt.Errorf("found %d columns; want 7", len(f))
|
||||
}
|
||||
|
||||
e := new(Entry)
|
||||
c := &e.Cookie
|
||||
|
||||
if domain := f[0]; strings.HasPrefix(domain, "#HttpOnly_") {
|
||||
c.HttpOnly = true
|
||||
e.Host = strings.TrimPrefix(domain[10:], ".")
|
||||
} else {
|
||||
e.Host = strings.TrimPrefix(domain, ".")
|
||||
}
|
||||
|
||||
isDomain, err := strconv.ParseBool(f[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("non-boolean domain flag: %v", err)
|
||||
}
|
||||
if isDomain {
|
||||
c.Domain = e.Host
|
||||
}
|
||||
|
||||
c.Path = f[2]
|
||||
|
||||
c.Secure, err = strconv.ParseBool(f[3])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("non-boolean secure flag: %v", err)
|
||||
}
|
||||
|
||||
expiration, err := strconv.ParseInt(f[4], 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("malformed expiration: %v", err)
|
||||
}
|
||||
c.Expires = time.Unix(expiration, 0)
|
||||
|
||||
c.Name = f[5]
|
||||
c.Value = f[6]
|
||||
|
||||
return e, nil
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
// 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.
|
||||
|
||||
// gitauth uses 'git credential' to implement the GOAUTH protocol described in
|
||||
// https://golang.org/issue/26232. It expects an absolute path to the working
|
||||
// directory for the 'git' command as the first command-line argument.
|
||||
//
|
||||
// Example GOAUTH usage:
|
||||
//
|
||||
// export GOAUTH="gitauth $HOME"
|
||||
//
|
||||
// See https://git-scm.com/docs/gitcredentials or run 'man gitcredentials' for
|
||||
// information on how to configure 'git credential'.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 || !filepath.IsAbs(os.Args[1]) {
|
||||
fmt.Fprintf(os.Stderr, "usage: %s WORKDIR [URL]", os.Args[0])
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
log.SetPrefix("gitauth: ")
|
||||
|
||||
if len(os.Args) != 3 {
|
||||
// No explicit URL was passed on the command line, but 'git credential'
|
||||
// provides no way to enumerate existing credentials.
|
||||
// Wait for a request for a specific URL.
|
||||
return
|
||||
}
|
||||
|
||||
u, err := url.ParseRequestURI(os.Args[2])
|
||||
if err != nil {
|
||||
log.Fatalf("invalid request URI (%v): %q\n", err, os.Args[1])
|
||||
}
|
||||
|
||||
var (
|
||||
prefix *url.URL
|
||||
lastHeader http.Header
|
||||
lastStatus = http.StatusUnauthorized
|
||||
)
|
||||
for lastStatus == http.StatusUnauthorized {
|
||||
cmd := exec.Command("git", "credential", "fill")
|
||||
|
||||
// We don't want to execute a 'git' command in an arbitrary directory, since
|
||||
// that opens up a number of config-injection attacks (for example,
|
||||
// https://golang.org/issue/29230). Instead, we have the user configure a
|
||||
// directory explicitly on the command line.
|
||||
cmd.Dir = os.Args[1]
|
||||
|
||||
cmd.Stdin = strings.NewReader(fmt.Sprintf("url=%s\n", u))
|
||||
cmd.Stderr = os.Stderr
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
log.Fatalf("'git credential fill' failed: %v\n", err)
|
||||
}
|
||||
|
||||
prefix = new(url.URL)
|
||||
var username, password string
|
||||
lines := strings.Split(string(out), "\n")
|
||||
for _, line := range lines {
|
||||
frags := strings.SplitN(line, "=", 2)
|
||||
if len(frags) != 2 {
|
||||
continue // Ignore unrecognized response lines.
|
||||
}
|
||||
switch strings.TrimSpace(frags[0]) {
|
||||
case "protocol":
|
||||
prefix.Scheme = frags[1]
|
||||
case "host":
|
||||
prefix.Host = frags[1]
|
||||
case "path":
|
||||
prefix.Path = frags[1]
|
||||
case "username":
|
||||
username = frags[1]
|
||||
case "password":
|
||||
password = frags[1]
|
||||
case "url":
|
||||
// Write to a local variable instead of updating prefix directly:
|
||||
// if the url field is malformed, we don't want to invalidate
|
||||
// information parsed from the protocol, host, and path fields.
|
||||
u, err := url.ParseRequestURI(frags[1])
|
||||
if err == nil {
|
||||
prefix = u
|
||||
} else {
|
||||
log.Printf("malformed URL from 'git credential fill' (%v): %q\n", err, frags[1])
|
||||
// Proceed anyway: we might be able to parse the prefix from other fields of the response.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Double-check that the URL Git gave us is a prefix of the one we requested.
|
||||
if !strings.HasPrefix(u.String(), prefix.String()) {
|
||||
log.Fatalf("requested a credential for %q, but 'git credential fill' provided one for %q\n", u, prefix)
|
||||
}
|
||||
|
||||
// Send a HEAD request to try to detect whether the credential is valid.
|
||||
// If the user just typed in a correct password and has caching enabled,
|
||||
// we don't want to nag them for it again the next time they run a 'go' command.
|
||||
req, err := http.NewRequest("HEAD", u.String(), nil)
|
||||
if err != nil {
|
||||
log.Fatalf("internal error constructing HTTP HEAD request: %v\n", err)
|
||||
}
|
||||
req.SetBasicAuth(username, password)
|
||||
lastHeader = req.Header
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
log.Printf("HTTPS HEAD request failed to connect: %v\n", err)
|
||||
// Couldn't verify the credential, but we have no evidence that it is invalid either.
|
||||
// Proceed, but don't update git's credential cache.
|
||||
break
|
||||
}
|
||||
lastStatus = resp.StatusCode
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
log.Printf("%s: %v %s\n", u, resp.StatusCode, http.StatusText(resp.StatusCode))
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusUnauthorized {
|
||||
// We learned something about the credential: it either worked or it was invalid.
|
||||
// Approve or reject the credential (on a best-effort basis)
|
||||
// so that the git credential helper can update its cache as appropriate.
|
||||
action := "approve"
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
action = "reject"
|
||||
}
|
||||
cmd = exec.Command("git", "credential", action)
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout = os.Stderr
|
||||
cmd.Stdin = bytes.NewReader(out)
|
||||
_ = cmd.Run()
|
||||
}
|
||||
}
|
||||
|
||||
// Write out the credential in the format expected by the 'go' command.
|
||||
fmt.Printf("%s\n\n", prefix)
|
||||
lastHeader.Write(os.Stdout)
|
||||
fmt.Println()
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
// Copyright 2018 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.
|
||||
|
||||
// netrcauth uses a .netrc file (or _netrc file on Windows) to implement the
|
||||
// GOAUTH protocol described in https://golang.org/issue/26232.
|
||||
// It expects the location of the file as the first command-line argument.
|
||||
//
|
||||
// Example GOAUTH usage:
|
||||
//
|
||||
// export GOAUTH="netrcauth $HOME/.netrc"
|
||||
//
|
||||
// See https://www.gnu.org/software/inetutils/manual/html_node/The-_002enetrc-file.html
|
||||
// or run 'man 5 netrc' for a description of the .netrc file format.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Fprintf(os.Stderr, "usage: %s NETRCFILE [URL]", os.Args[0])
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
log.SetPrefix("netrcauth: ")
|
||||
|
||||
if len(os.Args) != 2 {
|
||||
// An explicit URL was passed on the command line, but netrcauth does not
|
||||
// have any URL-specific output: it dumps the entire .netrc file at the
|
||||
// first call.
|
||||
return
|
||||
}
|
||||
|
||||
path := os.Args[1]
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
log.Fatalf("failed to read %s: %v\n", path, err)
|
||||
}
|
||||
|
||||
u := &url.URL{Scheme: "https"}
|
||||
lines := parseNetrc(string(data))
|
||||
for _, l := range lines {
|
||||
u.Host = l.machine
|
||||
fmt.Printf("%s\n\n", u)
|
||||
|
||||
req := &http.Request{Header: make(http.Header)}
|
||||
req.SetBasicAuth(l.login, l.password)
|
||||
req.Header.Write(os.Stdout)
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
|
||||
// The following functions were extracted from src/cmd/go/internal/web2/web.go
|
||||
// as of https://golang.org/cl/161698.
|
||||
|
||||
type netrcLine struct {
|
||||
machine string
|
||||
login string
|
||||
password string
|
||||
}
|
||||
|
||||
func parseNetrc(data string) []netrcLine {
|
||||
// See https://www.gnu.org/software/inetutils/manual/html_node/The-_002enetrc-file.html
|
||||
// for documentation on the .netrc format.
|
||||
var nrc []netrcLine
|
||||
var l netrcLine
|
||||
inMacro := false
|
||||
for _, line := range strings.Split(data, "\n") {
|
||||
if inMacro {
|
||||
if line == "" {
|
||||
inMacro = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
f := strings.Fields(line)
|
||||
i := 0
|
||||
for ; i < len(f)-1; i += 2 {
|
||||
// Reset at each "machine" token.
|
||||
// “The auto-login process searches the .netrc file for a machine token
|
||||
// that matches […]. Once a match is made, the subsequent .netrc tokens
|
||||
// are processed, stopping when the end of file is reached or another
|
||||
// machine or a default token is encountered.”
|
||||
switch f[i] {
|
||||
case "machine":
|
||||
l = netrcLine{machine: f[i+1]}
|
||||
case "default":
|
||||
break
|
||||
case "login":
|
||||
l.login = f[i+1]
|
||||
case "password":
|
||||
l.password = f[i+1]
|
||||
case "macdef":
|
||||
// “A macro is defined with the specified name; its contents begin with
|
||||
// the next .netrc line and continue until a null line (consecutive
|
||||
// new-line characters) is encountered.”
|
||||
inMacro = true
|
||||
}
|
||||
if l.machine != "" && l.login != "" && l.password != "" {
|
||||
nrc = append(nrc, l)
|
||||
l = netrcLine{}
|
||||
}
|
||||
}
|
||||
|
||||
if i < len(f) && f[i] == "default" {
|
||||
// “There can be only one default token, and it must be after all machine tokens.”
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nrc
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
// Copyright 2014 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 main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"text/tabwriter"
|
||||
|
||||
"golang.org/x/tools/benchmark/parse"
|
||||
)
|
||||
|
||||
var (
|
||||
changedOnly = flag.Bool("changed", false, "show only benchmarks that have changed")
|
||||
magSort = flag.Bool("mag", false, "sort benchmarks by magnitude of change")
|
||||
best = flag.Bool("best", false, "compare best times from old and new")
|
||||
)
|
||||
|
||||
const usageFooter = `
|
||||
Each input file should be from:
|
||||
go test -run=NONE -bench=. > [old,new].txt
|
||||
|
||||
Benchcmp compares old and new for each benchmark.
|
||||
|
||||
If -test.benchmem=true is added to the "go test" command
|
||||
benchcmp will also compare memory allocations.
|
||||
`
|
||||
|
||||
func main() {
|
||||
fmt.Fprintf(os.Stderr, "benchcmp is deprecated in favor of benchstat: https://pkg.go.dev/golang.org/x/perf/cmd/benchstat\n")
|
||||
flag.Usage = func() {
|
||||
fmt.Fprintf(os.Stderr, "usage: %s old.txt new.txt\n\n", os.Args[0])
|
||||
flag.PrintDefaults()
|
||||
fmt.Fprint(os.Stderr, usageFooter)
|
||||
os.Exit(2)
|
||||
}
|
||||
flag.Parse()
|
||||
if flag.NArg() != 2 {
|
||||
flag.Usage()
|
||||
}
|
||||
|
||||
before := parseFile(flag.Arg(0))
|
||||
after := parseFile(flag.Arg(1))
|
||||
|
||||
cmps, warnings := Correlate(before, after)
|
||||
|
||||
for _, warn := range warnings {
|
||||
fmt.Fprintln(os.Stderr, warn)
|
||||
}
|
||||
|
||||
if len(cmps) == 0 {
|
||||
fatal("benchcmp: no repeated benchmarks")
|
||||
}
|
||||
|
||||
w := new(tabwriter.Writer)
|
||||
w.Init(os.Stdout, 0, 0, 5, ' ', 0)
|
||||
defer w.Flush()
|
||||
|
||||
var header bool // Has the header has been displayed yet for a given block?
|
||||
|
||||
if *magSort {
|
||||
sort.Sort(ByDeltaNsPerOp(cmps))
|
||||
} else {
|
||||
sort.Sort(ByParseOrder(cmps))
|
||||
}
|
||||
for _, cmp := range cmps {
|
||||
if !cmp.Measured(parse.NsPerOp) {
|
||||
continue
|
||||
}
|
||||
if delta := cmp.DeltaNsPerOp(); !*changedOnly || delta.Changed() {
|
||||
if !header {
|
||||
fmt.Fprint(w, "benchmark\told ns/op\tnew ns/op\tdelta\n")
|
||||
header = true
|
||||
}
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", cmp.Name(), formatNs(cmp.Before.NsPerOp), formatNs(cmp.After.NsPerOp), delta.Percent())
|
||||
}
|
||||
}
|
||||
|
||||
header = false
|
||||
if *magSort {
|
||||
sort.Sort(ByDeltaMBPerS(cmps))
|
||||
}
|
||||
for _, cmp := range cmps {
|
||||
if !cmp.Measured(parse.MBPerS) {
|
||||
continue
|
||||
}
|
||||
if delta := cmp.DeltaMBPerS(); !*changedOnly || delta.Changed() {
|
||||
if !header {
|
||||
fmt.Fprint(w, "\nbenchmark\told MB/s\tnew MB/s\tspeedup\n")
|
||||
header = true
|
||||
}
|
||||
fmt.Fprintf(w, "%s\t%.2f\t%.2f\t%s\n", cmp.Name(), cmp.Before.MBPerS, cmp.After.MBPerS, delta.Multiple())
|
||||
}
|
||||
}
|
||||
|
||||
header = false
|
||||
if *magSort {
|
||||
sort.Sort(ByDeltaAllocsPerOp(cmps))
|
||||
}
|
||||
for _, cmp := range cmps {
|
||||
if !cmp.Measured(parse.AllocsPerOp) {
|
||||
continue
|
||||
}
|
||||
if delta := cmp.DeltaAllocsPerOp(); !*changedOnly || delta.Changed() {
|
||||
if !header {
|
||||
fmt.Fprint(w, "\nbenchmark\told allocs\tnew allocs\tdelta\n")
|
||||
header = true
|
||||
}
|
||||
fmt.Fprintf(w, "%s\t%d\t%d\t%s\n", cmp.Name(), cmp.Before.AllocsPerOp, cmp.After.AllocsPerOp, delta.Percent())
|
||||
}
|
||||
}
|
||||
|
||||
header = false
|
||||
if *magSort {
|
||||
sort.Sort(ByDeltaAllocedBytesPerOp(cmps))
|
||||
}
|
||||
for _, cmp := range cmps {
|
||||
if !cmp.Measured(parse.AllocedBytesPerOp) {
|
||||
continue
|
||||
}
|
||||
if delta := cmp.DeltaAllocedBytesPerOp(); !*changedOnly || delta.Changed() {
|
||||
if !header {
|
||||
fmt.Fprint(w, "\nbenchmark\told bytes\tnew bytes\tdelta\n")
|
||||
header = true
|
||||
}
|
||||
fmt.Fprintf(w, "%s\t%d\t%d\t%s\n", cmp.Name(), cmp.Before.AllocedBytesPerOp, cmp.After.AllocedBytesPerOp, cmp.DeltaAllocedBytesPerOp().Percent())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fatal(msg interface{}) {
|
||||
fmt.Fprintln(os.Stderr, msg)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func parseFile(path string) parse.Set {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
bb, err := parse.ParseSet(f)
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
if *best {
|
||||
selectBest(bb)
|
||||
}
|
||||
return bb
|
||||
}
|
||||
|
||||
func selectBest(bs parse.Set) {
|
||||
for name, bb := range bs {
|
||||
if len(bb) < 2 {
|
||||
continue
|
||||
}
|
||||
ord := bb[0].Ord
|
||||
best := bb[0]
|
||||
for _, b := range bb {
|
||||
if b.NsPerOp < best.NsPerOp {
|
||||
b.Ord = ord
|
||||
best = b
|
||||
}
|
||||
}
|
||||
bs[name] = []*parse.Benchmark{best}
|
||||
}
|
||||
}
|
||||
|
||||
// formatNs formats ns measurements to expose a useful amount of
|
||||
// precision. It mirrors the ns precision logic of testing.B.
|
||||
func formatNs(ns float64) string {
|
||||
prec := 0
|
||||
switch {
|
||||
case ns < 10:
|
||||
prec = 2
|
||||
case ns < 100:
|
||||
prec = 1
|
||||
}
|
||||
return strconv.FormatFloat(ns, 'f', prec, 64)
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// Copyright 2014 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 main
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/benchmark/parse"
|
||||
)
|
||||
|
||||
func TestSelectBest(t *testing.T) {
|
||||
have := parse.Set{
|
||||
"Benchmark1": []*parse.Benchmark{
|
||||
{
|
||||
Name: "Benchmark1",
|
||||
N: 10, NsPerOp: 100, Measured: parse.NsPerOp,
|
||||
Ord: 0,
|
||||
},
|
||||
{
|
||||
Name: "Benchmark1",
|
||||
N: 10, NsPerOp: 50, Measured: parse.NsPerOp,
|
||||
Ord: 3,
|
||||
},
|
||||
},
|
||||
"Benchmark2": []*parse.Benchmark{
|
||||
{
|
||||
Name: "Benchmark2",
|
||||
N: 10, NsPerOp: 60, Measured: parse.NsPerOp,
|
||||
Ord: 1,
|
||||
},
|
||||
{
|
||||
Name: "Benchmark2",
|
||||
N: 10, NsPerOp: 500, Measured: parse.NsPerOp,
|
||||
Ord: 2,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
want := parse.Set{
|
||||
"Benchmark1": []*parse.Benchmark{
|
||||
{
|
||||
Name: "Benchmark1",
|
||||
N: 10, NsPerOp: 50, Measured: parse.NsPerOp,
|
||||
Ord: 0,
|
||||
},
|
||||
},
|
||||
"Benchmark2": []*parse.Benchmark{
|
||||
{
|
||||
Name: "Benchmark2",
|
||||
N: 10, NsPerOp: 60, Measured: parse.NsPerOp,
|
||||
Ord: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
selectBest(have)
|
||||
if !reflect.DeepEqual(want, have) {
|
||||
t.Errorf("filtered bench set incorrectly, want %v have %v", want, have)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatNs(t *testing.T) {
|
||||
tests := []struct {
|
||||
input float64
|
||||
expected string
|
||||
}{
|
||||
{input: 0, expected: "0.00"},
|
||||
{input: 0.2, expected: "0.20"},
|
||||
{input: 2, expected: "2.00"},
|
||||
{input: 2.2, expected: "2.20"},
|
||||
{input: 4, expected: "4.00"},
|
||||
{input: 16, expected: "16.0"},
|
||||
{input: 16.08, expected: "16.1"},
|
||||
{input: 128, expected: "128"},
|
||||
{input: 256.2, expected: "256"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
actual := formatNs(tt.input)
|
||||
if actual != tt.expected {
|
||||
t.Fatalf("%f. got %q, want %q", tt.input, actual, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
// Copyright 2014 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 main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"golang.org/x/tools/benchmark/parse"
|
||||
)
|
||||
|
||||
// BenchCmp is a pair of benchmarks.
|
||||
type BenchCmp struct {
|
||||
Before *parse.Benchmark
|
||||
After *parse.Benchmark
|
||||
}
|
||||
|
||||
// Correlate correlates benchmarks from two BenchSets.
|
||||
func Correlate(before, after parse.Set) (cmps []BenchCmp, warnings []string) {
|
||||
cmps = make([]BenchCmp, 0, len(after))
|
||||
for name, beforebb := range before {
|
||||
afterbb := after[name]
|
||||
if len(beforebb) != len(afterbb) {
|
||||
warnings = append(warnings, fmt.Sprintf("ignoring %s: before has %d instances, after has %d", name, len(beforebb), len(afterbb)))
|
||||
continue
|
||||
}
|
||||
for i, beforeb := range beforebb {
|
||||
afterb := afterbb[i]
|
||||
cmps = append(cmps, BenchCmp{beforeb, afterb})
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c BenchCmp) Name() string { return c.Before.Name }
|
||||
func (c BenchCmp) String() string { return fmt.Sprintf("<%s, %s>", c.Before, c.After) }
|
||||
func (c BenchCmp) Measured(flag int) bool { return (c.Before.Measured & c.After.Measured & flag) != 0 }
|
||||
func (c BenchCmp) DeltaNsPerOp() Delta { return Delta{c.Before.NsPerOp, c.After.NsPerOp} }
|
||||
func (c BenchCmp) DeltaMBPerS() Delta { return Delta{c.Before.MBPerS, c.After.MBPerS} }
|
||||
func (c BenchCmp) DeltaAllocedBytesPerOp() Delta {
|
||||
return Delta{float64(c.Before.AllocedBytesPerOp), float64(c.After.AllocedBytesPerOp)}
|
||||
}
|
||||
func (c BenchCmp) DeltaAllocsPerOp() Delta {
|
||||
return Delta{float64(c.Before.AllocsPerOp), float64(c.After.AllocsPerOp)}
|
||||
}
|
||||
|
||||
// Delta is the before and after value for a benchmark measurement.
|
||||
// Both must be non-negative.
|
||||
type Delta struct {
|
||||
Before float64
|
||||
After float64
|
||||
}
|
||||
|
||||
// mag calculates the magnitude of a change, regardless of the direction of
|
||||
// the change. mag is intended for sorting and has no independent meaning.
|
||||
func (d Delta) mag() float64 {
|
||||
switch {
|
||||
case d.Before != 0 && d.After != 0 && d.Before >= d.After:
|
||||
return d.After / d.Before
|
||||
case d.Before != 0 && d.After != 0 && d.Before < d.After:
|
||||
return d.Before / d.After
|
||||
case d.Before == 0 && d.After == 0:
|
||||
return 1
|
||||
default:
|
||||
// 0 -> 1 or 1 -> 0
|
||||
// These are significant changes and worth surfacing.
|
||||
return math.Inf(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Changed reports whether the benchmark quantities are different.
|
||||
func (d Delta) Changed() bool { return d.Before != d.After }
|
||||
|
||||
// Float64 returns After / Before. If Before is 0, Float64 returns
|
||||
// 1 if After is also 0, and +Inf otherwise.
|
||||
func (d Delta) Float64() float64 {
|
||||
switch {
|
||||
case d.Before != 0:
|
||||
return d.After / d.Before
|
||||
case d.After == 0:
|
||||
return 1
|
||||
default:
|
||||
return math.Inf(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Percent formats a Delta as a percent change, ranging from -100% up.
|
||||
func (d Delta) Percent() string {
|
||||
return fmt.Sprintf("%+.2f%%", 100*d.Float64()-100)
|
||||
}
|
||||
|
||||
// Multiple formats a Delta as a multiplier, ranging from 0.00x up.
|
||||
func (d Delta) Multiple() string {
|
||||
return fmt.Sprintf("%.2fx", d.Float64())
|
||||
}
|
||||
|
||||
func (d Delta) String() string {
|
||||
return fmt.Sprintf("Δ(%f, %f)", d.Before, d.After)
|
||||
}
|
||||
|
||||
// ByParseOrder sorts BenchCmps to match the order in
|
||||
// which the Before benchmarks were presented to Parse.
|
||||
type ByParseOrder []BenchCmp
|
||||
|
||||
func (x ByParseOrder) Len() int { return len(x) }
|
||||
func (x ByParseOrder) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
|
||||
func (x ByParseOrder) Less(i, j int) bool { return x[i].Before.Ord < x[j].Before.Ord }
|
||||
|
||||
// lessByDelta provides lexicographic ordering:
|
||||
// - largest delta by magnitude
|
||||
// - alphabetic by name
|
||||
func lessByDelta(i, j BenchCmp, calcDelta func(BenchCmp) Delta) bool {
|
||||
iDelta, jDelta := calcDelta(i).mag(), calcDelta(j).mag()
|
||||
if iDelta != jDelta {
|
||||
return iDelta < jDelta
|
||||
}
|
||||
return i.Name() < j.Name()
|
||||
}
|
||||
|
||||
// ByDeltaNsPerOp sorts BenchCmps lexicographically by change
|
||||
// in ns/op, descending, then by benchmark name.
|
||||
type ByDeltaNsPerOp []BenchCmp
|
||||
|
||||
func (x ByDeltaNsPerOp) Len() int { return len(x) }
|
||||
func (x ByDeltaNsPerOp) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
|
||||
func (x ByDeltaNsPerOp) Less(i, j int) bool { return lessByDelta(x[i], x[j], BenchCmp.DeltaNsPerOp) }
|
||||
|
||||
// ByDeltaMBPerS sorts BenchCmps lexicographically by change
|
||||
// in MB/s, descending, then by benchmark name.
|
||||
type ByDeltaMBPerS []BenchCmp
|
||||
|
||||
func (x ByDeltaMBPerS) Len() int { return len(x) }
|
||||
func (x ByDeltaMBPerS) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
|
||||
func (x ByDeltaMBPerS) Less(i, j int) bool { return lessByDelta(x[i], x[j], BenchCmp.DeltaMBPerS) }
|
||||
|
||||
// ByDeltaAllocedBytesPerOp sorts BenchCmps lexicographically by change
|
||||
// in B/op, descending, then by benchmark name.
|
||||
type ByDeltaAllocedBytesPerOp []BenchCmp
|
||||
|
||||
func (x ByDeltaAllocedBytesPerOp) Len() int { return len(x) }
|
||||
func (x ByDeltaAllocedBytesPerOp) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
|
||||
func (x ByDeltaAllocedBytesPerOp) Less(i, j int) bool {
|
||||
return lessByDelta(x[i], x[j], BenchCmp.DeltaAllocedBytesPerOp)
|
||||
}
|
||||
|
||||
// ByDeltaAllocsPerOp sorts BenchCmps lexicographically by change
|
||||
// in allocs/op, descending, then by benchmark name.
|
||||
type ByDeltaAllocsPerOp []BenchCmp
|
||||
|
||||
func (x ByDeltaAllocsPerOp) Len() int { return len(x) }
|
||||
func (x ByDeltaAllocsPerOp) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
|
||||
func (x ByDeltaAllocsPerOp) Less(i, j int) bool {
|
||||
return lessByDelta(x[i], x[j], BenchCmp.DeltaAllocsPerOp)
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
// Copyright 2014 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 main
|
||||
|
||||
import (
|
||||
"math"
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/benchmark/parse"
|
||||
)
|
||||
|
||||
func TestDelta(t *testing.T) {
|
||||
cases := []struct {
|
||||
before float64
|
||||
after float64
|
||||
mag float64
|
||||
f float64
|
||||
changed bool
|
||||
pct string
|
||||
mult string
|
||||
}{
|
||||
{before: 1, after: 1, mag: 1, f: 1, changed: false, pct: "+0.00%", mult: "1.00x"},
|
||||
{before: 1, after: 2, mag: 0.5, f: 2, changed: true, pct: "+100.00%", mult: "2.00x"},
|
||||
{before: 2, after: 1, mag: 0.5, f: 0.5, changed: true, pct: "-50.00%", mult: "0.50x"},
|
||||
{before: 0, after: 0, mag: 1, f: 1, changed: false, pct: "+0.00%", mult: "1.00x"},
|
||||
{before: 1, after: 0, mag: math.Inf(1), f: 0, changed: true, pct: "-100.00%", mult: "0.00x"},
|
||||
{before: 0, after: 1, mag: math.Inf(1), f: math.Inf(1), changed: true, pct: "+Inf%", mult: "+Infx"},
|
||||
}
|
||||
for _, tt := range cases {
|
||||
d := Delta{tt.before, tt.after}
|
||||
if want, have := tt.mag, d.mag(); want != have {
|
||||
t.Errorf("%s.mag(): want %f have %f", d, want, have)
|
||||
}
|
||||
if want, have := tt.f, d.Float64(); want != have {
|
||||
t.Errorf("%s.Float64(): want %f have %f", d, want, have)
|
||||
}
|
||||
if want, have := tt.changed, d.Changed(); want != have {
|
||||
t.Errorf("%s.Changed(): want %t have %t", d, want, have)
|
||||
}
|
||||
if want, have := tt.pct, d.Percent(); want != have {
|
||||
t.Errorf("%s.Percent(): want %q have %q", d, want, have)
|
||||
}
|
||||
if want, have := tt.mult, d.Multiple(); want != have {
|
||||
t.Errorf("%s.Multiple(): want %q have %q", d, want, have)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCorrelate(t *testing.T) {
|
||||
// Benches that are going to be successfully correlated get N thus:
|
||||
// 0x<counter><num benches><b = before | a = after>
|
||||
// Read this: "<counter> of <num benches>, from <before|after>".
|
||||
before := parse.Set{
|
||||
"BenchmarkOneEach": []*parse.Benchmark{{Name: "BenchmarkOneEach", N: 0x11b}},
|
||||
"BenchmarkOneToNone": []*parse.Benchmark{{Name: "BenchmarkOneToNone"}},
|
||||
"BenchmarkOneToTwo": []*parse.Benchmark{{Name: "BenchmarkOneToTwo"}},
|
||||
"BenchmarkTwoToOne": []*parse.Benchmark{
|
||||
{Name: "BenchmarkTwoToOne"},
|
||||
{Name: "BenchmarkTwoToOne"},
|
||||
},
|
||||
"BenchmarkTwoEach": []*parse.Benchmark{
|
||||
{Name: "BenchmarkTwoEach", N: 0x12b},
|
||||
{Name: "BenchmarkTwoEach", N: 0x22b},
|
||||
},
|
||||
}
|
||||
|
||||
after := parse.Set{
|
||||
"BenchmarkOneEach": []*parse.Benchmark{{Name: "BenchmarkOneEach", N: 0x11a}},
|
||||
"BenchmarkNoneToOne": []*parse.Benchmark{{Name: "BenchmarkNoneToOne"}},
|
||||
"BenchmarkTwoToOne": []*parse.Benchmark{{Name: "BenchmarkTwoToOne"}},
|
||||
"BenchmarkOneToTwo": []*parse.Benchmark{
|
||||
{Name: "BenchmarkOneToTwo"},
|
||||
{Name: "BenchmarkOneToTwo"},
|
||||
},
|
||||
"BenchmarkTwoEach": []*parse.Benchmark{
|
||||
{Name: "BenchmarkTwoEach", N: 0x12a},
|
||||
{Name: "BenchmarkTwoEach", N: 0x22a},
|
||||
},
|
||||
}
|
||||
|
||||
pairs, errs := Correlate(before, after)
|
||||
|
||||
// Fail to match: BenchmarkOneToNone, BenchmarkOneToTwo, BenchmarkTwoToOne.
|
||||
// Correlate does not notice BenchmarkNoneToOne.
|
||||
if len(errs) != 3 {
|
||||
t.Errorf("Correlated expected 4 errors, got %d: %v", len(errs), errs)
|
||||
}
|
||||
|
||||
// Want three correlated pairs: one BenchmarkOneEach, two BenchmarkTwoEach.
|
||||
if len(pairs) != 3 {
|
||||
t.Fatalf("Correlated expected 3 pairs, got %v", pairs)
|
||||
}
|
||||
|
||||
for _, pair := range pairs {
|
||||
if pair.Before.N&0xF != 0xb {
|
||||
t.Errorf("unexpected Before in pair %s", pair)
|
||||
}
|
||||
if pair.After.N&0xF != 0xa {
|
||||
t.Errorf("unexpected After in pair %s", pair)
|
||||
}
|
||||
if pair.Before.N>>4 != pair.After.N>>4 {
|
||||
t.Errorf("mismatched pair %s", pair)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBenchCmpSorting(t *testing.T) {
|
||||
c := []BenchCmp{
|
||||
{&parse.Benchmark{Name: "BenchmarkMuchFaster", NsPerOp: 10, Ord: 3}, &parse.Benchmark{Name: "BenchmarkMuchFaster", NsPerOp: 1}},
|
||||
{&parse.Benchmark{Name: "BenchmarkSameB", NsPerOp: 5, Ord: 1}, &parse.Benchmark{Name: "BenchmarkSameB", NsPerOp: 5}},
|
||||
{&parse.Benchmark{Name: "BenchmarkSameA", NsPerOp: 5, Ord: 2}, &parse.Benchmark{Name: "BenchmarkSameA", NsPerOp: 5}},
|
||||
{&parse.Benchmark{Name: "BenchmarkSlower", NsPerOp: 10, Ord: 0}, &parse.Benchmark{Name: "BenchmarkSlower", NsPerOp: 11}},
|
||||
}
|
||||
|
||||
// Test just one magnitude-based sort order; they are symmetric.
|
||||
sort.Sort(ByDeltaNsPerOp(c))
|
||||
want := []string{"BenchmarkMuchFaster", "BenchmarkSlower", "BenchmarkSameA", "BenchmarkSameB"}
|
||||
have := []string{c[0].Name(), c[1].Name(), c[2].Name(), c[3].Name()}
|
||||
if !reflect.DeepEqual(want, have) {
|
||||
t.Errorf("ByDeltaNsOp incorrect sorting: want %v have %v", want, have)
|
||||
}
|
||||
|
||||
sort.Sort(ByParseOrder(c))
|
||||
want = []string{"BenchmarkSlower", "BenchmarkSameB", "BenchmarkSameA", "BenchmarkMuchFaster"}
|
||||
have = []string{c[0].Name(), c[1].Name(), c[2].Name(), c[3].Name()}
|
||||
if !reflect.DeepEqual(want, have) {
|
||||
t.Errorf("ByParseOrder incorrect sorting: want %v have %v", want, have)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright 2014 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.
|
||||
|
||||
/*
|
||||
Deprecated: benchcmp is deprecated in favor of benchstat: golang.org/x/perf/cmd/benchstat
|
||||
|
||||
The benchcmp command displays performance changes between benchmarks.
|
||||
|
||||
Benchcmp parses the output of two 'go test' benchmark runs,
|
||||
correlates the results per benchmark, and displays the deltas.
|
||||
|
||||
To measure the performance impact of a change, use 'go test'
|
||||
to run benchmarks before and after the change:
|
||||
|
||||
go test -run=NONE -bench=. ./... > old.txt
|
||||
# make changes
|
||||
go test -run=NONE -bench=. ./... > new.txt
|
||||
|
||||
Then feed the benchmark results to benchcmp:
|
||||
|
||||
benchcmp old.txt new.txt
|
||||
|
||||
Benchcmp will summarize and display the performance changes,
|
||||
in a format like this:
|
||||
|
||||
$ benchcmp old.txt new.txt
|
||||
benchmark old ns/op new ns/op delta
|
||||
BenchmarkConcat 523 68.6 -86.88%
|
||||
|
||||
benchmark old allocs new allocs delta
|
||||
BenchmarkConcat 3 1 -66.67%
|
||||
|
||||
benchmark old bytes new bytes delta
|
||||
BenchmarkConcat 80 48 -40.00%
|
||||
*/
|
||||
package main // import "golang.org/x/tools/cmd/benchcmp"
|
||||
@@ -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.
|
||||
|
||||
//go:build !go1.20
|
||||
|
||||
package main
|
||||
|
||||
import "os/exec"
|
||||
|
||||
func cmdInterrupt(cmd *exec.Cmd) {
|
||||
// cmd.Cancel and cmd.WaitDelay not available before Go 1.20.
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// 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.
|
||||
|
||||
//go:build go1.20
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
)
|
||||
|
||||
func cmdInterrupt(cmd *exec.Cmd) {
|
||||
cmd.Cancel = func() error {
|
||||
// On timeout, send interrupt,
|
||||
// in hopes of shutting down process tree.
|
||||
// Ignore errors sending signal; it's all best effort
|
||||
// and not even implemented on Windows.
|
||||
// TODO(rsc): Maybe use a new process group and kill the whole group?
|
||||
cmd.Process.Signal(os.Interrupt)
|
||||
return nil
|
||||
}
|
||||
cmd.WaitDelay = 2 * time.Second
|
||||
}
|
||||
@@ -0,0 +1,733 @@
|
||||
// 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.
|
||||
|
||||
// Bisect finds changes responsible for causing a failure.
|
||||
// A typical use is to identify the source locations in a program
|
||||
// that are miscompiled by a given compiler optimization.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// bisect [flags] [var=value...] command [arguments...]
|
||||
//
|
||||
// Bisect operates on a target command line – the target – that can be
|
||||
// run with various changes individually enabled or disabled. With none
|
||||
// of the changes enabled, the target is known to succeed (exit with exit
|
||||
// code zero). With all the changes enabled, the target is known to fail
|
||||
// (exit any other way). Bisect repeats the target with different sets of
|
||||
// changes enabled, using binary search to find (non-overlapping) minimal
|
||||
// change sets that provoke the failure.
|
||||
//
|
||||
// The target must cooperate with bisect by accepting a change pattern
|
||||
// and then enabling and reporting the changes that match that pattern.
|
||||
// The change pattern is passed to the target by substituting it anywhere
|
||||
// the string PATTERN appears in the environment values or the command
|
||||
// arguments. For each change that matches the pattern, the target must
|
||||
// enable that change and also print one or more “match lines”
|
||||
// (to standard output or standard error) describing the change.
|
||||
// The [golang.org/x/tools/internal/bisect] package provides functions to help
|
||||
// targets implement this protocol. We plan to publish that package
|
||||
// in a non-internal location after finalizing its API.
|
||||
//
|
||||
// Bisect starts by running the target with no changes enabled and then
|
||||
// with all changes enabled. It expects the former to succeed and the latter to fail,
|
||||
// and then it will search for the minimal set of changes that must be enabled
|
||||
// to provoke the failure. If the situation is reversed – the target fails with no
|
||||
// changes enabled and succeeds with all changes enabled – then bisect
|
||||
// automatically runs in reverse as well, searching for the minimal set of changes
|
||||
// that must be disabled to provoke the failure.
|
||||
//
|
||||
// Bisect prints tracing logs to standard error and the minimal change sets
|
||||
// to standard output.
|
||||
//
|
||||
// # Command Line Flags
|
||||
//
|
||||
// Bisect supports the following command-line flags:
|
||||
//
|
||||
// -max=M
|
||||
//
|
||||
// Stop after finding M minimal change sets. The default is no maximum, meaning to run until
|
||||
// all changes that provoke a failure have been identified.
|
||||
//
|
||||
// -maxset=S
|
||||
//
|
||||
// Disallow change sets larger than S elements. The default is no maximum.
|
||||
//
|
||||
// -timeout=D
|
||||
//
|
||||
// If the target runs for longer than duration D, stop the target and interpret that as a failure.
|
||||
// The default is no timeout.
|
||||
//
|
||||
// -count=N
|
||||
//
|
||||
// Run each trial N times (default 2), checking for consistency.
|
||||
//
|
||||
// -v
|
||||
//
|
||||
// Print verbose output, showing each run and its match lines.
|
||||
//
|
||||
// In addition to these general flags,
|
||||
// bisect supports a few “shortcut” flags that make it more convenient
|
||||
// to use with specific targets.
|
||||
//
|
||||
// -compile=<rewrite>
|
||||
//
|
||||
// This flag is equivalent to adding an environment variable
|
||||
// “GOCOMPILEDEBUG=<rewrite>hash=PATTERN”,
|
||||
// which, as discussed in more detail in the example below,
|
||||
// allows bisect to identify the specific source locations where the
|
||||
// compiler rewrite causes the target to fail.
|
||||
//
|
||||
// -godebug=<name>=<value>
|
||||
//
|
||||
// This flag is equivalent to adding an environment variable
|
||||
// “GODEBUG=<name>=<value>#PATTERN”,
|
||||
// which allows bisect to identify the specific call stacks where
|
||||
// the changed [GODEBUG setting] value causes the target to fail.
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// The Go compiler provides support for enabling or disabling certain rewrites
|
||||
// and optimizations to allow bisect to identify specific source locations where
|
||||
// the rewrite causes the program to fail. For example, to bisect a failure caused
|
||||
// by the new loop variable semantics:
|
||||
//
|
||||
// bisect go test -gcflags=all=-d=loopvarhash=PATTERN
|
||||
//
|
||||
// The -gcflags=all= instructs the go command to pass the -d=... to the Go compiler
|
||||
// when compiling all packages. Bisect varies PATTERN to determine the minimal set of changes
|
||||
// needed to reproduce the failure.
|
||||
//
|
||||
// The go command also checks the GOCOMPILEDEBUG environment variable for flags
|
||||
// to pass to the compiler, so the above command is equivalent to:
|
||||
//
|
||||
// bisect GOCOMPILEDEBUG=loopvarhash=PATTERN go test
|
||||
//
|
||||
// Finally, as mentioned earlier, the -compile flag allows shortening this command further:
|
||||
//
|
||||
// bisect -compile=loopvar go test
|
||||
//
|
||||
// # Defeating Build Caches
|
||||
//
|
||||
// Build systems cache build results, to avoid repeating the same compilations
|
||||
// over and over. When using a cached build result, the go command (correctly)
|
||||
// reprints the cached standard output and standard error associated with that
|
||||
// command invocation. (This makes commands like 'go build -gcflags=-S' for
|
||||
// printing an assembly listing work reliably.)
|
||||
//
|
||||
// Unfortunately, most build systems, including Bazel, are not as careful
|
||||
// as the go command about reprinting compiler output. If the compiler is
|
||||
// what prints match lines, a build system that suppresses compiler
|
||||
// output when using cached compiler results will confuse bisect.
|
||||
// To defeat such build caches, bisect replaces the literal text “RANDOM”
|
||||
// in environment values and command arguments with a random 64-bit value
|
||||
// during each invocation. The Go compiler conveniently accepts a
|
||||
// -d=ignore=... debug flag that ignores its argument, so to run the
|
||||
// previous example using Bazel, the invocation is:
|
||||
//
|
||||
// bazel test --define=gc_goopts=-d=loopvarhash=PATTERN,unused=RANDOM //path/to:test
|
||||
//
|
||||
// [GODEBUG setting]: https://tip.golang.org/doc/godebug
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math/bits"
|
||||
"math/rand"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/tools/internal/bisect"
|
||||
)
|
||||
|
||||
// Preserve import of bisect, to allow [bisect.Match] in the doc comment.
|
||||
var _ bisect.Matcher
|
||||
|
||||
func usage() {
|
||||
fmt.Fprintf(os.Stderr, "usage: bisect [flags] [var=value...] command [arguments...]\n")
|
||||
flag.PrintDefaults()
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
log.SetPrefix("bisect: ")
|
||||
|
||||
var b Bisect
|
||||
b.Stdout = os.Stdout
|
||||
b.Stderr = os.Stderr
|
||||
flag.IntVar(&b.Max, "max", 0, "stop after finding `m` failing change sets")
|
||||
flag.IntVar(&b.MaxSet, "maxset", 0, "do not search for change sets larger than `s` elements")
|
||||
flag.DurationVar(&b.Timeout, "timeout", 0, "stop target and consider failed after duration `d`")
|
||||
flag.IntVar(&b.Count, "count", 2, "run target `n` times for each trial")
|
||||
flag.BoolVar(&b.Verbose, "v", false, "enable verbose output")
|
||||
|
||||
env := ""
|
||||
envFlag := ""
|
||||
flag.Func("compile", "bisect source locations affected by Go compiler `rewrite` (fma, loopvar, ...)", func(value string) error {
|
||||
if envFlag != "" {
|
||||
return fmt.Errorf("cannot use -%s and -compile", envFlag)
|
||||
}
|
||||
envFlag = "compile"
|
||||
env = "GOCOMPILEDEBUG=" + value + "hash=PATTERN"
|
||||
return nil
|
||||
})
|
||||
flag.Func("godebug", "bisect call stacks affected by GODEBUG setting `name=value`", func(value string) error {
|
||||
if envFlag != "" {
|
||||
return fmt.Errorf("cannot use -%s and -godebug", envFlag)
|
||||
}
|
||||
envFlag = "godebug"
|
||||
env = "GODEBUG=" + value + "#PATTERN"
|
||||
return nil
|
||||
})
|
||||
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
args := flag.Args()
|
||||
|
||||
// Split command line into env settings, command name, args.
|
||||
i := 0
|
||||
for i < len(args) && strings.Contains(args[i], "=") {
|
||||
i++
|
||||
}
|
||||
if i == len(args) {
|
||||
usage()
|
||||
}
|
||||
b.Env, b.Cmd, b.Args = args[:i], args[i], args[i+1:]
|
||||
if env != "" {
|
||||
b.Env = append([]string{env}, b.Env...)
|
||||
}
|
||||
|
||||
// Check that PATTERN is available for us to vary.
|
||||
found := false
|
||||
for _, e := range b.Env {
|
||||
if _, v, _ := strings.Cut(e, "="); strings.Contains(v, "PATTERN") {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
for _, a := range b.Args {
|
||||
if strings.Contains(a, "PATTERN") {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
log.Fatalf("no PATTERN in target environment or args")
|
||||
}
|
||||
|
||||
if !b.Search() {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// A Bisect holds the state for a bisect invocation.
|
||||
type Bisect struct {
|
||||
// Env is the additional environment variables for the command.
|
||||
// PATTERN and RANDOM are substituted in the values, but not the names.
|
||||
Env []string
|
||||
|
||||
// Cmd is the command (program name) to run.
|
||||
// PATTERN and RANDOM are not substituted.
|
||||
Cmd string
|
||||
|
||||
// Args is the command arguments.
|
||||
// PATTERN and RANDOM are substituted anywhere they appear.
|
||||
Args []string
|
||||
|
||||
// Command-line flags controlling bisect behavior.
|
||||
Max int // maximum number of sets to report (0 = unlimited)
|
||||
MaxSet int // maximum number of elements in a set (0 = unlimited)
|
||||
Timeout time.Duration // kill target and assume failed after this duration (0 = unlimited)
|
||||
Count int // run target this many times for each trial and give up if flaky (min 1 assumed; default 2 on command line set in main)
|
||||
Verbose bool // print long output about each trial (only useful for debugging bisect itself)
|
||||
|
||||
// State for running bisect, replaced during testing.
|
||||
// Failing change sets are printed to Stdout; all other output goes to Stderr.
|
||||
Stdout io.Writer // where to write standard output (usually os.Stdout)
|
||||
Stderr io.Writer // where to write standard error (usually os.Stderr)
|
||||
TestRun func(env []string, cmd string, args []string) (out []byte, err error) // if non-nil, used instead of exec.Command
|
||||
|
||||
// State maintained by Search.
|
||||
|
||||
// By default, Search looks for a minimal set of changes that cause a failure when enabled.
|
||||
// If Disable is true, the search is inverted and seeks a minimal set of changes that
|
||||
// cause a failure when disabled. In this case, the search proceeds as normal except that
|
||||
// each pattern starts with a !.
|
||||
Disable bool
|
||||
|
||||
// SkipDigits is the number of hex digits to use in skip messages.
|
||||
// If the set of available changes is the same in each run, as it should be,
|
||||
// then this doesn't matter: we'll only exclude suffixes that uniquely identify
|
||||
// a given change. But for some programs, especially bisecting runtime
|
||||
// behaviors, sometimes enabling one change unlocks questions about other
|
||||
// changes. Strictly speaking this is a misuse of bisect, but just to make
|
||||
// bisect more robust, we use the y and n runs to create an estimate of the
|
||||
// number of bits needed for a unique suffix, and then we round it up to
|
||||
// a number of hex digits, with one extra digit for good measure, and then
|
||||
// we always use that many hex digits for skips.
|
||||
SkipHexDigits int
|
||||
|
||||
// Add is a list of suffixes to add to every trial, because they
|
||||
// contain changes that are necessary for a group we are assembling.
|
||||
Add []string
|
||||
|
||||
// Skip is a list of suffixes that uniquely identify changes to exclude from every trial,
|
||||
// because they have already been used in failing change sets.
|
||||
// Suffixes later in the list may only be unique after removing
|
||||
// the ones earlier in the list.
|
||||
// Skip applies after Add.
|
||||
Skip []string
|
||||
}
|
||||
|
||||
// A Result holds the result of a single target trial.
|
||||
type Result struct {
|
||||
Success bool // whether the target succeeded (exited with zero status)
|
||||
Cmd string // full target command line
|
||||
Out string // full target output (stdout and stderr combined)
|
||||
|
||||
Suffix string // the suffix used for collecting MatchIDs, MatchText, and MatchFull
|
||||
MatchIDs []uint64 // match IDs enabled during this trial
|
||||
MatchText []string // match reports for the IDs, with match markers removed
|
||||
MatchFull []string // full match lines for the IDs, with match markers kept
|
||||
}
|
||||
|
||||
// &searchFatal is a special panic value to signal that Search failed.
|
||||
// This lets us unwind the search recursion on a fatal error
|
||||
// but have Search return normally.
|
||||
var searchFatal int
|
||||
|
||||
// Search runs a bisect search according to the configuration in b.
|
||||
// It reports whether any failing change sets were found.
|
||||
func (b *Bisect) Search() bool {
|
||||
defer func() {
|
||||
// Recover from panic(&searchFatal), implicitly returning false from Search.
|
||||
// Re-panic on any other panic.
|
||||
if e := recover(); e != nil && e != &searchFatal {
|
||||
panic(e)
|
||||
}
|
||||
}()
|
||||
|
||||
// Run with no changes and all changes, to figure out which direction we're searching.
|
||||
// The goal is to find the minimal set of changes to toggle
|
||||
// starting with the state where everything works.
|
||||
// If "no changes" succeeds and "all changes" fails,
|
||||
// we're looking for a minimal set of changes to enable to provoke the failure
|
||||
// (broken = runY, b.Negate = false)
|
||||
// If "no changes" fails and "all changes" succeeds,
|
||||
// we're looking for a minimal set of changes to disable to provoke the failure
|
||||
// (broken = runN, b.Negate = true).
|
||||
|
||||
b.Logf("checking target with all changes disabled")
|
||||
runN := b.Run("n")
|
||||
|
||||
b.Logf("checking target with all changes enabled")
|
||||
runY := b.Run("y")
|
||||
|
||||
var broken *Result
|
||||
switch {
|
||||
case runN.Success && !runY.Success:
|
||||
b.Logf("target succeeds with no changes, fails with all changes")
|
||||
b.Logf("searching for minimal set of enabled changes causing failure")
|
||||
broken = runY
|
||||
b.Disable = false
|
||||
|
||||
case !runN.Success && runY.Success:
|
||||
b.Logf("target fails with no changes, succeeds with all changes")
|
||||
b.Logf("searching for minimal set of disabled changes causing failure")
|
||||
broken = runN
|
||||
b.Disable = true
|
||||
|
||||
case runN.Success && runY.Success:
|
||||
b.Fatalf("target succeeds with no changes and all changes")
|
||||
|
||||
case !runN.Success && !runY.Success:
|
||||
b.Fatalf("target fails with no changes and all changes")
|
||||
}
|
||||
|
||||
// Compute minimum number of bits needed to distinguish
|
||||
// all the changes we saw during N and all the changes we saw during Y.
|
||||
b.SkipHexDigits = skipHexDigits(runN.MatchIDs, runY.MatchIDs)
|
||||
|
||||
// Loop finding and printing change sets, until none remain.
|
||||
found := 0
|
||||
for {
|
||||
// Find set.
|
||||
bad := b.search(broken)
|
||||
if bad == nil {
|
||||
if found == 0 {
|
||||
b.Fatalf("cannot find any failing change sets of size ≤ %d", b.MaxSet)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// Confirm that set really does fail, to avoid false accusations.
|
||||
// Also asking for user-visible output; earlier runs did not.
|
||||
b.Logf("confirming failing change set")
|
||||
b.Add = append(b.Add[:0], bad...)
|
||||
broken = b.Run("v")
|
||||
if broken.Success {
|
||||
b.Logf("confirmation run succeeded unexpectedly")
|
||||
}
|
||||
b.Add = b.Add[:0]
|
||||
|
||||
// Print confirmed change set.
|
||||
found++
|
||||
b.Logf("FOUND failing change set")
|
||||
desc := "(enabling changes causes failure)"
|
||||
if b.Disable {
|
||||
desc = "(disabling changes causes failure)"
|
||||
}
|
||||
fmt.Fprintf(b.Stdout, "--- change set #%d %s\n%s\n---\n", found, desc, strings.Join(broken.MatchText, "\n"))
|
||||
|
||||
// Stop if we've found enough change sets.
|
||||
if b.Max > 0 && found >= b.Max {
|
||||
break
|
||||
}
|
||||
|
||||
// If running bisect target | tee bad.txt, prints to stdout and stderr
|
||||
// both appear on the terminal, but the ones to stdout go through tee
|
||||
// and can take a little bit of extra time. Sleep 1 millisecond to give
|
||||
// tee time to catch up, so that its stdout print does not get interlaced
|
||||
// with the stderr print from the next b.Log message.
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
|
||||
// Disable the now-known-bad changes and see if any failures remain.
|
||||
b.Logf("checking for more failures")
|
||||
b.Skip = append(bad, b.Skip...)
|
||||
broken = b.Run("")
|
||||
if broken.Success {
|
||||
what := "enabled"
|
||||
if b.Disable {
|
||||
what = "disabled"
|
||||
}
|
||||
b.Logf("target succeeds with all remaining changes %s", what)
|
||||
break
|
||||
}
|
||||
b.Logf("target still fails; searching for more bad changes")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Fatalf prints a message to standard error and then panics,
|
||||
// causing Search to return false.
|
||||
func (b *Bisect) Fatalf(format string, args ...any) {
|
||||
s := fmt.Sprintf("bisect: fatal error: "+format, args...)
|
||||
if !strings.HasSuffix(s, "\n") {
|
||||
s += "\n"
|
||||
}
|
||||
b.Stderr.Write([]byte(s))
|
||||
panic(&searchFatal)
|
||||
}
|
||||
|
||||
// Logf prints a message to standard error.
|
||||
func (b *Bisect) Logf(format string, args ...any) {
|
||||
s := fmt.Sprintf("bisect: "+format, args...)
|
||||
if !strings.HasSuffix(s, "\n") {
|
||||
s += "\n"
|
||||
}
|
||||
b.Stderr.Write([]byte(s))
|
||||
}
|
||||
|
||||
func skipHexDigits(idY, idN []uint64) int {
|
||||
var all []uint64
|
||||
seen := make(map[uint64]bool)
|
||||
for _, x := range idY {
|
||||
seen[x] = true
|
||||
all = append(all, x)
|
||||
}
|
||||
for _, x := range idN {
|
||||
if !seen[x] {
|
||||
seen[x] = true
|
||||
all = append(all, x)
|
||||
}
|
||||
}
|
||||
sort.Slice(all, func(i, j int) bool { return bits.Reverse64(all[i]) < bits.Reverse64(all[j]) })
|
||||
digits := sort.Search(64/4, func(digits int) bool {
|
||||
mask := uint64(1)<<(4*digits) - 1
|
||||
for i := 0; i+1 < len(all); i++ {
|
||||
if all[i]&mask == all[i+1]&mask {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
if digits < 64/4 {
|
||||
digits++
|
||||
}
|
||||
return digits
|
||||
}
|
||||
|
||||
// search searches for a single locally minimal change set.
|
||||
//
|
||||
// Invariant: r describes the result of r.Suffix + b.Add, which failed.
|
||||
// (There's an implicit -b.Skip everywhere here. b.Skip does not change.)
|
||||
// We want to extend r.Suffix to preserve the failure, working toward
|
||||
// a suffix that identifies a single change.
|
||||
func (b *Bisect) search(r *Result) []string {
|
||||
// The caller should be passing in a failure result that we diagnose.
|
||||
if r.Success {
|
||||
b.Fatalf("internal error: unexpected success") // mistake by caller
|
||||
}
|
||||
|
||||
// If the failure reported no changes, the target is misbehaving.
|
||||
if len(r.MatchIDs) == 0 {
|
||||
b.Fatalf("failure with no reported changes:\n\n$ %s\n%s\n", r.Cmd, r.Out)
|
||||
}
|
||||
|
||||
// If there's one matching change, that's the one we're looking for.
|
||||
if len(r.MatchIDs) == 1 {
|
||||
return []string{fmt.Sprintf("x%0*x", b.SkipHexDigits, r.MatchIDs[0]&(1<<(4*b.SkipHexDigits)-1))}
|
||||
}
|
||||
|
||||
// If the suffix we were tracking in the trial is already 64 bits,
|
||||
// either the target is bad or bisect itself is buggy.
|
||||
if len(r.Suffix) >= 64 {
|
||||
b.Fatalf("failed to isolate a single change with very long suffix")
|
||||
}
|
||||
|
||||
// We want to split the current matchIDs by left-extending the suffix with 0 and 1.
|
||||
// If all the matches have the same next bit, that won't cause a split, which doesn't
|
||||
// break the algorithm but does waste time. Avoid wasting time by left-extending
|
||||
// the suffix to the longest suffix shared by all the current match IDs
|
||||
// before adding 0 or 1.
|
||||
suffix := commonSuffix(r.MatchIDs)
|
||||
if !strings.HasSuffix(suffix, r.Suffix) {
|
||||
b.Fatalf("internal error: invalid common suffix") // bug in commonSuffix
|
||||
}
|
||||
|
||||
// Run 0suffix and 1suffix. If one fails, chase down the failure in that half.
|
||||
r0 := b.Run("0" + suffix)
|
||||
if !r0.Success {
|
||||
return b.search(r0)
|
||||
}
|
||||
r1 := b.Run("1" + suffix)
|
||||
if !r1.Success {
|
||||
return b.search(r1)
|
||||
}
|
||||
|
||||
// suffix failed, but 0suffix and 1suffix succeeded.
|
||||
// Assuming the target isn't flaky, this means we need
|
||||
// at least one change from 0suffix AND at least one from 1suffix.
|
||||
// We are already tracking N = len(b.Add) other changes and are
|
||||
// allowed to build sets of size at least 1+N (or we shouldn't be here at all).
|
||||
// If we aren't allowed to build sets of size 2+N, give up this branch.
|
||||
if b.MaxSet > 0 && 2+len(b.Add) > b.MaxSet {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Adding all matches for 1suffix, recurse to narrow down 0suffix.
|
||||
old := len(b.Add)
|
||||
b.Add = append(b.Add, "1"+suffix)
|
||||
r0 = b.Run("0" + suffix)
|
||||
if r0.Success {
|
||||
// 0suffix + b.Add + 1suffix = suffix + b.Add is what r describes, and it failed.
|
||||
b.Fatalf("target fails inconsistently")
|
||||
}
|
||||
bad0 := b.search(r0)
|
||||
if bad0 == nil {
|
||||
// Search failed due to MaxSet limit.
|
||||
return nil
|
||||
}
|
||||
b.Add = b.Add[:old]
|
||||
|
||||
// Adding the specific match we found in 0suffix, recurse to narrow down 1suffix.
|
||||
b.Add = append(b.Add[:old], bad0...)
|
||||
r1 = b.Run("1" + suffix)
|
||||
if r1.Success {
|
||||
// 1suffix + b.Add + bad0 = bad0 + b.Add + 1suffix is what b.search(r0) reported as a failure.
|
||||
b.Fatalf("target fails inconsistently")
|
||||
}
|
||||
bad1 := b.search(r1)
|
||||
if bad1 == nil {
|
||||
// Search failed due to MaxSet limit.
|
||||
return nil
|
||||
}
|
||||
b.Add = b.Add[:old]
|
||||
|
||||
// bad0 and bad1 together provoke the failure.
|
||||
return append(bad0, bad1...)
|
||||
}
|
||||
|
||||
// Run runs a set of trials selecting changes with the given suffix,
|
||||
// plus the ones in b.Add and not the ones in b.Skip.
|
||||
// The returned result's MatchIDs, MatchText, and MatchFull
|
||||
// only list the changes that match suffix.
|
||||
// When b.Count > 1, Run runs b.Count trials and requires
|
||||
// that they all succeed or they all fail. If not, it calls b.Fatalf.
|
||||
func (b *Bisect) Run(suffix string) *Result {
|
||||
out := b.run(suffix)
|
||||
for i := 1; i < b.Count; i++ {
|
||||
r := b.run(suffix)
|
||||
if r.Success != out.Success {
|
||||
b.Fatalf("target fails inconsistently")
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// run runs a single trial for Run.
|
||||
func (b *Bisect) run(suffix string) *Result {
|
||||
random := fmt.Sprint(rand.Uint64())
|
||||
|
||||
// Accept suffix == "v" to mean we need user-visible output.
|
||||
visible := ""
|
||||
if suffix == "v" {
|
||||
visible = "v"
|
||||
suffix = ""
|
||||
}
|
||||
|
||||
// Construct change ID pattern.
|
||||
var pattern string
|
||||
if suffix == "y" || suffix == "n" {
|
||||
pattern = suffix
|
||||
suffix = ""
|
||||
} else {
|
||||
var elem []string
|
||||
if suffix != "" {
|
||||
elem = append(elem, "+", suffix)
|
||||
}
|
||||
for _, x := range b.Add {
|
||||
elem = append(elem, "+", x)
|
||||
}
|
||||
for _, x := range b.Skip {
|
||||
elem = append(elem, "-", x)
|
||||
}
|
||||
pattern = strings.Join(elem, "")
|
||||
if pattern == "" {
|
||||
pattern = "y"
|
||||
}
|
||||
}
|
||||
if b.Disable {
|
||||
pattern = "!" + pattern
|
||||
}
|
||||
pattern = visible + pattern
|
||||
|
||||
// Construct substituted env and args.
|
||||
env := make([]string, len(b.Env))
|
||||
for i, x := range b.Env {
|
||||
k, v, _ := strings.Cut(x, "=")
|
||||
env[i] = k + "=" + replace(v, pattern, random)
|
||||
}
|
||||
args := make([]string, len(b.Args))
|
||||
for i, x := range b.Args {
|
||||
args[i] = replace(x, pattern, random)
|
||||
}
|
||||
|
||||
// Construct and log command line.
|
||||
// There is no newline in the log print.
|
||||
// The line will be completed when the command finishes.
|
||||
cmdText := strings.Join(append(append(env, b.Cmd), args...), " ")
|
||||
fmt.Fprintf(b.Stderr, "bisect: run: %s...", cmdText)
|
||||
|
||||
// Run command with args and env.
|
||||
var out []byte
|
||||
var err error
|
||||
if b.TestRun != nil {
|
||||
out, err = b.TestRun(env, b.Cmd, args)
|
||||
} else {
|
||||
ctx := context.Background()
|
||||
if b.Timeout != 0 {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeout(ctx, b.Timeout)
|
||||
defer cancel()
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, b.Cmd, args...)
|
||||
cmd.Env = append(os.Environ(), env...)
|
||||
// Set up cmd.Cancel, cmd.WaitDelay on Go 1.20 and later
|
||||
// TODO(rsc): Inline go120.go's cmdInterrupt once we stop supporting Go 1.19.
|
||||
cmdInterrupt(cmd)
|
||||
out, err = cmd.CombinedOutput()
|
||||
}
|
||||
|
||||
// Parse output to construct result.
|
||||
r := &Result{
|
||||
Suffix: suffix,
|
||||
Success: err == nil,
|
||||
Cmd: cmdText,
|
||||
Out: string(out),
|
||||
}
|
||||
|
||||
// Calculate bits, mask to identify suffix matches.
|
||||
var bits, mask uint64
|
||||
if suffix != "" && suffix != "y" && suffix != "n" && suffix != "v" {
|
||||
var err error
|
||||
bits, err = strconv.ParseUint(suffix, 2, 64)
|
||||
if err != nil {
|
||||
b.Fatalf("internal error: bad suffix")
|
||||
}
|
||||
mask = uint64(1<<len(suffix)) - 1
|
||||
}
|
||||
|
||||
// Process output, collecting match reports for suffix.
|
||||
have := make(map[uint64]bool)
|
||||
all := r.Out
|
||||
for all != "" {
|
||||
var line string
|
||||
line, all, _ = strings.Cut(all, "\n")
|
||||
short, id, ok := bisect.CutMarker(line)
|
||||
if !ok || (id&mask) != bits {
|
||||
continue
|
||||
}
|
||||
|
||||
if !have[id] {
|
||||
have[id] = true
|
||||
r.MatchIDs = append(r.MatchIDs, id)
|
||||
}
|
||||
r.MatchText = append(r.MatchText, short)
|
||||
r.MatchFull = append(r.MatchFull, line)
|
||||
}
|
||||
|
||||
// Finish log print from above, describing the command's completion.
|
||||
if err == nil {
|
||||
fmt.Fprintf(b.Stderr, " ok (%d matches)\n", len(r.MatchIDs))
|
||||
} else {
|
||||
fmt.Fprintf(b.Stderr, " FAIL (%d matches)\n", len(r.MatchIDs))
|
||||
}
|
||||
|
||||
if err != nil && len(r.MatchIDs) == 0 {
|
||||
b.Fatalf("target failed without printing any matches\n%s", r.Out)
|
||||
}
|
||||
|
||||
// In verbose mode, print extra debugging: all the lines with match markers.
|
||||
if b.Verbose {
|
||||
b.Logf("matches:\n%s", strings.Join(r.MatchFull, "\n\t"))
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// replace returns x with literal text PATTERN and RANDOM replaced by pattern and random.
|
||||
func replace(x, pattern, random string) string {
|
||||
x = strings.ReplaceAll(x, "PATTERN", pattern)
|
||||
x = strings.ReplaceAll(x, "RANDOM", random)
|
||||
return x
|
||||
}
|
||||
|
||||
// commonSuffix returns the longest common binary suffix shared by all uint64s in list.
|
||||
// If list is empty, commonSuffix returns an empty string.
|
||||
func commonSuffix(list []uint64) string {
|
||||
if len(list) == 0 {
|
||||
return ""
|
||||
}
|
||||
b := list[0]
|
||||
n := 64
|
||||
for _, x := range list {
|
||||
for x&((1<<n)-1) != b {
|
||||
n--
|
||||
b &= (1 << n) - 1
|
||||
}
|
||||
}
|
||||
s := make([]byte, n)
|
||||
for i := n - 1; i >= 0; i-- {
|
||||
s[i] = '0' + byte(b&1)
|
||||
b >>= 1
|
||||
}
|
||||
return string(s[:])
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
// 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 main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/build/constraint"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/internal/bisect"
|
||||
"golang.org/x/tools/internal/diffp"
|
||||
"golang.org/x/tools/txtar"
|
||||
)
|
||||
|
||||
var update = flag.Bool("update", false, "update testdata with new stdout/stderr")
|
||||
|
||||
func Test(t *testing.T) {
|
||||
files, err := filepath.Glob("testdata/*.txt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, file := range files {
|
||||
t.Run(strings.TrimSuffix(filepath.Base(file), ".txt"), func(t *testing.T) {
|
||||
data, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a := txtar.Parse(data)
|
||||
var wantStdout, wantStderr []byte
|
||||
files := a.Files
|
||||
if len(files) > 0 && files[0].Name == "stdout" {
|
||||
wantStdout = files[0].Data
|
||||
files = files[1:]
|
||||
}
|
||||
if len(files) > 0 && files[0].Name == "stderr" {
|
||||
wantStderr = files[0].Data
|
||||
files = files[1:]
|
||||
}
|
||||
if len(files) > 0 {
|
||||
t.Fatalf("unexpected txtar entry: %s", files[0].Name)
|
||||
}
|
||||
|
||||
var tt struct {
|
||||
Fail string
|
||||
Bisect Bisect
|
||||
}
|
||||
if err := json.Unmarshal(a.Comment, &tt); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expr, err := constraint.Parse("//go:build " + tt.Fail)
|
||||
if err != nil {
|
||||
t.Fatalf("invalid Cmd: %v", err)
|
||||
}
|
||||
|
||||
rnd := rand.New(rand.NewSource(1))
|
||||
b := &tt.Bisect
|
||||
b.Cmd = "test"
|
||||
b.Args = []string{"PATTERN"}
|
||||
var stdout, stderr bytes.Buffer
|
||||
b.Stdout = &stdout
|
||||
b.Stderr = &stderr
|
||||
b.TestRun = func(env []string, cmd string, args []string) (out []byte, err error) {
|
||||
pattern := args[0]
|
||||
m, err := bisect.New(pattern)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
have := make(map[string]bool)
|
||||
for i, color := range colors {
|
||||
if m.ShouldEnable(uint64(i)) {
|
||||
have[color] = true
|
||||
}
|
||||
if m.ShouldReport(uint64(i)) {
|
||||
out = fmt.Appendf(out, "%s %s\n", color, bisect.Marker(uint64(i)))
|
||||
}
|
||||
}
|
||||
err = nil
|
||||
if eval(rnd, expr, have) {
|
||||
err = fmt.Errorf("failed")
|
||||
}
|
||||
return out, err
|
||||
}
|
||||
|
||||
if !b.Search() {
|
||||
stderr.WriteString("<bisect failed>\n")
|
||||
}
|
||||
rewrite := false
|
||||
if !bytes.Equal(stdout.Bytes(), wantStdout) {
|
||||
if *update {
|
||||
rewrite = true
|
||||
} else {
|
||||
t.Errorf("incorrect stdout: %s", diffp.Diff("have", stdout.Bytes(), "want", wantStdout))
|
||||
}
|
||||
}
|
||||
if !bytes.Equal(stderr.Bytes(), wantStderr) {
|
||||
if *update {
|
||||
rewrite = true
|
||||
} else {
|
||||
t.Errorf("incorrect stderr: %s", diffp.Diff("have", stderr.Bytes(), "want", wantStderr))
|
||||
}
|
||||
}
|
||||
if rewrite {
|
||||
a.Files = []txtar.File{{Name: "stdout", Data: stdout.Bytes()}, {Name: "stderr", Data: stderr.Bytes()}}
|
||||
err := os.WriteFile(file, txtar.Format(a), 0666)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("updated %s", file)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func eval(rnd *rand.Rand, z constraint.Expr, have map[string]bool) bool {
|
||||
switch z := z.(type) {
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected type %T", z))
|
||||
case *constraint.NotExpr:
|
||||
return !eval(rnd, z.X, have)
|
||||
case *constraint.AndExpr:
|
||||
return eval(rnd, z.X, have) && eval(rnd, z.Y, have)
|
||||
case *constraint.OrExpr:
|
||||
return eval(rnd, z.X, have) || eval(rnd, z.Y, have)
|
||||
case *constraint.TagExpr:
|
||||
if z.Tag == "random" {
|
||||
return rnd.Intn(2) == 1
|
||||
}
|
||||
return have[z.Tag]
|
||||
}
|
||||
}
|
||||
|
||||
var colors = strings.Fields(`
|
||||
aliceblue
|
||||
amaranth
|
||||
amber
|
||||
amethyst
|
||||
applegreen
|
||||
applered
|
||||
apricot
|
||||
aquamarine
|
||||
azure
|
||||
babyblue
|
||||
beige
|
||||
brickred
|
||||
black
|
||||
blue
|
||||
bluegreen
|
||||
blueviolet
|
||||
blush
|
||||
bronze
|
||||
brown
|
||||
burgundy
|
||||
byzantium
|
||||
carmine
|
||||
cerise
|
||||
cerulean
|
||||
champagne
|
||||
chartreusegreen
|
||||
chocolate
|
||||
cobaltblue
|
||||
coffee
|
||||
copper
|
||||
coral
|
||||
crimson
|
||||
cyan
|
||||
desertsand
|
||||
electricblue
|
||||
emerald
|
||||
erin
|
||||
gold
|
||||
gray
|
||||
green
|
||||
harlequin
|
||||
indigo
|
||||
ivory
|
||||
jade
|
||||
junglegreen
|
||||
lavender
|
||||
lemon
|
||||
lilac
|
||||
lime
|
||||
magenta
|
||||
magentarose
|
||||
maroon
|
||||
mauve
|
||||
navyblue
|
||||
ochre
|
||||
olive
|
||||
orange
|
||||
orangered
|
||||
orchid
|
||||
peach
|
||||
pear
|
||||
periwinkle
|
||||
persianblue
|
||||
pink
|
||||
plum
|
||||
prussianblue
|
||||
puce
|
||||
purple
|
||||
raspberry
|
||||
red
|
||||
redviolet
|
||||
rose
|
||||
ruby
|
||||
salmon
|
||||
sangria
|
||||
sapphire
|
||||
scarlet
|
||||
silver
|
||||
slategray
|
||||
springbud
|
||||
springgreen
|
||||
tan
|
||||
taupe
|
||||
teal
|
||||
turquoise
|
||||
ultramarine
|
||||
violet
|
||||
viridian
|
||||
white
|
||||
yellow
|
||||
`)
|
||||
@@ -0,0 +1,20 @@
|
||||
// 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.
|
||||
|
||||
// Starting in Go 1.20, the global rand is auto-seeded,
|
||||
// with a better value than the current Unix nanoseconds.
|
||||
// Only seed if we're using older versions of Go.
|
||||
|
||||
//go:build !go1.20
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
This directory contains test inputs for the bisect command.
|
||||
|
||||
Each text file is a txtar archive (see <https://pkg.go.dev/golang.org/x/tools/txtar>
|
||||
or `go doc txtar`).
|
||||
|
||||
The comment at the top of the archive is a JSON object describing a
|
||||
target behavior. Specifically, the Fail key gives a boolean expression
|
||||
that should provoke a failure. Bisect's job is to discover this
|
||||
condition.
|
||||
|
||||
The Bisect key describes settings in the Bisect struct that we want to
|
||||
change, to simulate the use of various command-line options.
|
||||
|
||||
The txtar archive files should be "stdout" and "stderr", giving the
|
||||
expected standard output and standard error. If the bisect command
|
||||
should exit with a non-zero status, the stderr in the archive will end
|
||||
with the line "<bisect failed>".
|
||||
|
||||
Running `go test -update` will rewrite the stdout and stderr files in
|
||||
each testdata archive to match the current state of the tool. This is
|
||||
a useful command when the logging prints from bisect change or when
|
||||
writing a new test.
|
||||
|
||||
To use `go test -update` to write a new test:
|
||||
|
||||
- Create a new .txt file with just a JSON object at the top,
|
||||
specifying what you want to test.
|
||||
- Run `go test -update`.
|
||||
- Reload the .txt file and read the stdout and stderr to see if you agree.
|
||||
@@ -0,0 +1,44 @@
|
||||
{"Fail": "amber || apricot"}
|
||||
-- stdout --
|
||||
--- change set #1 (enabling changes causes failure)
|
||||
amber
|
||||
---
|
||||
--- change set #2 (enabling changes causes failure)
|
||||
apricot
|
||||
---
|
||||
-- stderr --
|
||||
bisect: checking target with all changes disabled
|
||||
bisect: run: test n... ok (90 matches)
|
||||
bisect: checking target with all changes enabled
|
||||
bisect: run: test y... FAIL (90 matches)
|
||||
bisect: target succeeds with no changes, fails with all changes
|
||||
bisect: searching for minimal set of enabled changes causing failure
|
||||
bisect: run: test +0... FAIL (45 matches)
|
||||
bisect: run: test +00... ok (23 matches)
|
||||
bisect: run: test +10... FAIL (22 matches)
|
||||
bisect: run: test +010... FAIL (11 matches)
|
||||
bisect: run: test +0010... FAIL (6 matches)
|
||||
bisect: run: test +00010... FAIL (3 matches)
|
||||
bisect: run: test +000010... FAIL (2 matches)
|
||||
bisect: run: test +0000010... FAIL (1 matches)
|
||||
bisect: confirming failing change set
|
||||
bisect: run: test v+x002... FAIL (1 matches)
|
||||
bisect: FOUND failing change set
|
||||
bisect: checking for more failures
|
||||
bisect: run: test -x002... FAIL (89 matches)
|
||||
bisect: target still fails; searching for more bad changes
|
||||
bisect: run: test +0-x002... FAIL (44 matches)
|
||||
bisect: run: test +00-x002... ok (23 matches)
|
||||
bisect: run: test +10-x002... FAIL (21 matches)
|
||||
bisect: run: test +010-x002... ok (10 matches)
|
||||
bisect: run: test +110-x002... FAIL (11 matches)
|
||||
bisect: run: test +0110-x002... FAIL (6 matches)
|
||||
bisect: run: test +00110-x002... FAIL (3 matches)
|
||||
bisect: run: test +000110-x002... FAIL (2 matches)
|
||||
bisect: run: test +0000110-x002... FAIL (1 matches)
|
||||
bisect: confirming failing change set
|
||||
bisect: run: test v+x006-x002... FAIL (1 matches)
|
||||
bisect: FOUND failing change set
|
||||
bisect: checking for more failures
|
||||
bisect: run: test -x006-x002... ok (88 matches)
|
||||
bisect: target succeeds with all remaining changes enabled
|
||||
@@ -0,0 +1,67 @@
|
||||
{"Fail": "amber || apricot", "Bisect": {"Count": 2}}
|
||||
-- stdout --
|
||||
--- change set #1 (enabling changes causes failure)
|
||||
amber
|
||||
---
|
||||
--- change set #2 (enabling changes causes failure)
|
||||
apricot
|
||||
---
|
||||
-- stderr --
|
||||
bisect: checking target with all changes disabled
|
||||
bisect: run: test n... ok (90 matches)
|
||||
bisect: run: test n... ok (90 matches)
|
||||
bisect: checking target with all changes enabled
|
||||
bisect: run: test y... FAIL (90 matches)
|
||||
bisect: run: test y... FAIL (90 matches)
|
||||
bisect: target succeeds with no changes, fails with all changes
|
||||
bisect: searching for minimal set of enabled changes causing failure
|
||||
bisect: run: test +0... FAIL (45 matches)
|
||||
bisect: run: test +0... FAIL (45 matches)
|
||||
bisect: run: test +00... ok (23 matches)
|
||||
bisect: run: test +00... ok (23 matches)
|
||||
bisect: run: test +10... FAIL (22 matches)
|
||||
bisect: run: test +10... FAIL (22 matches)
|
||||
bisect: run: test +010... FAIL (11 matches)
|
||||
bisect: run: test +010... FAIL (11 matches)
|
||||
bisect: run: test +0010... FAIL (6 matches)
|
||||
bisect: run: test +0010... FAIL (6 matches)
|
||||
bisect: run: test +00010... FAIL (3 matches)
|
||||
bisect: run: test +00010... FAIL (3 matches)
|
||||
bisect: run: test +000010... FAIL (2 matches)
|
||||
bisect: run: test +000010... FAIL (2 matches)
|
||||
bisect: run: test +0000010... FAIL (1 matches)
|
||||
bisect: run: test +0000010... FAIL (1 matches)
|
||||
bisect: confirming failing change set
|
||||
bisect: run: test v+x002... FAIL (1 matches)
|
||||
bisect: run: test v+x002... FAIL (1 matches)
|
||||
bisect: FOUND failing change set
|
||||
bisect: checking for more failures
|
||||
bisect: run: test -x002... FAIL (89 matches)
|
||||
bisect: run: test -x002... FAIL (89 matches)
|
||||
bisect: target still fails; searching for more bad changes
|
||||
bisect: run: test +0-x002... FAIL (44 matches)
|
||||
bisect: run: test +0-x002... FAIL (44 matches)
|
||||
bisect: run: test +00-x002... ok (23 matches)
|
||||
bisect: run: test +00-x002... ok (23 matches)
|
||||
bisect: run: test +10-x002... FAIL (21 matches)
|
||||
bisect: run: test +10-x002... FAIL (21 matches)
|
||||
bisect: run: test +010-x002... ok (10 matches)
|
||||
bisect: run: test +010-x002... ok (10 matches)
|
||||
bisect: run: test +110-x002... FAIL (11 matches)
|
||||
bisect: run: test +110-x002... FAIL (11 matches)
|
||||
bisect: run: test +0110-x002... FAIL (6 matches)
|
||||
bisect: run: test +0110-x002... FAIL (6 matches)
|
||||
bisect: run: test +00110-x002... FAIL (3 matches)
|
||||
bisect: run: test +00110-x002... FAIL (3 matches)
|
||||
bisect: run: test +000110-x002... FAIL (2 matches)
|
||||
bisect: run: test +000110-x002... FAIL (2 matches)
|
||||
bisect: run: test +0000110-x002... FAIL (1 matches)
|
||||
bisect: run: test +0000110-x002... FAIL (1 matches)
|
||||
bisect: confirming failing change set
|
||||
bisect: run: test v+x006-x002... FAIL (1 matches)
|
||||
bisect: run: test v+x006-x002... FAIL (1 matches)
|
||||
bisect: FOUND failing change set
|
||||
bisect: checking for more failures
|
||||
bisect: run: test -x006-x002... ok (88 matches)
|
||||
bisect: run: test -x006-x002... ok (88 matches)
|
||||
bisect: target succeeds with all remaining changes enabled
|
||||
@@ -0,0 +1,57 @@
|
||||
{"Fail": "amber || apricot && peach"}
|
||||
-- stdout --
|
||||
--- change set #1 (enabling changes causes failure)
|
||||
amber
|
||||
---
|
||||
--- change set #2 (enabling changes causes failure)
|
||||
apricot
|
||||
peach
|
||||
---
|
||||
-- stderr --
|
||||
bisect: checking target with all changes disabled
|
||||
bisect: run: test n... ok (90 matches)
|
||||
bisect: checking target with all changes enabled
|
||||
bisect: run: test y... FAIL (90 matches)
|
||||
bisect: target succeeds with no changes, fails with all changes
|
||||
bisect: searching for minimal set of enabled changes causing failure
|
||||
bisect: run: test +0... FAIL (45 matches)
|
||||
bisect: run: test +00... ok (23 matches)
|
||||
bisect: run: test +10... FAIL (22 matches)
|
||||
bisect: run: test +010... FAIL (11 matches)
|
||||
bisect: run: test +0010... FAIL (6 matches)
|
||||
bisect: run: test +00010... FAIL (3 matches)
|
||||
bisect: run: test +000010... FAIL (2 matches)
|
||||
bisect: run: test +0000010... FAIL (1 matches)
|
||||
bisect: confirming failing change set
|
||||
bisect: run: test v+x002... FAIL (1 matches)
|
||||
bisect: FOUND failing change set
|
||||
bisect: checking for more failures
|
||||
bisect: run: test -x002... FAIL (89 matches)
|
||||
bisect: target still fails; searching for more bad changes
|
||||
bisect: run: test +0-x002... ok (44 matches)
|
||||
bisect: run: test +1-x002... ok (45 matches)
|
||||
bisect: run: test +0+1-x002... FAIL (44 matches)
|
||||
bisect: run: test +00+1-x002... ok (23 matches)
|
||||
bisect: run: test +10+1-x002... FAIL (21 matches)
|
||||
bisect: run: test +010+1-x002... ok (10 matches)
|
||||
bisect: run: test +110+1-x002... FAIL (11 matches)
|
||||
bisect: run: test +0110+1-x002... FAIL (6 matches)
|
||||
bisect: run: test +00110+1-x002... FAIL (3 matches)
|
||||
bisect: run: test +000110+1-x002... FAIL (2 matches)
|
||||
bisect: run: test +0000110+1-x002... FAIL (1 matches)
|
||||
bisect: run: test +1+x006-x002... FAIL (45 matches)
|
||||
bisect: run: test +01+x006-x002... ok (23 matches)
|
||||
bisect: run: test +11+x006-x002... FAIL (22 matches)
|
||||
bisect: run: test +011+x006-x002... FAIL (11 matches)
|
||||
bisect: run: test +0011+x006-x002... ok (6 matches)
|
||||
bisect: run: test +1011+x006-x002... FAIL (5 matches)
|
||||
bisect: run: test +01011+x006-x002... ok (3 matches)
|
||||
bisect: run: test +11011+x006-x002... FAIL (2 matches)
|
||||
bisect: run: test +011011+x006-x002... ok (1 matches)
|
||||
bisect: run: test +111011+x006-x002... FAIL (1 matches)
|
||||
bisect: confirming failing change set
|
||||
bisect: run: test v+x006+x03b-x002... FAIL (2 matches)
|
||||
bisect: FOUND failing change set
|
||||
bisect: checking for more failures
|
||||
bisect: run: test -x006-x03b-x002... ok (87 matches)
|
||||
bisect: target succeeds with all remaining changes enabled
|
||||
@@ -0,0 +1,23 @@
|
||||
{"Fail": "amber || apricot && peach", "Bisect": {"Max": 1}}
|
||||
-- stdout --
|
||||
--- change set #1 (enabling changes causes failure)
|
||||
amber
|
||||
---
|
||||
-- stderr --
|
||||
bisect: checking target with all changes disabled
|
||||
bisect: run: test n... ok (90 matches)
|
||||
bisect: checking target with all changes enabled
|
||||
bisect: run: test y... FAIL (90 matches)
|
||||
bisect: target succeeds with no changes, fails with all changes
|
||||
bisect: searching for minimal set of enabled changes causing failure
|
||||
bisect: run: test +0... FAIL (45 matches)
|
||||
bisect: run: test +00... ok (23 matches)
|
||||
bisect: run: test +10... FAIL (22 matches)
|
||||
bisect: run: test +010... FAIL (11 matches)
|
||||
bisect: run: test +0010... FAIL (6 matches)
|
||||
bisect: run: test +00010... FAIL (3 matches)
|
||||
bisect: run: test +000010... FAIL (2 matches)
|
||||
bisect: run: test +0000010... FAIL (1 matches)
|
||||
bisect: confirming failing change set
|
||||
bisect: run: test v+x002... FAIL (1 matches)
|
||||
bisect: FOUND failing change set
|
||||
@@ -0,0 +1,59 @@
|
||||
{"Fail": "amber || apricot && peach || red && green && blue || cyan && magenta && yellow && black", "Bisect": {"Max": 2}}
|
||||
-- stdout --
|
||||
--- change set #1 (enabling changes causes failure)
|
||||
amber
|
||||
---
|
||||
--- change set #2 (enabling changes causes failure)
|
||||
blue
|
||||
green
|
||||
red
|
||||
---
|
||||
-- stderr --
|
||||
bisect: checking target with all changes disabled
|
||||
bisect: run: test n... ok (90 matches)
|
||||
bisect: checking target with all changes enabled
|
||||
bisect: run: test y... FAIL (90 matches)
|
||||
bisect: target succeeds with no changes, fails with all changes
|
||||
bisect: searching for minimal set of enabled changes causing failure
|
||||
bisect: run: test +0... FAIL (45 matches)
|
||||
bisect: run: test +00... ok (23 matches)
|
||||
bisect: run: test +10... FAIL (22 matches)
|
||||
bisect: run: test +010... FAIL (11 matches)
|
||||
bisect: run: test +0010... FAIL (6 matches)
|
||||
bisect: run: test +00010... FAIL (3 matches)
|
||||
bisect: run: test +000010... FAIL (2 matches)
|
||||
bisect: run: test +0000010... FAIL (1 matches)
|
||||
bisect: confirming failing change set
|
||||
bisect: run: test v+x002... FAIL (1 matches)
|
||||
bisect: FOUND failing change set
|
||||
bisect: checking for more failures
|
||||
bisect: run: test -x002... FAIL (89 matches)
|
||||
bisect: target still fails; searching for more bad changes
|
||||
bisect: run: test +0-x002... ok (44 matches)
|
||||
bisect: run: test +1-x002... FAIL (45 matches)
|
||||
bisect: run: test +01-x002... ok (23 matches)
|
||||
bisect: run: test +11-x002... ok (22 matches)
|
||||
bisect: run: test +01+11-x002... FAIL (23 matches)
|
||||
bisect: run: test +001+11-x002... ok (12 matches)
|
||||
bisect: run: test +101+11-x002... FAIL (11 matches)
|
||||
bisect: run: test +0101+11-x002... ok (6 matches)
|
||||
bisect: run: test +1101+11-x002... ok (5 matches)
|
||||
bisect: run: test +0101+11+1101-x002... FAIL (6 matches)
|
||||
bisect: run: test +00101+11+1101-x002... FAIL (3 matches)
|
||||
bisect: run: test +000101+11+1101-x002... FAIL (2 matches)
|
||||
bisect: run: test +0000101+11+1101-x002... ok (1 matches)
|
||||
bisect: run: test +1000101+11+1101-x002... FAIL (1 matches)
|
||||
bisect: run: test +1101+11+x045-x002... FAIL (5 matches)
|
||||
bisect: run: test +01101+11+x045-x002... FAIL (3 matches)
|
||||
bisect: run: test +001101+11+x045-x002... FAIL (2 matches)
|
||||
bisect: run: test +0001101+11+x045-x002... FAIL (1 matches)
|
||||
bisect: run: test +11+x045+x00d-x002... FAIL (22 matches)
|
||||
bisect: run: test +011+x045+x00d-x002... ok (11 matches)
|
||||
bisect: run: test +111+x045+x00d-x002... FAIL (11 matches)
|
||||
bisect: run: test +0111+x045+x00d-x002... FAIL (6 matches)
|
||||
bisect: run: test +00111+x045+x00d-x002... FAIL (3 matches)
|
||||
bisect: run: test +000111+x045+x00d-x002... ok (2 matches)
|
||||
bisect: run: test +100111+x045+x00d-x002... FAIL (1 matches)
|
||||
bisect: confirming failing change set
|
||||
bisect: run: test v+x045+x00d+x027-x002... FAIL (3 matches)
|
||||
bisect: FOUND failing change set
|
||||
@@ -0,0 +1,84 @@
|
||||
{"Fail": "amber || apricot && peach || red && green && blue || cyan && magenta && yellow && black", "Bisect": {"MaxSet": 3}}
|
||||
-- stdout --
|
||||
--- change set #1 (enabling changes causes failure)
|
||||
amber
|
||||
---
|
||||
--- change set #2 (enabling changes causes failure)
|
||||
blue
|
||||
green
|
||||
red
|
||||
---
|
||||
-- stderr --
|
||||
bisect: checking target with all changes disabled
|
||||
bisect: run: test n... ok (90 matches)
|
||||
bisect: checking target with all changes enabled
|
||||
bisect: run: test y... FAIL (90 matches)
|
||||
bisect: target succeeds with no changes, fails with all changes
|
||||
bisect: searching for minimal set of enabled changes causing failure
|
||||
bisect: run: test +0... FAIL (45 matches)
|
||||
bisect: run: test +00... ok (23 matches)
|
||||
bisect: run: test +10... FAIL (22 matches)
|
||||
bisect: run: test +010... FAIL (11 matches)
|
||||
bisect: run: test +0010... FAIL (6 matches)
|
||||
bisect: run: test +00010... FAIL (3 matches)
|
||||
bisect: run: test +000010... FAIL (2 matches)
|
||||
bisect: run: test +0000010... FAIL (1 matches)
|
||||
bisect: confirming failing change set
|
||||
bisect: run: test v+x002... FAIL (1 matches)
|
||||
bisect: FOUND failing change set
|
||||
bisect: checking for more failures
|
||||
bisect: run: test -x002... FAIL (89 matches)
|
||||
bisect: target still fails; searching for more bad changes
|
||||
bisect: run: test +0-x002... ok (44 matches)
|
||||
bisect: run: test +1-x002... FAIL (45 matches)
|
||||
bisect: run: test +01-x002... ok (23 matches)
|
||||
bisect: run: test +11-x002... ok (22 matches)
|
||||
bisect: run: test +01+11-x002... FAIL (23 matches)
|
||||
bisect: run: test +001+11-x002... ok (12 matches)
|
||||
bisect: run: test +101+11-x002... FAIL (11 matches)
|
||||
bisect: run: test +0101+11-x002... ok (6 matches)
|
||||
bisect: run: test +1101+11-x002... ok (5 matches)
|
||||
bisect: run: test +0101+11+1101-x002... FAIL (6 matches)
|
||||
bisect: run: test +00101+11+1101-x002... FAIL (3 matches)
|
||||
bisect: run: test +000101+11+1101-x002... FAIL (2 matches)
|
||||
bisect: run: test +0000101+11+1101-x002... ok (1 matches)
|
||||
bisect: run: test +1000101+11+1101-x002... FAIL (1 matches)
|
||||
bisect: run: test +1101+11+x045-x002... FAIL (5 matches)
|
||||
bisect: run: test +01101+11+x045-x002... FAIL (3 matches)
|
||||
bisect: run: test +001101+11+x045-x002... FAIL (2 matches)
|
||||
bisect: run: test +0001101+11+x045-x002... FAIL (1 matches)
|
||||
bisect: run: test +11+x045+x00d-x002... FAIL (22 matches)
|
||||
bisect: run: test +011+x045+x00d-x002... ok (11 matches)
|
||||
bisect: run: test +111+x045+x00d-x002... FAIL (11 matches)
|
||||
bisect: run: test +0111+x045+x00d-x002... FAIL (6 matches)
|
||||
bisect: run: test +00111+x045+x00d-x002... FAIL (3 matches)
|
||||
bisect: run: test +000111+x045+x00d-x002... ok (2 matches)
|
||||
bisect: run: test +100111+x045+x00d-x002... FAIL (1 matches)
|
||||
bisect: confirming failing change set
|
||||
bisect: run: test v+x045+x00d+x027-x002... FAIL (3 matches)
|
||||
bisect: FOUND failing change set
|
||||
bisect: checking for more failures
|
||||
bisect: run: test -x045-x00d-x027-x002... FAIL (86 matches)
|
||||
bisect: target still fails; searching for more bad changes
|
||||
bisect: run: test +0-x045-x00d-x027-x002... ok (44 matches)
|
||||
bisect: run: test +1-x045-x00d-x027-x002... ok (42 matches)
|
||||
bisect: run: test +0+1-x045-x00d-x027-x002... FAIL (44 matches)
|
||||
bisect: run: test +00+1-x045-x00d-x027-x002... FAIL (23 matches)
|
||||
bisect: run: test +000+1-x045-x00d-x027-x002... ok (12 matches)
|
||||
bisect: run: test +100+1-x045-x00d-x027-x002... ok (11 matches)
|
||||
bisect: run: test +000+1+100-x045-x00d-x027-x002... FAIL (12 matches)
|
||||
bisect: run: test +0000+1+100-x045-x00d-x027-x002... FAIL (6 matches)
|
||||
bisect: run: test +00000+1+100-x045-x00d-x027-x002... FAIL (3 matches)
|
||||
bisect: run: test +000000+1+100-x045-x00d-x027-x002... ok (2 matches)
|
||||
bisect: run: test +100000+1+100-x045-x00d-x027-x002... FAIL (1 matches)
|
||||
bisect: run: test +100+1+x020-x045-x00d-x027-x002... FAIL (11 matches)
|
||||
bisect: run: test +0100+1+x020-x045-x00d-x027-x002... ok (6 matches)
|
||||
bisect: run: test +1100+1+x020-x045-x00d-x027-x002... FAIL (5 matches)
|
||||
bisect: run: test +01100+1+x020-x045-x00d-x027-x002... FAIL (3 matches)
|
||||
bisect: run: test +001100+1+x020-x045-x00d-x027-x002... FAIL (2 matches)
|
||||
bisect: run: test +0001100+1+x020-x045-x00d-x027-x002... FAIL (1 matches)
|
||||
bisect: run: test +1+x020+x00c-x045-x00d-x027-x002... FAIL (42 matches)
|
||||
bisect: run: test +01+x020+x00c-x045-x00d-x027-x002... FAIL (21 matches)
|
||||
bisect: run: test +001+x020+x00c-x045-x00d-x027-x002... FAIL (12 matches)
|
||||
bisect: run: test +0001+x020+x00c-x045-x00d-x027-x002... ok (6 matches)
|
||||
bisect: run: test +1001+x020+x00c-x045-x00d-x027-x002... ok (6 matches)
|
||||
@@ -0,0 +1,13 @@
|
||||
{"Fail": "apricot && peach", "Bisect": {"MaxSet": 1}}
|
||||
-- stdout --
|
||||
-- stderr --
|
||||
bisect: checking target with all changes disabled
|
||||
bisect: run: test n... ok (90 matches)
|
||||
bisect: checking target with all changes enabled
|
||||
bisect: run: test y... FAIL (90 matches)
|
||||
bisect: target succeeds with no changes, fails with all changes
|
||||
bisect: searching for minimal set of enabled changes causing failure
|
||||
bisect: run: test +0... ok (45 matches)
|
||||
bisect: run: test +1... ok (45 matches)
|
||||
bisect: fatal error: cannot find any failing change sets of size ≤ 1
|
||||
<bisect failed>
|
||||
@@ -0,0 +1,138 @@
|
||||
{"Fail": "amber || apricot && peach || red && green && blue || cyan && magenta && yellow && black", "Bisect": {"MaxSet": 4}}
|
||||
-- stdout --
|
||||
--- change set #1 (enabling changes causes failure)
|
||||
amber
|
||||
---
|
||||
--- change set #2 (enabling changes causes failure)
|
||||
blue
|
||||
green
|
||||
red
|
||||
---
|
||||
--- change set #3 (enabling changes causes failure)
|
||||
black
|
||||
cyan
|
||||
magenta
|
||||
yellow
|
||||
---
|
||||
--- change set #4 (enabling changes causes failure)
|
||||
apricot
|
||||
peach
|
||||
---
|
||||
-- stderr --
|
||||
bisect: checking target with all changes disabled
|
||||
bisect: run: test n... ok (90 matches)
|
||||
bisect: checking target with all changes enabled
|
||||
bisect: run: test y... FAIL (90 matches)
|
||||
bisect: target succeeds with no changes, fails with all changes
|
||||
bisect: searching for minimal set of enabled changes causing failure
|
||||
bisect: run: test +0... FAIL (45 matches)
|
||||
bisect: run: test +00... ok (23 matches)
|
||||
bisect: run: test +10... FAIL (22 matches)
|
||||
bisect: run: test +010... FAIL (11 matches)
|
||||
bisect: run: test +0010... FAIL (6 matches)
|
||||
bisect: run: test +00010... FAIL (3 matches)
|
||||
bisect: run: test +000010... FAIL (2 matches)
|
||||
bisect: run: test +0000010... FAIL (1 matches)
|
||||
bisect: confirming failing change set
|
||||
bisect: run: test v+x002... FAIL (1 matches)
|
||||
bisect: FOUND failing change set
|
||||
bisect: checking for more failures
|
||||
bisect: run: test -x002... FAIL (89 matches)
|
||||
bisect: target still fails; searching for more bad changes
|
||||
bisect: run: test +0-x002... ok (44 matches)
|
||||
bisect: run: test +1-x002... FAIL (45 matches)
|
||||
bisect: run: test +01-x002... ok (23 matches)
|
||||
bisect: run: test +11-x002... ok (22 matches)
|
||||
bisect: run: test +01+11-x002... FAIL (23 matches)
|
||||
bisect: run: test +001+11-x002... ok (12 matches)
|
||||
bisect: run: test +101+11-x002... FAIL (11 matches)
|
||||
bisect: run: test +0101+11-x002... ok (6 matches)
|
||||
bisect: run: test +1101+11-x002... ok (5 matches)
|
||||
bisect: run: test +0101+11+1101-x002... FAIL (6 matches)
|
||||
bisect: run: test +00101+11+1101-x002... FAIL (3 matches)
|
||||
bisect: run: test +000101+11+1101-x002... FAIL (2 matches)
|
||||
bisect: run: test +0000101+11+1101-x002... ok (1 matches)
|
||||
bisect: run: test +1000101+11+1101-x002... FAIL (1 matches)
|
||||
bisect: run: test +1101+11+x045-x002... FAIL (5 matches)
|
||||
bisect: run: test +01101+11+x045-x002... FAIL (3 matches)
|
||||
bisect: run: test +001101+11+x045-x002... FAIL (2 matches)
|
||||
bisect: run: test +0001101+11+x045-x002... FAIL (1 matches)
|
||||
bisect: run: test +11+x045+x00d-x002... FAIL (22 matches)
|
||||
bisect: run: test +011+x045+x00d-x002... ok (11 matches)
|
||||
bisect: run: test +111+x045+x00d-x002... FAIL (11 matches)
|
||||
bisect: run: test +0111+x045+x00d-x002... FAIL (6 matches)
|
||||
bisect: run: test +00111+x045+x00d-x002... FAIL (3 matches)
|
||||
bisect: run: test +000111+x045+x00d-x002... ok (2 matches)
|
||||
bisect: run: test +100111+x045+x00d-x002... FAIL (1 matches)
|
||||
bisect: confirming failing change set
|
||||
bisect: run: test v+x045+x00d+x027-x002... FAIL (3 matches)
|
||||
bisect: FOUND failing change set
|
||||
bisect: checking for more failures
|
||||
bisect: run: test -x045-x00d-x027-x002... FAIL (86 matches)
|
||||
bisect: target still fails; searching for more bad changes
|
||||
bisect: run: test +0-x045-x00d-x027-x002... ok (44 matches)
|
||||
bisect: run: test +1-x045-x00d-x027-x002... ok (42 matches)
|
||||
bisect: run: test +0+1-x045-x00d-x027-x002... FAIL (44 matches)
|
||||
bisect: run: test +00+1-x045-x00d-x027-x002... FAIL (23 matches)
|
||||
bisect: run: test +000+1-x045-x00d-x027-x002... ok (12 matches)
|
||||
bisect: run: test +100+1-x045-x00d-x027-x002... ok (11 matches)
|
||||
bisect: run: test +000+1+100-x045-x00d-x027-x002... FAIL (12 matches)
|
||||
bisect: run: test +0000+1+100-x045-x00d-x027-x002... FAIL (6 matches)
|
||||
bisect: run: test +00000+1+100-x045-x00d-x027-x002... FAIL (3 matches)
|
||||
bisect: run: test +000000+1+100-x045-x00d-x027-x002... ok (2 matches)
|
||||
bisect: run: test +100000+1+100-x045-x00d-x027-x002... FAIL (1 matches)
|
||||
bisect: run: test +100+1+x020-x045-x00d-x027-x002... FAIL (11 matches)
|
||||
bisect: run: test +0100+1+x020-x045-x00d-x027-x002... ok (6 matches)
|
||||
bisect: run: test +1100+1+x020-x045-x00d-x027-x002... FAIL (5 matches)
|
||||
bisect: run: test +01100+1+x020-x045-x00d-x027-x002... FAIL (3 matches)
|
||||
bisect: run: test +001100+1+x020-x045-x00d-x027-x002... FAIL (2 matches)
|
||||
bisect: run: test +0001100+1+x020-x045-x00d-x027-x002... FAIL (1 matches)
|
||||
bisect: run: test +1+x020+x00c-x045-x00d-x027-x002... FAIL (42 matches)
|
||||
bisect: run: test +01+x020+x00c-x045-x00d-x027-x002... FAIL (21 matches)
|
||||
bisect: run: test +001+x020+x00c-x045-x00d-x027-x002... FAIL (12 matches)
|
||||
bisect: run: test +0001+x020+x00c-x045-x00d-x027-x002... ok (6 matches)
|
||||
bisect: run: test +1001+x020+x00c-x045-x00d-x027-x002... ok (6 matches)
|
||||
bisect: run: test +0001+x020+x00c+1001-x045-x00d-x027-x002... FAIL (6 matches)
|
||||
bisect: run: test +00001+x020+x00c+1001-x045-x00d-x027-x002... ok (3 matches)
|
||||
bisect: run: test +10001+x020+x00c+1001-x045-x00d-x027-x002... FAIL (3 matches)
|
||||
bisect: run: test +010001+x020+x00c+1001-x045-x00d-x027-x002... ok (2 matches)
|
||||
bisect: run: test +110001+x020+x00c+1001-x045-x00d-x027-x002... FAIL (1 matches)
|
||||
bisect: run: test +1001+x020+x00c+x031-x045-x00d-x027-x002... FAIL (6 matches)
|
||||
bisect: run: test +01001+x020+x00c+x031-x045-x00d-x027-x002... ok (3 matches)
|
||||
bisect: run: test +11001+x020+x00c+x031-x045-x00d-x027-x002... FAIL (3 matches)
|
||||
bisect: run: test +011001+x020+x00c+x031-x045-x00d-x027-x002... FAIL (2 matches)
|
||||
bisect: run: test +0011001+x020+x00c+x031-x045-x00d-x027-x002... ok (1 matches)
|
||||
bisect: run: test +1011001+x020+x00c+x031-x045-x00d-x027-x002... FAIL (1 matches)
|
||||
bisect: confirming failing change set
|
||||
bisect: run: test v+x020+x00c+x031+x059-x045-x00d-x027-x002... FAIL (4 matches)
|
||||
bisect: FOUND failing change set
|
||||
bisect: checking for more failures
|
||||
bisect: run: test -x020-x00c-x031-x059-x045-x00d-x027-x002... FAIL (82 matches)
|
||||
bisect: target still fails; searching for more bad changes
|
||||
bisect: run: test +0-x020-x00c-x031-x059-x045-x00d-x027-x002... ok (42 matches)
|
||||
bisect: run: test +1-x020-x00c-x031-x059-x045-x00d-x027-x002... ok (40 matches)
|
||||
bisect: run: test +0+1-x020-x00c-x031-x059-x045-x00d-x027-x002... FAIL (42 matches)
|
||||
bisect: run: test +00+1-x020-x00c-x031-x059-x045-x00d-x027-x002... ok (21 matches)
|
||||
bisect: run: test +10+1-x020-x00c-x031-x059-x045-x00d-x027-x002... FAIL (21 matches)
|
||||
bisect: run: test +010+1-x020-x00c-x031-x059-x045-x00d-x027-x002... ok (10 matches)
|
||||
bisect: run: test +110+1-x020-x00c-x031-x059-x045-x00d-x027-x002... FAIL (11 matches)
|
||||
bisect: run: test +0110+1-x020-x00c-x031-x059-x045-x00d-x027-x002... FAIL (6 matches)
|
||||
bisect: run: test +00110+1-x020-x00c-x031-x059-x045-x00d-x027-x002... FAIL (3 matches)
|
||||
bisect: run: test +000110+1-x020-x00c-x031-x059-x045-x00d-x027-x002... FAIL (2 matches)
|
||||
bisect: run: test +0000110+1-x020-x00c-x031-x059-x045-x00d-x027-x002... FAIL (1 matches)
|
||||
bisect: run: test +1+x006-x020-x00c-x031-x059-x045-x00d-x027-x002... FAIL (40 matches)
|
||||
bisect: run: test +01+x006-x020-x00c-x031-x059-x045-x00d-x027-x002... ok (19 matches)
|
||||
bisect: run: test +11+x006-x020-x00c-x031-x059-x045-x00d-x027-x002... FAIL (21 matches)
|
||||
bisect: run: test +011+x006-x020-x00c-x031-x059-x045-x00d-x027-x002... FAIL (11 matches)
|
||||
bisect: run: test +0011+x006-x020-x00c-x031-x059-x045-x00d-x027-x002... ok (6 matches)
|
||||
bisect: run: test +1011+x006-x020-x00c-x031-x059-x045-x00d-x027-x002... FAIL (5 matches)
|
||||
bisect: run: test +01011+x006-x020-x00c-x031-x059-x045-x00d-x027-x002... ok (3 matches)
|
||||
bisect: run: test +11011+x006-x020-x00c-x031-x059-x045-x00d-x027-x002... FAIL (2 matches)
|
||||
bisect: run: test +011011+x006-x020-x00c-x031-x059-x045-x00d-x027-x002... ok (1 matches)
|
||||
bisect: run: test +111011+x006-x020-x00c-x031-x059-x045-x00d-x027-x002... FAIL (1 matches)
|
||||
bisect: confirming failing change set
|
||||
bisect: run: test v+x006+x03b-x020-x00c-x031-x059-x045-x00d-x027-x002... FAIL (2 matches)
|
||||
bisect: FOUND failing change set
|
||||
bisect: checking for more failures
|
||||
bisect: run: test -x006-x03b-x020-x00c-x031-x059-x045-x00d-x027-x002... ok (80 matches)
|
||||
bisect: target succeeds with all remaining changes enabled
|
||||
@@ -0,0 +1,57 @@
|
||||
{"Fail": "!amber || !apricot && !peach"}
|
||||
-- stdout --
|
||||
--- change set #1 (disabling changes causes failure)
|
||||
amber
|
||||
---
|
||||
--- change set #2 (disabling changes causes failure)
|
||||
apricot
|
||||
peach
|
||||
---
|
||||
-- stderr --
|
||||
bisect: checking target with all changes disabled
|
||||
bisect: run: test n... FAIL (90 matches)
|
||||
bisect: checking target with all changes enabled
|
||||
bisect: run: test y... ok (90 matches)
|
||||
bisect: target fails with no changes, succeeds with all changes
|
||||
bisect: searching for minimal set of disabled changes causing failure
|
||||
bisect: run: test !+0... FAIL (45 matches)
|
||||
bisect: run: test !+00... ok (23 matches)
|
||||
bisect: run: test !+10... FAIL (22 matches)
|
||||
bisect: run: test !+010... FAIL (11 matches)
|
||||
bisect: run: test !+0010... FAIL (6 matches)
|
||||
bisect: run: test !+00010... FAIL (3 matches)
|
||||
bisect: run: test !+000010... FAIL (2 matches)
|
||||
bisect: run: test !+0000010... FAIL (1 matches)
|
||||
bisect: confirming failing change set
|
||||
bisect: run: test v!+x002... FAIL (1 matches)
|
||||
bisect: FOUND failing change set
|
||||
bisect: checking for more failures
|
||||
bisect: run: test !-x002... FAIL (89 matches)
|
||||
bisect: target still fails; searching for more bad changes
|
||||
bisect: run: test !+0-x002... ok (44 matches)
|
||||
bisect: run: test !+1-x002... ok (45 matches)
|
||||
bisect: run: test !+0+1-x002... FAIL (44 matches)
|
||||
bisect: run: test !+00+1-x002... ok (23 matches)
|
||||
bisect: run: test !+10+1-x002... FAIL (21 matches)
|
||||
bisect: run: test !+010+1-x002... ok (10 matches)
|
||||
bisect: run: test !+110+1-x002... FAIL (11 matches)
|
||||
bisect: run: test !+0110+1-x002... FAIL (6 matches)
|
||||
bisect: run: test !+00110+1-x002... FAIL (3 matches)
|
||||
bisect: run: test !+000110+1-x002... FAIL (2 matches)
|
||||
bisect: run: test !+0000110+1-x002... FAIL (1 matches)
|
||||
bisect: run: test !+1+x006-x002... FAIL (45 matches)
|
||||
bisect: run: test !+01+x006-x002... ok (23 matches)
|
||||
bisect: run: test !+11+x006-x002... FAIL (22 matches)
|
||||
bisect: run: test !+011+x006-x002... FAIL (11 matches)
|
||||
bisect: run: test !+0011+x006-x002... ok (6 matches)
|
||||
bisect: run: test !+1011+x006-x002... FAIL (5 matches)
|
||||
bisect: run: test !+01011+x006-x002... ok (3 matches)
|
||||
bisect: run: test !+11011+x006-x002... FAIL (2 matches)
|
||||
bisect: run: test !+011011+x006-x002... ok (1 matches)
|
||||
bisect: run: test !+111011+x006-x002... FAIL (1 matches)
|
||||
bisect: confirming failing change set
|
||||
bisect: run: test v!+x006+x03b-x002... FAIL (2 matches)
|
||||
bisect: FOUND failing change set
|
||||
bisect: checking for more failures
|
||||
bisect: run: test !-x006-x03b-x002... ok (87 matches)
|
||||
bisect: target succeeds with all remaining changes disabled
|
||||
@@ -0,0 +1,59 @@
|
||||
{"Fail": "amber || apricot || blue && random"}
|
||||
-- stdout --
|
||||
--- change set #1 (enabling changes causes failure)
|
||||
amber
|
||||
---
|
||||
--- change set #2 (enabling changes causes failure)
|
||||
apricot
|
||||
---
|
||||
-- stderr --
|
||||
bisect: checking target with all changes disabled
|
||||
bisect: run: test n... ok (90 matches)
|
||||
bisect: checking target with all changes enabled
|
||||
bisect: run: test y... FAIL (90 matches)
|
||||
bisect: target succeeds with no changes, fails with all changes
|
||||
bisect: searching for minimal set of enabled changes causing failure
|
||||
bisect: run: test +0... FAIL (45 matches)
|
||||
bisect: run: test +00... ok (23 matches)
|
||||
bisect: run: test +10... FAIL (22 matches)
|
||||
bisect: run: test +010... FAIL (11 matches)
|
||||
bisect: run: test +0010... FAIL (6 matches)
|
||||
bisect: run: test +00010... FAIL (3 matches)
|
||||
bisect: run: test +000010... FAIL (2 matches)
|
||||
bisect: run: test +0000010... FAIL (1 matches)
|
||||
bisect: confirming failing change set
|
||||
bisect: run: test v+x002... FAIL (1 matches)
|
||||
bisect: FOUND failing change set
|
||||
bisect: checking for more failures
|
||||
bisect: run: test -x002... FAIL (89 matches)
|
||||
bisect: target still fails; searching for more bad changes
|
||||
bisect: run: test +0-x002... FAIL (44 matches)
|
||||
bisect: run: test +00-x002... ok (23 matches)
|
||||
bisect: run: test +10-x002... FAIL (21 matches)
|
||||
bisect: run: test +010-x002... ok (10 matches)
|
||||
bisect: run: test +110-x002... FAIL (11 matches)
|
||||
bisect: run: test +0110-x002... FAIL (6 matches)
|
||||
bisect: run: test +00110-x002... FAIL (3 matches)
|
||||
bisect: run: test +000110-x002... FAIL (2 matches)
|
||||
bisect: run: test +0000110-x002... FAIL (1 matches)
|
||||
bisect: confirming failing change set
|
||||
bisect: run: test v+x006-x002... FAIL (1 matches)
|
||||
bisect: FOUND failing change set
|
||||
bisect: checking for more failures
|
||||
bisect: run: test -x006-x002... FAIL (88 matches)
|
||||
bisect: target still fails; searching for more bad changes
|
||||
bisect: run: test +0-x006-x002... ok (43 matches)
|
||||
bisect: run: test +1-x006-x002... FAIL (45 matches)
|
||||
bisect: run: test +01-x006-x002... FAIL (23 matches)
|
||||
bisect: run: test +001-x006-x002... ok (12 matches)
|
||||
bisect: run: test +101-x006-x002... FAIL (11 matches)
|
||||
bisect: run: test +0101-x006-x002... ok (6 matches)
|
||||
bisect: run: test +1101-x006-x002... FAIL (5 matches)
|
||||
bisect: run: test +01101-x006-x002... ok (3 matches)
|
||||
bisect: run: test +11101-x006-x002... ok (2 matches)
|
||||
bisect: run: test +01101+11101-x006-x002... FAIL (3 matches)
|
||||
bisect: run: test +001101+11101-x006-x002... ok (2 matches)
|
||||
bisect: run: test +101101+11101-x006-x002... ok (1 matches)
|
||||
bisect: run: test +001101+11101+101101-x006-x002... ok (2 matches)
|
||||
bisect: fatal error: target fails inconsistently
|
||||
<bisect failed>
|
||||
@@ -0,0 +1,24 @@
|
||||
{"Fail": "blue && random"}
|
||||
-- stdout --
|
||||
-- stderr --
|
||||
bisect: checking target with all changes disabled
|
||||
bisect: run: test n... ok (90 matches)
|
||||
bisect: checking target with all changes enabled
|
||||
bisect: run: test y... FAIL (90 matches)
|
||||
bisect: target succeeds with no changes, fails with all changes
|
||||
bisect: searching for minimal set of enabled changes causing failure
|
||||
bisect: run: test +0... ok (45 matches)
|
||||
bisect: run: test +1... FAIL (45 matches)
|
||||
bisect: run: test +01... FAIL (23 matches)
|
||||
bisect: run: test +001... ok (12 matches)
|
||||
bisect: run: test +101... FAIL (11 matches)
|
||||
bisect: run: test +0101... ok (6 matches)
|
||||
bisect: run: test +1101... FAIL (5 matches)
|
||||
bisect: run: test +01101... ok (3 matches)
|
||||
bisect: run: test +11101... ok (2 matches)
|
||||
bisect: run: test +01101+11101... FAIL (3 matches)
|
||||
bisect: run: test +001101+11101... ok (2 matches)
|
||||
bisect: run: test +101101+11101... ok (1 matches)
|
||||
bisect: run: test +001101+11101+101101... ok (2 matches)
|
||||
bisect: fatal error: target fails inconsistently
|
||||
<bisect failed>
|
||||
@@ -0,0 +1,19 @@
|
||||
{"Fail": "blue && random", "Bisect": {"Count": 2}}
|
||||
-- stdout --
|
||||
-- stderr --
|
||||
bisect: checking target with all changes disabled
|
||||
bisect: run: test n... ok (90 matches)
|
||||
bisect: run: test n... ok (90 matches)
|
||||
bisect: checking target with all changes enabled
|
||||
bisect: run: test y... FAIL (90 matches)
|
||||
bisect: run: test y... FAIL (90 matches)
|
||||
bisect: target succeeds with no changes, fails with all changes
|
||||
bisect: searching for minimal set of enabled changes causing failure
|
||||
bisect: run: test +0... ok (45 matches)
|
||||
bisect: run: test +0... ok (45 matches)
|
||||
bisect: run: test +1... FAIL (45 matches)
|
||||
bisect: run: test +1... FAIL (45 matches)
|
||||
bisect: run: test +01... FAIL (23 matches)
|
||||
bisect: run: test +01... ok (23 matches)
|
||||
bisect: fatal error: target fails inconsistently
|
||||
<bisect failed>
|
||||
@@ -0,0 +1 @@
|
||||
testdata/out.got
|
||||
@@ -0,0 +1,485 @@
|
||||
// Copyright 2015 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.
|
||||
|
||||
// Bundle creates a single-source-file version of a source package
|
||||
// suitable for inclusion in a particular target package.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// bundle [-o file] [-dst path] [-pkg name] [-prefix p] [-import old=new] [-tags build_constraints] <src>
|
||||
//
|
||||
// The src argument specifies the import path of the package to bundle.
|
||||
// The bundling of a directory of source files into a single source file
|
||||
// necessarily imposes a number of constraints.
|
||||
// The package being bundled must not use cgo; must not use conditional
|
||||
// file compilation, whether with build tags or system-specific file names
|
||||
// like code_amd64.go; must not depend on any special comments, which
|
||||
// may not be preserved; must not use any assembly sources;
|
||||
// must not use renaming imports; and must not use reflection-based APIs
|
||||
// that depend on the specific names of types or struct fields.
|
||||
//
|
||||
// By default, bundle writes the bundled code to standard output.
|
||||
// If the -o argument is given, bundle writes to the named file
|
||||
// and also includes a “//go:generate” comment giving the exact
|
||||
// command line used, for regenerating the file with “go generate.”
|
||||
//
|
||||
// Bundle customizes its output for inclusion in a particular package, the destination package.
|
||||
// By default bundle assumes the destination is the package in the current directory,
|
||||
// but the destination package can be specified explicitly using the -dst option,
|
||||
// which takes an import path as its argument.
|
||||
// If the source package imports the destination package, bundle will remove
|
||||
// those imports and rewrite any references to use direct references to the
|
||||
// corresponding symbols.
|
||||
// Bundle also must write a package declaration in the output and must
|
||||
// choose a name to use in that declaration.
|
||||
// If the -pkg option is given, bundle uses that name.
|
||||
// Otherwise, the name of the destination package is used.
|
||||
// Build constraints for the generated file can be specified using the -tags option.
|
||||
//
|
||||
// To avoid collisions, bundle inserts a prefix at the beginning of
|
||||
// every package-level const, func, type, and var identifier in src's code,
|
||||
// updating references accordingly. The default prefix is the package name
|
||||
// of the source package followed by an underscore. The -prefix option
|
||||
// specifies an alternate prefix.
|
||||
//
|
||||
// Occasionally it is necessary to rewrite imports during the bundling
|
||||
// process. The -import option, which may be repeated, specifies that
|
||||
// an import of "old" should be rewritten to import "new" instead.
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// Bundle archive/zip for inclusion in cmd/dist:
|
||||
//
|
||||
// cd $GOROOT/src/cmd/dist
|
||||
// bundle -o zip.go archive/zip
|
||||
//
|
||||
// Bundle golang.org/x/net/http2 for inclusion in net/http,
|
||||
// prefixing all identifiers by "http2" instead of "http2_", and
|
||||
// including a "!nethttpomithttp2" build constraint:
|
||||
//
|
||||
// cd $GOROOT/src/net/http
|
||||
// bundle -o h2_bundle.go -prefix http2 -tags '!nethttpomithttp2' golang.org/x/net/http2
|
||||
//
|
||||
// Update the http2 bundle in net/http:
|
||||
//
|
||||
// go generate net/http
|
||||
//
|
||||
// Update all bundles in the standard library:
|
||||
//
|
||||
// go generate -run bundle std
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/format"
|
||||
"go/printer"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"golang.org/x/tools/go/packages"
|
||||
)
|
||||
|
||||
var (
|
||||
outputFile = flag.String("o", "", "write output to `file` (default standard output)")
|
||||
dstPath = flag.String("dst", ".", "set destination import `path`")
|
||||
pkgName = flag.String("pkg", "", "set destination package `name`")
|
||||
prefix = flag.String("prefix", "&_", "set bundled identifier prefix to `p` (default is \"&_\", where & stands for the original name)")
|
||||
buildTags = flag.String("tags", "", "the build constraints to be inserted into the generated file")
|
||||
|
||||
importMap = map[string]string{}
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.Var(flagFunc(addImportMap), "import", "rewrite import using `map`, of form old=new (can be repeated)")
|
||||
}
|
||||
|
||||
func addImportMap(s string) {
|
||||
if strings.Count(s, "=") != 1 {
|
||||
log.Fatal("-import argument must be of the form old=new")
|
||||
}
|
||||
i := strings.Index(s, "=")
|
||||
old, new := s[:i], s[i+1:]
|
||||
if old == "" || new == "" {
|
||||
log.Fatal("-import argument must be of the form old=new; old and new must be non-empty")
|
||||
}
|
||||
importMap[old] = new
|
||||
}
|
||||
|
||||
func usage() {
|
||||
fmt.Fprintf(os.Stderr, "Usage: bundle [options] <src>\n")
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.SetPrefix("bundle: ")
|
||||
log.SetFlags(0)
|
||||
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
args := flag.Args()
|
||||
if len(args) != 1 {
|
||||
usage()
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
cfg := &packages.Config{Mode: packages.NeedName}
|
||||
pkgs, err := packages.Load(cfg, *dstPath)
|
||||
if err != nil {
|
||||
log.Fatalf("cannot load destination package: %v", err)
|
||||
}
|
||||
if packages.PrintErrors(pkgs) > 0 || len(pkgs) != 1 {
|
||||
log.Fatalf("failed to load destination package")
|
||||
}
|
||||
if *pkgName == "" {
|
||||
*pkgName = pkgs[0].Name
|
||||
}
|
||||
|
||||
code, err := bundle(args[0], pkgs[0].PkgPath, *pkgName, *prefix, *buildTags)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if *outputFile != "" {
|
||||
err := os.WriteFile(*outputFile, code, 0666)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
} else {
|
||||
_, err := os.Stdout.Write(code)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// isStandardImportPath is copied from cmd/go in the standard library.
|
||||
func isStandardImportPath(path string) bool {
|
||||
i := strings.Index(path, "/")
|
||||
if i < 0 {
|
||||
i = len(path)
|
||||
}
|
||||
elem := path[:i]
|
||||
return !strings.Contains(elem, ".")
|
||||
}
|
||||
|
||||
var testingOnlyPackagesConfig *packages.Config
|
||||
|
||||
func bundle(src, dst, dstpkg, prefix, buildTags string) ([]byte, error) {
|
||||
// Load the initial package.
|
||||
cfg := &packages.Config{}
|
||||
if testingOnlyPackagesConfig != nil {
|
||||
*cfg = *testingOnlyPackagesConfig
|
||||
} else {
|
||||
// Bypass default vendor mode, as we need a package not available in the
|
||||
// std module vendor folder.
|
||||
cfg.Env = append(os.Environ(), "GOFLAGS=-mod=mod")
|
||||
}
|
||||
cfg.Mode = packages.NeedTypes | packages.NeedSyntax | packages.NeedTypesInfo
|
||||
pkgs, err := packages.Load(cfg, src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if packages.PrintErrors(pkgs) > 0 || len(pkgs) != 1 {
|
||||
return nil, fmt.Errorf("failed to load source package")
|
||||
}
|
||||
pkg := pkgs[0]
|
||||
|
||||
if strings.Contains(prefix, "&") {
|
||||
prefix = strings.Replace(prefix, "&", pkg.Syntax[0].Name.Name, -1)
|
||||
}
|
||||
|
||||
objsToUpdate := make(map[types.Object]bool)
|
||||
var rename func(from types.Object)
|
||||
rename = func(from types.Object) {
|
||||
if !objsToUpdate[from] {
|
||||
objsToUpdate[from] = true
|
||||
|
||||
// Renaming a type that is used as an embedded field
|
||||
// requires renaming the field too. e.g.
|
||||
// type T int // if we rename this to U..
|
||||
// var s struct {T}
|
||||
// print(s.T) // ...this must change too
|
||||
if _, ok := from.(*types.TypeName); ok {
|
||||
for id, obj := range pkg.TypesInfo.Uses {
|
||||
if obj == from {
|
||||
if field := pkg.TypesInfo.Defs[id]; field != nil {
|
||||
rename(field)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rename each package-level object.
|
||||
scope := pkg.Types.Scope()
|
||||
for _, name := range scope.Names() {
|
||||
rename(scope.Lookup(name))
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
if buildTags != "" {
|
||||
fmt.Fprintf(&out, "//go:build %s\n", buildTags)
|
||||
}
|
||||
|
||||
fmt.Fprintf(&out, "// Code generated by golang.org/x/tools/cmd/bundle. DO NOT EDIT.\n")
|
||||
if *outputFile != "" && buildTags == "" {
|
||||
fmt.Fprintf(&out, "//go:generate bundle %s\n", strings.Join(quoteArgs(os.Args[1:]), " "))
|
||||
} else {
|
||||
fmt.Fprintf(&out, "// $ bundle %s\n", strings.Join(os.Args[1:], " "))
|
||||
}
|
||||
fmt.Fprintf(&out, "\n")
|
||||
|
||||
// Concatenate package comments from all files...
|
||||
for _, f := range pkg.Syntax {
|
||||
if doc := f.Doc.Text(); strings.TrimSpace(doc) != "" {
|
||||
for _, line := range strings.Split(doc, "\n") {
|
||||
fmt.Fprintf(&out, "// %s\n", line)
|
||||
}
|
||||
}
|
||||
}
|
||||
// ...but don't let them become the actual package comment.
|
||||
fmt.Fprintln(&out)
|
||||
|
||||
fmt.Fprintf(&out, "package %s\n\n", dstpkg)
|
||||
|
||||
// BUG(adonovan,shurcooL): bundle may generate incorrect code
|
||||
// due to shadowing between identifiers and imported package names.
|
||||
//
|
||||
// The generated code will either fail to compile or
|
||||
// (unlikely) compile successfully but have different behavior
|
||||
// than the original package. The risk of this happening is higher
|
||||
// when the original package has renamed imports (they're typically
|
||||
// renamed in order to resolve a shadow inside that particular .go file).
|
||||
|
||||
// TODO(adonovan,shurcooL):
|
||||
// - detect shadowing issues, and either return error or resolve them
|
||||
// - preserve comments from the original import declarations.
|
||||
|
||||
// pkgStd and pkgExt are sets of printed import specs. This is done
|
||||
// to deduplicate instances of the same import name and path.
|
||||
var pkgStd = make(map[string]bool)
|
||||
var pkgExt = make(map[string]bool)
|
||||
for _, f := range pkg.Syntax {
|
||||
for _, imp := range f.Imports {
|
||||
path, err := strconv.Unquote(imp.Path.Value)
|
||||
if err != nil {
|
||||
log.Fatalf("invalid import path string: %v", err) // Shouldn't happen here since packages.Load succeeded.
|
||||
}
|
||||
if path == dst {
|
||||
continue
|
||||
}
|
||||
if newPath, ok := importMap[path]; ok {
|
||||
path = newPath
|
||||
}
|
||||
|
||||
var name string
|
||||
if imp.Name != nil {
|
||||
name = imp.Name.Name
|
||||
}
|
||||
spec := fmt.Sprintf("%s %q", name, path)
|
||||
if isStandardImportPath(path) {
|
||||
pkgStd[spec] = true
|
||||
} else {
|
||||
pkgExt[spec] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Print a single declaration that imports all necessary packages.
|
||||
fmt.Fprintln(&out, "import (")
|
||||
for p := range pkgStd {
|
||||
fmt.Fprintf(&out, "\t%s\n", p)
|
||||
}
|
||||
if len(pkgExt) > 0 {
|
||||
fmt.Fprintln(&out)
|
||||
}
|
||||
for p := range pkgExt {
|
||||
fmt.Fprintf(&out, "\t%s\n", p)
|
||||
}
|
||||
fmt.Fprint(&out, ")\n\n")
|
||||
|
||||
// Modify and print each file.
|
||||
for _, f := range pkg.Syntax {
|
||||
// Update renamed identifiers.
|
||||
for id, obj := range pkg.TypesInfo.Defs {
|
||||
if objsToUpdate[obj] {
|
||||
id.Name = prefix + obj.Name()
|
||||
}
|
||||
}
|
||||
for id, obj := range pkg.TypesInfo.Uses {
|
||||
if objsToUpdate[obj] {
|
||||
id.Name = prefix + obj.Name()
|
||||
}
|
||||
}
|
||||
|
||||
// For each qualified identifier that refers to the
|
||||
// destination package, remove the qualifier.
|
||||
// The "@@@." strings are removed in postprocessing.
|
||||
ast.Inspect(f, func(n ast.Node) bool {
|
||||
if sel, ok := n.(*ast.SelectorExpr); ok {
|
||||
if id, ok := sel.X.(*ast.Ident); ok {
|
||||
if obj, ok := pkg.TypesInfo.Uses[id].(*types.PkgName); ok {
|
||||
if obj.Imported().Path() == dst {
|
||||
id.Name = "@@@"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
last := f.Package
|
||||
if len(f.Imports) > 0 {
|
||||
imp := f.Imports[len(f.Imports)-1]
|
||||
last = imp.End()
|
||||
if imp.Comment != nil {
|
||||
if e := imp.Comment.End(); e > last {
|
||||
last = e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pretty-print package-level declarations.
|
||||
// but no package or import declarations.
|
||||
var buf bytes.Buffer
|
||||
for _, decl := range f.Decls {
|
||||
if decl, ok := decl.(*ast.GenDecl); ok && decl.Tok == token.IMPORT {
|
||||
continue
|
||||
}
|
||||
|
||||
beg, end := sourceRange(decl)
|
||||
|
||||
printComments(&out, f.Comments, last, beg)
|
||||
|
||||
buf.Reset()
|
||||
format.Node(&buf, pkg.Fset, &printer.CommentedNode{Node: decl, Comments: f.Comments})
|
||||
// Remove each "@@@." in the output.
|
||||
// TODO(adonovan): not hygienic.
|
||||
out.Write(bytes.Replace(buf.Bytes(), []byte("@@@."), nil, -1))
|
||||
|
||||
last = printSameLineComment(&out, f.Comments, pkg.Fset, end)
|
||||
|
||||
out.WriteString("\n\n")
|
||||
}
|
||||
|
||||
printLastComments(&out, f.Comments, last)
|
||||
}
|
||||
|
||||
// Now format the entire thing.
|
||||
result, err := format.Source(out.Bytes())
|
||||
if err != nil {
|
||||
log.Fatalf("formatting failed: %v", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// sourceRange returns the [beg, end) interval of source code
|
||||
// belonging to decl (incl. associated comments).
|
||||
func sourceRange(decl ast.Decl) (beg, end token.Pos) {
|
||||
beg = decl.Pos()
|
||||
end = decl.End()
|
||||
|
||||
var doc, com *ast.CommentGroup
|
||||
|
||||
switch d := decl.(type) {
|
||||
case *ast.GenDecl:
|
||||
doc = d.Doc
|
||||
if len(d.Specs) > 0 {
|
||||
switch spec := d.Specs[len(d.Specs)-1].(type) {
|
||||
case *ast.ValueSpec:
|
||||
com = spec.Comment
|
||||
case *ast.TypeSpec:
|
||||
com = spec.Comment
|
||||
}
|
||||
}
|
||||
case *ast.FuncDecl:
|
||||
doc = d.Doc
|
||||
}
|
||||
|
||||
if doc != nil {
|
||||
beg = doc.Pos()
|
||||
}
|
||||
if com != nil && com.End() > end {
|
||||
end = com.End()
|
||||
}
|
||||
|
||||
return beg, end
|
||||
}
|
||||
|
||||
func printComments(out *bytes.Buffer, comments []*ast.CommentGroup, pos, end token.Pos) {
|
||||
for _, cg := range comments {
|
||||
if pos <= cg.Pos() && cg.Pos() < end {
|
||||
for _, c := range cg.List {
|
||||
fmt.Fprintln(out, c.Text)
|
||||
}
|
||||
fmt.Fprintln(out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const infinity = 1 << 30
|
||||
|
||||
func printLastComments(out *bytes.Buffer, comments []*ast.CommentGroup, pos token.Pos) {
|
||||
printComments(out, comments, pos, infinity)
|
||||
}
|
||||
|
||||
func printSameLineComment(out *bytes.Buffer, comments []*ast.CommentGroup, fset *token.FileSet, pos token.Pos) token.Pos {
|
||||
tf := fset.File(pos)
|
||||
for _, cg := range comments {
|
||||
if pos <= cg.Pos() && tf.Line(cg.Pos()) == tf.Line(pos) {
|
||||
for _, c := range cg.List {
|
||||
fmt.Fprintln(out, c.Text)
|
||||
}
|
||||
return cg.End()
|
||||
}
|
||||
}
|
||||
return pos
|
||||
}
|
||||
|
||||
func quoteArgs(ss []string) []string {
|
||||
// From go help generate:
|
||||
//
|
||||
// > The arguments to the directive are space-separated tokens or
|
||||
// > double-quoted strings passed to the generator as individual
|
||||
// > arguments when it is run.
|
||||
//
|
||||
// > Quoted strings use Go syntax and are evaluated before execution; a
|
||||
// > quoted string appears as a single argument to the generator.
|
||||
//
|
||||
var qs []string
|
||||
for _, s := range ss {
|
||||
if s == "" || containsSpace(s) {
|
||||
s = strconv.Quote(s)
|
||||
}
|
||||
qs = append(qs, s)
|
||||
}
|
||||
return qs
|
||||
}
|
||||
|
||||
func containsSpace(s string) bool {
|
||||
for _, r := range s {
|
||||
if unicode.IsSpace(r) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type flagFunc func(string)
|
||||
|
||||
func (f flagFunc) Set(s string) error {
|
||||
f(s)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f flagFunc) String() string { return "" }
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright 2015 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 main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/go/packages/packagestest"
|
||||
)
|
||||
|
||||
func TestBundle(t *testing.T) { packagestest.TestAll(t, testBundle) }
|
||||
func testBundle(t *testing.T, x packagestest.Exporter) {
|
||||
load := func(name string) string {
|
||||
data, err := os.ReadFile(name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
e := packagestest.Export(t, x, []packagestest.Module{
|
||||
{
|
||||
Name: "initial",
|
||||
Files: map[string]interface{}{
|
||||
"a.go": load("testdata/src/initial/a.go"),
|
||||
"b.go": load("testdata/src/initial/b.go"),
|
||||
"c.go": load("testdata/src/initial/c.go"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "domain.name/importdecl",
|
||||
Files: map[string]interface{}{
|
||||
"p.go": load("testdata/src/domain.name/importdecl/p.go"),
|
||||
},
|
||||
},
|
||||
})
|
||||
defer e.Cleanup()
|
||||
testingOnlyPackagesConfig = e.Config
|
||||
|
||||
os.Args = os.Args[:1] // avoid e.g. -test=short in the output
|
||||
out, err := bundle("initial", "github.com/dest", "dest", "prefix", "tag")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if got, want := string(out), load("testdata/out.golden"); got != want {
|
||||
t.Errorf("-- got --\n%s\n-- want --\n%s\n-- diff --", got, want)
|
||||
|
||||
if err := os.WriteFile("testdata/out.got", out, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(diff("testdata/out.golden", "testdata/out.got"))
|
||||
}
|
||||
}
|
||||
|
||||
func diff(a, b string) string {
|
||||
var cmd *exec.Cmd
|
||||
switch runtime.GOOS {
|
||||
case "plan9":
|
||||
cmd = exec.Command("/bin/diff", "-c", a, b)
|
||||
default:
|
||||
cmd = exec.Command("/usr/bin/diff", "-u", a, b)
|
||||
}
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
cmd.Stderr = &out
|
||||
cmd.Run() // nonzero exit is expected
|
||||
if out.Len() == 0 {
|
||||
return "(failed to compute diff)"
|
||||
}
|
||||
return out.String()
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
//go:build tag
|
||||
|
||||
// Code generated by golang.org/x/tools/cmd/bundle. DO NOT EDIT.
|
||||
// $ bundle
|
||||
|
||||
// The package doc comment
|
||||
//
|
||||
|
||||
package dest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
. "fmt"
|
||||
_ "fmt"
|
||||
renamedfmt "fmt"
|
||||
renamedfmt2 "fmt"
|
||||
|
||||
"domain.name/importdecl"
|
||||
)
|
||||
|
||||
// init functions are not renamed
|
||||
func init() { prefixfoo() }
|
||||
|
||||
// Type S.
|
||||
type prefixS struct {
|
||||
prefixt
|
||||
u int
|
||||
} /* multi-line
|
||||
comment
|
||||
*/
|
||||
|
||||
// non-associated comment
|
||||
|
||||
/*
|
||||
non-associated comment2
|
||||
*/
|
||||
|
||||
// Function bar.
|
||||
func prefixbar(s *prefixS) {
|
||||
fmt.Println(s.prefixt, s.u) // comment inside function
|
||||
}
|
||||
|
||||
// file-end comment
|
||||
|
||||
type prefixt int // type1
|
||||
|
||||
// const1
|
||||
const prefixc = 1 // const2
|
||||
|
||||
func prefixfoo() {
|
||||
fmt.Println(importdecl.F())
|
||||
}
|
||||
|
||||
// zinit
|
||||
const (
|
||||
prefixz1 = iota // z1
|
||||
prefixz2 // z2
|
||||
) // zend
|
||||
|
||||
func prefixbaz() {
|
||||
renamedfmt.Println()
|
||||
renamedfmt2.Println()
|
||||
Println()
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package importdecl
|
||||
|
||||
func F() int { return 1 }
|
||||
@@ -0,0 +1,27 @@
|
||||
package initial
|
||||
|
||||
import "fmt" // this comment should not be visible
|
||||
|
||||
// init functions are not renamed
|
||||
func init() { foo() }
|
||||
|
||||
// Type S.
|
||||
type S struct {
|
||||
t
|
||||
u int
|
||||
} /* multi-line
|
||||
comment
|
||||
*/
|
||||
|
||||
// non-associated comment
|
||||
|
||||
/*
|
||||
non-associated comment2
|
||||
*/
|
||||
|
||||
// Function bar.
|
||||
func bar(s *S) {
|
||||
fmt.Println(s.t, s.u) // comment inside function
|
||||
}
|
||||
|
||||
// file-end comment
|
||||
@@ -0,0 +1,23 @@
|
||||
// The package doc comment
|
||||
package initial
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"domain.name/importdecl"
|
||||
)
|
||||
|
||||
type t int // type1
|
||||
|
||||
// const1
|
||||
const c = 1 // const2
|
||||
|
||||
func foo() {
|
||||
fmt.Println(importdecl.F())
|
||||
}
|
||||
|
||||
// zinit
|
||||
const (
|
||||
z1 = iota // z1
|
||||
z2 // z2
|
||||
) // zend
|
||||
@@ -0,0 +1,12 @@
|
||||
package initial
|
||||
|
||||
import _ "fmt"
|
||||
import renamedfmt "fmt"
|
||||
import renamedfmt2 "fmt"
|
||||
import . "fmt"
|
||||
|
||||
func baz() {
|
||||
renamedfmt.Println()
|
||||
renamedfmt2.Println()
|
||||
Println()
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
// Copyright 2014 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.
|
||||
|
||||
// callgraph: a tool for reporting the call graph of a Go program.
|
||||
// See Usage for details, or run with -help.
|
||||
package main // import "golang.org/x/tools/cmd/callgraph"
|
||||
|
||||
// TODO(adonovan):
|
||||
//
|
||||
// Features:
|
||||
// - restrict graph to a single package
|
||||
// - output
|
||||
// - functions reachable from root (use digraph tool?)
|
||||
// - unreachable functions (use digraph tool?)
|
||||
// - dynamic (runtime) types
|
||||
// - indexed output (numbered nodes)
|
||||
// - JSON output
|
||||
// - additional template fields:
|
||||
// callee file/line/col
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/build"
|
||||
"go/token"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"text/template"
|
||||
|
||||
"golang.org/x/tools/go/buildutil"
|
||||
"golang.org/x/tools/go/callgraph"
|
||||
"golang.org/x/tools/go/callgraph/cha"
|
||||
"golang.org/x/tools/go/callgraph/rta"
|
||||
"golang.org/x/tools/go/callgraph/static"
|
||||
"golang.org/x/tools/go/callgraph/vta"
|
||||
"golang.org/x/tools/go/packages"
|
||||
"golang.org/x/tools/go/ssa"
|
||||
"golang.org/x/tools/go/ssa/ssautil"
|
||||
)
|
||||
|
||||
// flags
|
||||
var (
|
||||
algoFlag = flag.String("algo", "rta",
|
||||
`Call graph construction algorithm (static, cha, rta, vta)`)
|
||||
|
||||
testFlag = flag.Bool("test", false,
|
||||
"Loads test code (*_test.go) for imported packages")
|
||||
|
||||
formatFlag = flag.String("format",
|
||||
"{{.Caller}}\t--{{.Dynamic}}-{{.Line}}:{{.Column}}-->\t{{.Callee}}",
|
||||
"A template expression specifying how to format an edge")
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.Var((*buildutil.TagsFlag)(&build.Default.BuildTags), "tags", buildutil.TagsFlagDoc)
|
||||
}
|
||||
|
||||
const Usage = `callgraph: display the call graph of a Go program.
|
||||
|
||||
Usage:
|
||||
|
||||
callgraph [-algo=static|cha|rta|vta] [-test] [-format=...] package...
|
||||
|
||||
Flags:
|
||||
|
||||
-algo Specifies the call-graph construction algorithm, one of:
|
||||
|
||||
static static calls only (unsound)
|
||||
cha Class Hierarchy Analysis
|
||||
rta Rapid Type Analysis
|
||||
vta Variable Type Analysis
|
||||
|
||||
The algorithms are ordered by increasing precision in their
|
||||
treatment of dynamic calls (and thus also computational cost).
|
||||
RTA requires a whole program (main or test), and
|
||||
include only functions reachable from main.
|
||||
|
||||
-test Include the package's tests in the analysis.
|
||||
|
||||
-format Specifies the format in which each call graph edge is displayed.
|
||||
One of:
|
||||
|
||||
digraph output suitable for input to
|
||||
golang.org/x/tools/cmd/digraph.
|
||||
graphviz output in AT&T GraphViz (.dot) format.
|
||||
|
||||
All other values are interpreted using text/template syntax.
|
||||
The default value is:
|
||||
|
||||
{{.Caller}}\t--{{.Dynamic}}-{{.Line}}:{{.Column}}-->\t{{.Callee}}
|
||||
|
||||
The structure passed to the template is (effectively):
|
||||
|
||||
type Edge struct {
|
||||
Caller *ssa.Function // calling function
|
||||
Callee *ssa.Function // called function
|
||||
|
||||
// Call site:
|
||||
Filename string // containing file
|
||||
Offset int // offset within file of '('
|
||||
Line int // line number
|
||||
Column int // column number of call
|
||||
Dynamic string // "static" or "dynamic"
|
||||
Description string // e.g. "static method call"
|
||||
}
|
||||
|
||||
Caller and Callee are *ssa.Function values, which print as
|
||||
"(*sync/atomic.Mutex).Lock", but other attributes may be
|
||||
derived from them. For example:
|
||||
|
||||
- {{.Caller.Pkg.Pkg.Path}} yields the import path of the
|
||||
enclosing package; and
|
||||
|
||||
- {{(.Caller.Prog.Fset.Position .Caller.Pos).Filename}}
|
||||
yields the name of the file that declares the caller.
|
||||
|
||||
- The 'posn' template function returns the token.Position
|
||||
of an ssa.Function, so the previous example can be
|
||||
reduced to {{(posn .Caller).Filename}}.
|
||||
|
||||
Consult the documentation for go/token, text/template, and
|
||||
golang.org/x/tools/go/ssa for more detail.
|
||||
|
||||
Examples:
|
||||
|
||||
Show the call graph of the trivial web server application:
|
||||
|
||||
callgraph -format digraph $GOROOT/src/net/http/triv.go
|
||||
|
||||
Same, but show only the packages of each function:
|
||||
|
||||
callgraph -format '{{.Caller.Pkg.Pkg.Path}} -> {{.Callee.Pkg.Pkg.Path}}' \
|
||||
$GOROOT/src/net/http/triv.go | sort | uniq
|
||||
|
||||
Show functions that make dynamic calls into the 'fmt' test package,
|
||||
using the Rapid Type Analysis algorithm:
|
||||
|
||||
callgraph -format='{{.Caller}} -{{.Dynamic}}-> {{.Callee}}' -test -algo=rta fmt |
|
||||
sed -ne 's/-dynamic-/--/p' |
|
||||
sed -ne 's/-->.*fmt_test.*$//p' | sort | uniq
|
||||
|
||||
Show all functions directly called by the callgraph tool's main function:
|
||||
|
||||
callgraph -format=digraph golang.org/x/tools/cmd/callgraph |
|
||||
digraph succs golang.org/x/tools/cmd/callgraph.main
|
||||
`
|
||||
|
||||
func init() {
|
||||
// If $GOMAXPROCS isn't set, use the full capacity of the machine.
|
||||
// For small machines, use at least 4 threads.
|
||||
if os.Getenv("GOMAXPROCS") == "" {
|
||||
n := runtime.NumCPU()
|
||||
if n < 4 {
|
||||
n = 4
|
||||
}
|
||||
runtime.GOMAXPROCS(n)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
if err := doCallgraph("", "", *algoFlag, *formatFlag, *testFlag, flag.Args()); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "callgraph: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
var stdout io.Writer = os.Stdout
|
||||
|
||||
func doCallgraph(dir, gopath, algo, format string, tests bool, args []string) error {
|
||||
if len(args) == 0 {
|
||||
fmt.Fprint(os.Stderr, Usage)
|
||||
return nil
|
||||
}
|
||||
|
||||
cfg := &packages.Config{
|
||||
Mode: packages.LoadAllSyntax,
|
||||
Tests: tests,
|
||||
Dir: dir,
|
||||
}
|
||||
if gopath != "" {
|
||||
cfg.Env = append(os.Environ(), "GOPATH="+gopath) // to enable testing
|
||||
}
|
||||
initial, err := packages.Load(cfg, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if packages.PrintErrors(initial) > 0 {
|
||||
return fmt.Errorf("packages contain errors")
|
||||
}
|
||||
|
||||
// Create and build SSA-form program representation.
|
||||
mode := ssa.InstantiateGenerics // instantiate generics by default for soundness
|
||||
prog, pkgs := ssautil.AllPackages(initial, mode)
|
||||
prog.Build()
|
||||
|
||||
// -- call graph construction ------------------------------------------
|
||||
|
||||
var cg *callgraph.Graph
|
||||
|
||||
switch algo {
|
||||
case "static":
|
||||
cg = static.CallGraph(prog)
|
||||
|
||||
case "cha":
|
||||
cg = cha.CallGraph(prog)
|
||||
|
||||
case "pta":
|
||||
return fmt.Errorf("pointer analysis is no longer supported (see Go issue #59676)")
|
||||
|
||||
case "rta":
|
||||
mains, err := mainPackages(pkgs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var roots []*ssa.Function
|
||||
for _, main := range mains {
|
||||
roots = append(roots, main.Func("init"), main.Func("main"))
|
||||
}
|
||||
rtares := rta.Analyze(roots, true)
|
||||
cg = rtares.CallGraph
|
||||
|
||||
// NB: RTA gives us Reachable and RuntimeTypes too.
|
||||
|
||||
case "vta":
|
||||
cg = vta.CallGraph(ssautil.AllFunctions(prog), cha.CallGraph(prog))
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unknown algorithm: %s", algo)
|
||||
}
|
||||
|
||||
cg.DeleteSyntheticNodes()
|
||||
|
||||
// -- output------------------------------------------------------------
|
||||
|
||||
var before, after string
|
||||
|
||||
// Pre-canned formats.
|
||||
switch format {
|
||||
case "digraph":
|
||||
format = `{{printf "%q %q" .Caller .Callee}}`
|
||||
|
||||
case "graphviz":
|
||||
before = "digraph callgraph {\n"
|
||||
after = "}\n"
|
||||
format = ` {{printf "%q" .Caller}} -> {{printf "%q" .Callee}}`
|
||||
}
|
||||
|
||||
funcMap := template.FuncMap{
|
||||
"posn": func(f *ssa.Function) token.Position {
|
||||
return f.Prog.Fset.Position(f.Pos())
|
||||
},
|
||||
}
|
||||
tmpl, err := template.New("-format").Funcs(funcMap).Parse(format)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid -format template: %v", err)
|
||||
}
|
||||
|
||||
// Allocate these once, outside the traversal.
|
||||
var buf bytes.Buffer
|
||||
data := Edge{fset: prog.Fset}
|
||||
|
||||
fmt.Fprint(stdout, before)
|
||||
if err := callgraph.GraphVisitEdges(cg, func(edge *callgraph.Edge) error {
|
||||
data.position.Offset = -1
|
||||
data.edge = edge
|
||||
data.Caller = edge.Caller.Func
|
||||
data.Callee = edge.Callee.Func
|
||||
|
||||
buf.Reset()
|
||||
if err := tmpl.Execute(&buf, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
stdout.Write(buf.Bytes())
|
||||
if len := buf.Len(); len == 0 || buf.Bytes()[len-1] != '\n' {
|
||||
fmt.Fprintln(stdout)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprint(stdout, after)
|
||||
return nil
|
||||
}
|
||||
|
||||
// mainPackages returns the main packages to analyze.
|
||||
// Each resulting package is named "main" and has a main function.
|
||||
func mainPackages(pkgs []*ssa.Package) ([]*ssa.Package, error) {
|
||||
var mains []*ssa.Package
|
||||
for _, p := range pkgs {
|
||||
if p != nil && p.Pkg.Name() == "main" && p.Func("main") != nil {
|
||||
mains = append(mains, p)
|
||||
}
|
||||
}
|
||||
if len(mains) == 0 {
|
||||
return nil, fmt.Errorf("no main packages")
|
||||
}
|
||||
return mains, nil
|
||||
}
|
||||
|
||||
type Edge struct {
|
||||
Caller *ssa.Function
|
||||
Callee *ssa.Function
|
||||
|
||||
edge *callgraph.Edge
|
||||
fset *token.FileSet
|
||||
position token.Position // initialized lazily
|
||||
}
|
||||
|
||||
func (e *Edge) pos() *token.Position {
|
||||
if e.position.Offset == -1 {
|
||||
e.position = e.fset.Position(e.edge.Pos()) // called lazily
|
||||
}
|
||||
return &e.position
|
||||
}
|
||||
|
||||
func (e *Edge) Filename() string { return e.pos().Filename }
|
||||
func (e *Edge) Column() int { return e.pos().Column }
|
||||
func (e *Edge) Line() int { return e.pos().Line }
|
||||
func (e *Edge) Offset() int { return e.pos().Offset }
|
||||
|
||||
func (e *Edge) Dynamic() string {
|
||||
if e.edge.Site != nil && e.edge.Site.Common().StaticCallee() == nil {
|
||||
return "dynamic"
|
||||
}
|
||||
return "static"
|
||||
}
|
||||
|
||||
func (e *Edge) Description() string { return e.edge.Description() }
|
||||
@@ -0,0 +1,101 @@
|
||||
// Copyright 2014 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.
|
||||
|
||||
// No testdata on Android.
|
||||
|
||||
//go:build !android && go1.11
|
||||
// +build !android,go1.11
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/internal/testenv"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// This test currently requires GOPATH mode.
|
||||
// Explicitly disabling module mode should suffix, but
|
||||
// we'll also turn off GOPROXY just for good measure.
|
||||
if err := os.Setenv("GO111MODULE", "off"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := os.Setenv("GOPROXY", "off"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCallgraph(t *testing.T) {
|
||||
testenv.NeedsTool(t, "go")
|
||||
|
||||
gopath, err := filepath.Abs("testdata")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, test := range []struct {
|
||||
algo string
|
||||
tests bool
|
||||
want []string
|
||||
}{
|
||||
{"rta", false, []string{
|
||||
// rta imprecisely shows cross product of {main,main2} x {C,D}
|
||||
`pkg.main --> (pkg.C).f`,
|
||||
`pkg.main --> (pkg.D).f`,
|
||||
`pkg.main --> pkg.main2`,
|
||||
`pkg.main2 --> (pkg.C).f`,
|
||||
`pkg.main2 --> (pkg.D).f`,
|
||||
}},
|
||||
{"vta", false, []string{
|
||||
// vta distinguishes main->C, main2->D.
|
||||
"pkg.main --> (pkg.C).f",
|
||||
"pkg.main --> pkg.main2",
|
||||
"pkg.main2 --> (pkg.D).f",
|
||||
}},
|
||||
// tests: both the package's main and the test's main are called.
|
||||
// The callgraph includes all the guts of the "testing" package.
|
||||
{"rta", true, []string{
|
||||
`pkg.test.main --> testing.MainStart`,
|
||||
`testing.runExample --> pkg.Example`,
|
||||
`pkg.Example --> (pkg.C).f`,
|
||||
`pkg.main --> (pkg.C).f`,
|
||||
}},
|
||||
{"vta", true, []string{
|
||||
`pkg.test.main --> testing.MainStart`,
|
||||
`testing.runExample --> pkg.Example`,
|
||||
`pkg.Example --> (pkg.C).f`,
|
||||
`pkg.main --> (pkg.C).f`,
|
||||
}},
|
||||
} {
|
||||
const format = "{{.Caller}} --> {{.Callee}}"
|
||||
stdout = new(bytes.Buffer)
|
||||
if err := doCallgraph("testdata/src", gopath, test.algo, format, test.tests, []string{"pkg"}); err != nil {
|
||||
t.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
edges := make(map[string]bool)
|
||||
for _, line := range strings.Split(fmt.Sprint(stdout), "\n") {
|
||||
edges[line] = true
|
||||
}
|
||||
ok := true
|
||||
for _, edge := range test.want {
|
||||
if !edges[edge] {
|
||||
ok = false
|
||||
t.Errorf("callgraph(%q, %t): missing edge: %s",
|
||||
test.algo, test.tests, edge)
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
t.Log("got:\n", stdout)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package main
|
||||
|
||||
type I interface {
|
||||
f()
|
||||
}
|
||||
|
||||
type C int
|
||||
|
||||
func (C) f() {}
|
||||
|
||||
type D int
|
||||
|
||||
func (D) f() {}
|
||||
|
||||
func main() {
|
||||
var i I = C(0)
|
||||
i.f() // dynamic call
|
||||
|
||||
main2()
|
||||
}
|
||||
|
||||
func main2() {
|
||||
var i I = D(0)
|
||||
i.f() // dynamic call
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package main
|
||||
|
||||
// An Example function must have an "Output:" comment for the go build
|
||||
// system to generate a call to it from the test main package.
|
||||
|
||||
func Example() {
|
||||
C(0).f()
|
||||
|
||||
// Output:
|
||||
}
|
||||
@@ -0,0 +1,697 @@
|
||||
// Copyright 2015 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.
|
||||
|
||||
// Compilebench benchmarks the speed of the Go compiler.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// compilebench [options]
|
||||
//
|
||||
// It times the compilation of various packages and prints results in
|
||||
// the format used by package testing (and expected by golang.org/x/perf/cmd/benchstat).
|
||||
//
|
||||
// The options are:
|
||||
//
|
||||
// -alloc
|
||||
// Report allocations.
|
||||
//
|
||||
// -compile exe
|
||||
// Use exe as the path to the cmd/compile binary.
|
||||
//
|
||||
// -compileflags 'list'
|
||||
// Pass the space-separated list of flags to the compilation.
|
||||
//
|
||||
// -link exe
|
||||
// Use exe as the path to the cmd/link binary.
|
||||
//
|
||||
// -linkflags 'list'
|
||||
// Pass the space-separated list of flags to the linker.
|
||||
//
|
||||
// -count n
|
||||
// Run each benchmark n times (default 1).
|
||||
//
|
||||
// -cpuprofile file
|
||||
// Write a CPU profile of the compiler to file.
|
||||
//
|
||||
// -go path
|
||||
// Path to "go" command (default "go").
|
||||
//
|
||||
// -memprofile file
|
||||
// Write a memory profile of the compiler to file.
|
||||
//
|
||||
// -memprofilerate rate
|
||||
// Set runtime.MemProfileRate during compilation.
|
||||
//
|
||||
// -obj
|
||||
// Report object file statistics.
|
||||
//
|
||||
// -pkg pkg
|
||||
// Benchmark compiling a single package.
|
||||
//
|
||||
// -run regexp
|
||||
// Only run benchmarks with names matching regexp.
|
||||
//
|
||||
// -short
|
||||
// Skip long-running benchmarks.
|
||||
//
|
||||
// Although -cpuprofile and -memprofile are intended to write a
|
||||
// combined profile for all the executed benchmarks to file,
|
||||
// today they write only the profile for the last benchmark executed.
|
||||
//
|
||||
// The default memory profiling rate is one profile sample per 512 kB
|
||||
// allocated (see “go doc runtime.MemProfileRate”).
|
||||
// Lowering the rate (for example, -memprofilerate 64000) produces
|
||||
// a more fine-grained and therefore accurate profile, but it also incurs
|
||||
// execution cost. For benchmark comparisons, never use timings
|
||||
// obtained with a low -memprofilerate option.
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// Assuming the base version of the compiler has been saved with
|
||||
// “toolstash save,” this sequence compares the old and new compiler:
|
||||
//
|
||||
// compilebench -count 10 -compile $(toolstash -n compile) >old.txt
|
||||
// compilebench -count 10 >new.txt
|
||||
// benchstat old.txt new.txt
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
goroot string
|
||||
compiler string
|
||||
assembler string
|
||||
linker string
|
||||
runRE *regexp.Regexp
|
||||
is6g bool
|
||||
needCompilingRuntimeFlag bool
|
||||
)
|
||||
|
||||
var (
|
||||
flagGoCmd = flag.String("go", "go", "path to \"go\" command")
|
||||
flagAlloc = flag.Bool("alloc", false, "report allocations")
|
||||
flagObj = flag.Bool("obj", false, "report object file stats")
|
||||
flagCompiler = flag.String("compile", "", "use `exe` as the cmd/compile binary")
|
||||
flagAssembler = flag.String("asm", "", "use `exe` as the cmd/asm binary")
|
||||
flagCompilerFlags = flag.String("compileflags", "", "additional `flags` to pass to compile")
|
||||
flagLinker = flag.String("link", "", "use `exe` as the cmd/link binary")
|
||||
flagLinkerFlags = flag.String("linkflags", "", "additional `flags` to pass to link")
|
||||
flagRun = flag.String("run", "", "run benchmarks matching `regexp`")
|
||||
flagCount = flag.Int("count", 1, "run benchmarks `n` times")
|
||||
flagCpuprofile = flag.String("cpuprofile", "", "write CPU profile to `file`")
|
||||
flagMemprofile = flag.String("memprofile", "", "write memory profile to `file`")
|
||||
flagMemprofilerate = flag.Int64("memprofilerate", -1, "set memory profile `rate`")
|
||||
flagPackage = flag.String("pkg", "", "if set, benchmark the package at path `pkg`")
|
||||
flagShort = flag.Bool("short", false, "skip long-running benchmarks")
|
||||
flagTrace = flag.Bool("trace", false, "debug tracing of builds")
|
||||
)
|
||||
|
||||
type test struct {
|
||||
name string
|
||||
r runner
|
||||
}
|
||||
|
||||
type runner interface {
|
||||
long() bool
|
||||
run(name string, count int) error
|
||||
}
|
||||
|
||||
var tests = []test{
|
||||
{"BenchmarkTemplate", compile{"html/template"}},
|
||||
{"BenchmarkUnicode", compile{"unicode"}},
|
||||
{"BenchmarkGoTypes", compile{"go/types"}},
|
||||
{"BenchmarkCompiler", compile{"cmd/compile/internal/gc"}},
|
||||
{"BenchmarkSSA", compile{"cmd/compile/internal/ssa"}},
|
||||
{"BenchmarkFlate", compile{"compress/flate"}},
|
||||
{"BenchmarkGoParser", compile{"go/parser"}},
|
||||
{"BenchmarkReflect", compile{"reflect"}},
|
||||
{"BenchmarkTar", compile{"archive/tar"}},
|
||||
{"BenchmarkXML", compile{"encoding/xml"}},
|
||||
{"BenchmarkLinkCompiler", link{"cmd/compile", ""}},
|
||||
{"BenchmarkExternalLinkCompiler", link{"cmd/compile", "-linkmode=external"}},
|
||||
{"BenchmarkLinkWithoutDebugCompiler", link{"cmd/compile", "-w"}},
|
||||
{"BenchmarkStdCmd", goBuild{[]string{"std", "cmd"}}},
|
||||
{"BenchmarkHelloSize", size{"$GOROOT/test/helloworld.go", false}},
|
||||
{"BenchmarkCmdGoSize", size{"cmd/go", true}},
|
||||
}
|
||||
|
||||
func usage() {
|
||||
fmt.Fprintf(os.Stderr, "usage: compilebench [options]\n")
|
||||
fmt.Fprintf(os.Stderr, "options:\n")
|
||||
flag.PrintDefaults()
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
log.SetPrefix("compilebench: ")
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
if flag.NArg() != 0 {
|
||||
usage()
|
||||
}
|
||||
|
||||
s, err := exec.Command(*flagGoCmd, "env", "GOROOT").CombinedOutput()
|
||||
if err != nil {
|
||||
log.Fatalf("%s env GOROOT: %v", *flagGoCmd, err)
|
||||
}
|
||||
goroot = strings.TrimSpace(string(s))
|
||||
os.Setenv("GOROOT", goroot) // for any subcommands
|
||||
|
||||
compiler = *flagCompiler
|
||||
if compiler == "" {
|
||||
var foundTool string
|
||||
foundTool, compiler = toolPath("compile", "6g")
|
||||
if foundTool == "6g" {
|
||||
is6g = true
|
||||
}
|
||||
}
|
||||
assembler = *flagAssembler
|
||||
if assembler == "" {
|
||||
_, assembler = toolPath("asm")
|
||||
}
|
||||
if err := checkCompilingRuntimeFlag(assembler); err != nil {
|
||||
log.Fatalf("checkCompilingRuntimeFlag: %v", err)
|
||||
}
|
||||
|
||||
linker = *flagLinker
|
||||
if linker == "" && !is6g { // TODO: Support 6l
|
||||
_, linker = toolPath("link")
|
||||
}
|
||||
|
||||
if is6g {
|
||||
*flagMemprofilerate = -1
|
||||
*flagAlloc = false
|
||||
*flagCpuprofile = ""
|
||||
*flagMemprofile = ""
|
||||
}
|
||||
|
||||
if *flagRun != "" {
|
||||
r, err := regexp.Compile(*flagRun)
|
||||
if err != nil {
|
||||
log.Fatalf("invalid -run argument: %v", err)
|
||||
}
|
||||
runRE = r
|
||||
}
|
||||
|
||||
if *flagPackage != "" {
|
||||
tests = []test{
|
||||
{"BenchmarkPkg", compile{*flagPackage}},
|
||||
{"BenchmarkPkgLink", link{*flagPackage, ""}},
|
||||
}
|
||||
runRE = nil
|
||||
}
|
||||
|
||||
for i := 0; i < *flagCount; i++ {
|
||||
for _, tt := range tests {
|
||||
if tt.r.long() && *flagShort {
|
||||
continue
|
||||
}
|
||||
if runRE == nil || runRE.MatchString(tt.name) {
|
||||
if err := tt.r.run(tt.name, i); err != nil {
|
||||
log.Printf("%s: %v", tt.name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toolPath(names ...string) (found, path string) {
|
||||
var out1 []byte
|
||||
var err1 error
|
||||
for i, name := range names {
|
||||
out, err := exec.Command(*flagGoCmd, "tool", "-n", name).CombinedOutput()
|
||||
if err == nil {
|
||||
return name, strings.TrimSpace(string(out))
|
||||
}
|
||||
if i == 0 {
|
||||
out1, err1 = out, err
|
||||
}
|
||||
}
|
||||
log.Fatalf("go tool -n %s: %v\n%s", names[0], err1, out1)
|
||||
return "", ""
|
||||
}
|
||||
|
||||
type Pkg struct {
|
||||
ImportPath string
|
||||
Dir string
|
||||
GoFiles []string
|
||||
SFiles []string
|
||||
}
|
||||
|
||||
func goList(dir string) (*Pkg, error) {
|
||||
var pkg Pkg
|
||||
out, err := exec.Command(*flagGoCmd, "list", "-json", dir).Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("go list -json %s: %v", dir, err)
|
||||
}
|
||||
if err := json.Unmarshal(out, &pkg); err != nil {
|
||||
return nil, fmt.Errorf("go list -json %s: unmarshal: %v", dir, err)
|
||||
}
|
||||
return &pkg, nil
|
||||
}
|
||||
|
||||
func runCmd(name string, cmd *exec.Cmd) error {
|
||||
start := time.Now()
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%v\n%s", err, out)
|
||||
}
|
||||
fmt.Printf("%s 1 %d ns/op\n", name, time.Since(start).Nanoseconds())
|
||||
return nil
|
||||
}
|
||||
|
||||
type goBuild struct{ pkgs []string }
|
||||
|
||||
func (goBuild) long() bool { return true }
|
||||
|
||||
func (r goBuild) run(name string, count int) error {
|
||||
args := []string{"build", "-a"}
|
||||
if *flagCompilerFlags != "" {
|
||||
args = append(args, "-gcflags", *flagCompilerFlags)
|
||||
}
|
||||
args = append(args, r.pkgs...)
|
||||
cmd := exec.Command(*flagGoCmd, args...)
|
||||
cmd.Dir = filepath.Join(goroot, "src")
|
||||
return runCmd(name, cmd)
|
||||
}
|
||||
|
||||
type size struct {
|
||||
// path is either a path to a file ("$GOROOT/test/helloworld.go") or a package path ("cmd/go").
|
||||
path string
|
||||
isLong bool
|
||||
}
|
||||
|
||||
func (r size) long() bool { return r.isLong }
|
||||
|
||||
func (r size) run(name string, count int) error {
|
||||
if strings.HasPrefix(r.path, "$GOROOT/") {
|
||||
r.path = goroot + "/" + r.path[len("$GOROOT/"):]
|
||||
}
|
||||
|
||||
cmd := exec.Command(*flagGoCmd, "build", "-o", "_compilebenchout_", r.path)
|
||||
cmd.Stdout = os.Stderr
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove("_compilebenchout_")
|
||||
info, err := os.Stat("_compilebenchout_")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out, err := exec.Command("size", "_compilebenchout_").CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("size: %v\n%s", err, out)
|
||||
}
|
||||
lines := strings.Split(string(out), "\n")
|
||||
if len(lines) < 2 {
|
||||
return fmt.Errorf("not enough output from size: %s", out)
|
||||
}
|
||||
f := strings.Fields(lines[1])
|
||||
if strings.HasPrefix(lines[0], "__TEXT") && len(f) >= 2 { // OS X
|
||||
fmt.Printf("%s 1 %s text-bytes %s data-bytes %v exe-bytes\n", name, f[0], f[1], info.Size())
|
||||
} else if strings.Contains(lines[0], "bss") && len(f) >= 3 {
|
||||
fmt.Printf("%s 1 %s text-bytes %s data-bytes %s bss-bytes %v exe-bytes\n", name, f[0], f[1], f[2], info.Size())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type compile struct{ dir string }
|
||||
|
||||
func (compile) long() bool { return false }
|
||||
|
||||
func (c compile) run(name string, count int) error {
|
||||
// Make sure dependencies needed by go tool compile are built.
|
||||
out, err := exec.Command(*flagGoCmd, "build", c.dir).CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("go build %s: %v\n%s", c.dir, err, out)
|
||||
}
|
||||
|
||||
// Find dir and source file list.
|
||||
pkg, err := goList(c.dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
importcfg, err := genImportcfgFile(c.dir, "", false) // TODO: pass compiler flags?
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If this package has assembly files, we'll need to pass a symabis
|
||||
// file to the compiler; call a helper to invoke the assembler
|
||||
// to do that.
|
||||
var symAbisFile string
|
||||
var asmIncFile string
|
||||
if len(pkg.SFiles) != 0 {
|
||||
symAbisFile = filepath.Join(pkg.Dir, "symabis")
|
||||
asmIncFile = filepath.Join(pkg.Dir, "go_asm.h")
|
||||
content := "\n"
|
||||
if err := os.WriteFile(asmIncFile, []byte(content), 0666); err != nil {
|
||||
return fmt.Errorf("os.WriteFile(%s) failed: %v", asmIncFile, err)
|
||||
}
|
||||
defer os.Remove(symAbisFile)
|
||||
defer os.Remove(asmIncFile)
|
||||
if err := genSymAbisFile(pkg, symAbisFile, pkg.Dir); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
args := []string{"-o", "_compilebench_.o", "-p", pkg.ImportPath}
|
||||
args = append(args, strings.Fields(*flagCompilerFlags)...)
|
||||
if symAbisFile != "" {
|
||||
args = append(args, "-symabis", symAbisFile)
|
||||
}
|
||||
if importcfg != "" {
|
||||
args = append(args, "-importcfg", importcfg)
|
||||
defer os.Remove(importcfg)
|
||||
}
|
||||
args = append(args, pkg.GoFiles...)
|
||||
if err := runBuildCmd(name, count, pkg.Dir, compiler, args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opath := pkg.Dir + "/_compilebench_.o"
|
||||
if *flagObj {
|
||||
// TODO(josharian): object files are big; just read enough to find what we seek.
|
||||
data, err := os.ReadFile(opath)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
// Find start of export data.
|
||||
i := bytes.Index(data, []byte("\n$$B\n")) + len("\n$$B\n")
|
||||
// Count bytes to end of export data.
|
||||
nexport := bytes.Index(data[i:], []byte("\n$$\n"))
|
||||
fmt.Printf(" %d object-bytes %d export-bytes", len(data), nexport)
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
os.Remove(opath)
|
||||
return nil
|
||||
}
|
||||
|
||||
type link struct{ dir, flags string }
|
||||
|
||||
func (link) long() bool { return false }
|
||||
|
||||
func (r link) run(name string, count int) error {
|
||||
if linker == "" {
|
||||
// No linker. Skip the test.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Build dependencies.
|
||||
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, "-ldflags="+ldflags, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(importcfg)
|
||||
|
||||
// Build the main package.
|
||||
pkg, err := goList(r.dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
args := []string{"-o", "_compilebench_.o", "-importcfg", importcfg}
|
||||
args = append(args, pkg.GoFiles...)
|
||||
if *flagTrace {
|
||||
fmt.Fprintf(os.Stderr, "running: %s %+v\n",
|
||||
compiler, args)
|
||||
}
|
||||
cmd := exec.Command(compiler, args...)
|
||||
cmd.Dir = pkg.Dir
|
||||
cmd.Stdout = os.Stderr
|
||||
cmd.Stderr = os.Stderr
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("compiling: %v", err)
|
||||
}
|
||||
defer os.Remove(pkg.Dir + "/_compilebench_.o")
|
||||
|
||||
// Link the main package.
|
||||
args = []string{"-o", "_compilebench_.exe", "-importcfg", importcfg}
|
||||
args = append(args, strings.Fields(*flagLinkerFlags)...)
|
||||
args = append(args, strings.Fields(r.flags)...)
|
||||
args = append(args, "_compilebench_.o")
|
||||
if err := runBuildCmd(name, count, pkg.Dir, linker, args); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println()
|
||||
defer os.Remove(pkg.Dir + "/_compilebench_.exe")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// runBuildCmd runs "tool args..." in dir, measures standard build
|
||||
// tool metrics, and prints a benchmark line. The caller may print
|
||||
// additional metrics and then must print a newline.
|
||||
//
|
||||
// This assumes tool accepts standard build tool flags like
|
||||
// -memprofilerate, -memprofile, and -cpuprofile.
|
||||
func runBuildCmd(name string, count int, dir, tool string, args []string) error {
|
||||
var preArgs []string
|
||||
if *flagMemprofilerate >= 0 {
|
||||
preArgs = append(preArgs, "-memprofilerate", fmt.Sprint(*flagMemprofilerate))
|
||||
}
|
||||
if *flagAlloc || *flagCpuprofile != "" || *flagMemprofile != "" {
|
||||
if *flagAlloc || *flagMemprofile != "" {
|
||||
preArgs = append(preArgs, "-memprofile", "_compilebench_.memprof")
|
||||
}
|
||||
if *flagCpuprofile != "" {
|
||||
preArgs = append(preArgs, "-cpuprofile", "_compilebench_.cpuprof")
|
||||
}
|
||||
}
|
||||
if *flagTrace {
|
||||
fmt.Fprintf(os.Stderr, "running: %s %+v\n",
|
||||
tool, append(preArgs, args...))
|
||||
}
|
||||
cmd := exec.Command(tool, append(preArgs, args...)...)
|
||||
cmd.Dir = dir
|
||||
cmd.Stdout = os.Stderr
|
||||
cmd.Stderr = os.Stderr
|
||||
start := time.Now()
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
end := time.Now()
|
||||
|
||||
haveAllocs, haveRSS := false, false
|
||||
var allocs, allocbytes, rssbytes int64
|
||||
if *flagAlloc || *flagMemprofile != "" {
|
||||
out, err := os.ReadFile(dir + "/_compilebench_.memprof")
|
||||
if err != nil {
|
||||
log.Print("cannot find memory profile after compilation")
|
||||
}
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
f := strings.Fields(line)
|
||||
if len(f) < 4 || f[0] != "#" || f[2] != "=" {
|
||||
continue
|
||||
}
|
||||
val, err := strconv.ParseInt(f[3], 0, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
haveAllocs = true
|
||||
switch f[1] {
|
||||
case "TotalAlloc":
|
||||
allocbytes = val
|
||||
case "Mallocs":
|
||||
allocs = val
|
||||
case "MaxRSS":
|
||||
haveRSS = true
|
||||
rssbytes = val
|
||||
}
|
||||
}
|
||||
if !haveAllocs {
|
||||
log.Println("missing stats in memprof (golang.org/issue/18641)")
|
||||
}
|
||||
|
||||
if *flagMemprofile != "" {
|
||||
outpath := *flagMemprofile
|
||||
if *flagCount != 1 {
|
||||
outpath = fmt.Sprintf("%s_%d", outpath, count)
|
||||
}
|
||||
if err := os.WriteFile(outpath, out, 0666); err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
}
|
||||
os.Remove(dir + "/_compilebench_.memprof")
|
||||
}
|
||||
|
||||
if *flagCpuprofile != "" {
|
||||
out, err := os.ReadFile(dir + "/_compilebench_.cpuprof")
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
outpath := *flagCpuprofile
|
||||
if *flagCount != 1 {
|
||||
outpath = fmt.Sprintf("%s_%d", outpath, count)
|
||||
}
|
||||
if err := os.WriteFile(outpath, out, 0666); err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
os.Remove(dir + "/_compilebench_.cpuprof")
|
||||
}
|
||||
|
||||
wallns := end.Sub(start).Nanoseconds()
|
||||
userns := cmd.ProcessState.UserTime().Nanoseconds()
|
||||
|
||||
fmt.Printf("%s 1 %d ns/op %d user-ns/op", name, wallns, userns)
|
||||
if haveAllocs {
|
||||
fmt.Printf(" %d B/op %d allocs/op", allocbytes, allocs)
|
||||
}
|
||||
if haveRSS {
|
||||
fmt.Printf(" %d maxRSS/op", rssbytes)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkCompilingRuntimeFlag(assembler string) error {
|
||||
td, err := os.MkdirTemp("", "asmsrcd")
|
||||
if err != nil {
|
||||
return fmt.Errorf("MkdirTemp failed: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(td)
|
||||
src := filepath.Join(td, "asm.s")
|
||||
obj := filepath.Join(td, "asm.o")
|
||||
const code = `
|
||||
TEXT ·foo(SB),$0-0
|
||||
RET
|
||||
`
|
||||
if err := os.WriteFile(src, []byte(code), 0644); err != nil {
|
||||
return fmt.Errorf("writing %s failed: %v", src, err)
|
||||
}
|
||||
|
||||
// Try compiling the assembly source file passing
|
||||
// -compiling-runtime; if it succeeds, then we'll need it
|
||||
// when doing assembly of the reflect package later on.
|
||||
// If it does not succeed, the assumption is that it's not
|
||||
// needed.
|
||||
args := []string{"-o", obj, "-p", "reflect", "-compiling-runtime", src}
|
||||
cmd := exec.Command(assembler, args...)
|
||||
cmd.Dir = td
|
||||
out, aerr := cmd.CombinedOutput()
|
||||
if aerr != nil {
|
||||
if strings.Contains(string(out), "flag provided but not defined: -compiling-runtime") {
|
||||
// flag not defined: assume we're using a recent assembler, so
|
||||
// don't use -compiling-runtime.
|
||||
return nil
|
||||
}
|
||||
// error is not flag-related; report it.
|
||||
return fmt.Errorf("problems invoking assembler with args %+v: error %v\n%s\n", args, aerr, out)
|
||||
}
|
||||
// asm invocation succeeded -- assume we need the flag.
|
||||
needCompilingRuntimeFlag = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// genSymAbisFile runs the assembler on the target package asm files
|
||||
// with "-gensymabis" to produce a symabis file that will feed into
|
||||
// the Go source compilation. This is fairly hacky in that if the
|
||||
// asm invocation convention changes it will need to be updated
|
||||
// (hopefully that will not be needed too frequently).
|
||||
func genSymAbisFile(pkg *Pkg, symAbisFile, incdir string) error {
|
||||
args := []string{"-gensymabis", "-o", symAbisFile,
|
||||
"-p", pkg.ImportPath,
|
||||
"-I", filepath.Join(goroot, "pkg", "include"),
|
||||
"-I", incdir,
|
||||
"-D", "GOOS_" + runtime.GOOS,
|
||||
"-D", "GOARCH_" + runtime.GOARCH}
|
||||
if pkg.ImportPath == "reflect" && needCompilingRuntimeFlag {
|
||||
args = append(args, "-compiling-runtime")
|
||||
}
|
||||
args = append(args, pkg.SFiles...)
|
||||
if *flagTrace {
|
||||
fmt.Fprintf(os.Stderr, "running: %s %+v\n",
|
||||
assembler, args)
|
||||
}
|
||||
cmd := exec.Command(assembler, args...)
|
||||
cmd.Dir = pkg.Dir
|
||||
cmd.Stdout = os.Stderr
|
||||
cmd.Stderr = os.Stderr
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("assembling to produce symabis file: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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, 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, flags, dir)
|
||||
cmd.Stderr = os.Stderr
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("go list -f %s %s: %v", need, dir, err)
|
||||
}
|
||||
// trim [ ]\n
|
||||
if len(out) < 3 || out[0] != '[' || out[len(out)-2] != ']' || out[len(out)-1] != '\n' {
|
||||
return "", fmt.Errorf("unexpected output from go list -f %s %s: %s", need, dir, out)
|
||||
}
|
||||
out = out[1 : len(out)-2]
|
||||
if len(out) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// build importcfg for imported packages
|
||||
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()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("generating importcfg for %s: %s: %v", dir, cmd, err)
|
||||
}
|
||||
|
||||
f, err := os.CreateTemp("", "importcfg")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("creating tmp importcfg file failed: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
if _, err := f.Write(out); err != nil {
|
||||
return "", fmt.Errorf("writing importcfg file %s failed: %v", f.Name(), err)
|
||||
}
|
||||
return f.Name(), nil
|
||||
}
|
||||
@@ -0,0 +1,648 @@
|
||||
// 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.
|
||||
|
||||
//go:build go1.20
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"golang.org/x/telemetry"
|
||||
"golang.org/x/tools/go/callgraph"
|
||||
"golang.org/x/tools/go/callgraph/rta"
|
||||
"golang.org/x/tools/go/packages"
|
||||
"golang.org/x/tools/go/ssa"
|
||||
"golang.org/x/tools/go/ssa/ssautil"
|
||||
"golang.org/x/tools/internal/typesinternal"
|
||||
)
|
||||
|
||||
//go:embed doc.go
|
||||
var doc string
|
||||
|
||||
// flags
|
||||
var (
|
||||
testFlag = flag.Bool("test", false, "include implicit test packages and executables")
|
||||
tagsFlag = flag.String("tags", "", "comma-separated list of extra build tags (see: go help buildconstraint)")
|
||||
|
||||
filterFlag = flag.String("filter", "<module>", "report only packages matching this regular expression (default: module of first package)")
|
||||
generatedFlag = flag.Bool("generated", false, "include dead functions in generated Go files")
|
||||
whyLiveFlag = flag.String("whylive", "", "show a path from main to the named function")
|
||||
formatFlag = flag.String("f", "", "format output records using template")
|
||||
jsonFlag = flag.Bool("json", false, "output JSON records")
|
||||
cpuProfile = flag.String("cpuprofile", "", "write CPU profile to this file")
|
||||
memProfile = flag.String("memprofile", "", "write memory profile to this file")
|
||||
)
|
||||
|
||||
func usage() {
|
||||
// Extract the content of the /* ... */ comment in doc.go.
|
||||
_, after, _ := strings.Cut(doc, "/*\n")
|
||||
doc, _, _ := strings.Cut(after, "*/")
|
||||
io.WriteString(flag.CommandLine.Output(), doc+`
|
||||
Flags:
|
||||
|
||||
`)
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
|
||||
func main() {
|
||||
telemetry.Start(telemetry.Config{ReportCrashes: true})
|
||||
|
||||
log.SetPrefix("deadcode: ")
|
||||
log.SetFlags(0) // no time prefix
|
||||
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
if len(flag.Args()) == 0 {
|
||||
usage()
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
if *cpuProfile != "" {
|
||||
f, err := os.Create(*cpuProfile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := pprof.StartCPUProfile(f); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// NB: profile won't be written in case of error.
|
||||
defer pprof.StopCPUProfile()
|
||||
}
|
||||
|
||||
if *memProfile != "" {
|
||||
f, err := os.Create(*memProfile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// NB: profile won't be written in case of error.
|
||||
defer func() {
|
||||
runtime.GC() // get up-to-date statistics
|
||||
if err := pprof.WriteHeapProfile(f); err != nil {
|
||||
log.Fatalf("Writing memory profile: %v", err)
|
||||
}
|
||||
f.Close()
|
||||
}()
|
||||
}
|
||||
|
||||
// Reject bad output options early.
|
||||
if *formatFlag != "" {
|
||||
if *jsonFlag {
|
||||
log.Fatalf("you cannot specify both -f=template and -json")
|
||||
}
|
||||
if _, err := template.New("deadcode").Parse(*formatFlag); err != nil {
|
||||
log.Fatalf("invalid -f: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Load, parse, and type-check the complete program(s).
|
||||
cfg := &packages.Config{
|
||||
BuildFlags: []string{"-tags=" + *tagsFlag},
|
||||
Mode: packages.LoadAllSyntax | packages.NeedModule,
|
||||
Tests: *testFlag,
|
||||
}
|
||||
initial, err := packages.Load(cfg, flag.Args()...)
|
||||
if err != nil {
|
||||
log.Fatalf("Load: %v", err)
|
||||
}
|
||||
if len(initial) == 0 {
|
||||
log.Fatalf("no packages")
|
||||
}
|
||||
if packages.PrintErrors(initial) > 0 {
|
||||
log.Fatalf("packages contain errors")
|
||||
}
|
||||
|
||||
// If -filter is unset, use first module (if available).
|
||||
if *filterFlag == "<module>" {
|
||||
if mod := initial[0].Module; mod != nil && mod.Path != "" {
|
||||
*filterFlag = "^" + regexp.QuoteMeta(mod.Path) + "\\b"
|
||||
} else {
|
||||
*filterFlag = "" // match any
|
||||
}
|
||||
}
|
||||
filter, err := regexp.Compile(*filterFlag)
|
||||
if err != nil {
|
||||
log.Fatalf("-filter: %v", err)
|
||||
}
|
||||
|
||||
// Create SSA-form program representation
|
||||
// and find main packages.
|
||||
prog, pkgs := ssautil.AllPackages(initial, ssa.InstantiateGenerics)
|
||||
prog.Build()
|
||||
|
||||
mains := ssautil.MainPackages(pkgs)
|
||||
if len(mains) == 0 {
|
||||
log.Fatalf("no main packages")
|
||||
}
|
||||
var roots []*ssa.Function
|
||||
for _, main := range mains {
|
||||
roots = append(roots, main.Func("init"), main.Func("main"))
|
||||
}
|
||||
|
||||
// Gather all source-level functions,
|
||||
// as the user interface is expressed in terms of them.
|
||||
//
|
||||
// We ignore synthetic wrappers, and nested functions. Literal
|
||||
// functions passed as arguments to other functions are of
|
||||
// course address-taken and there exists a dynamic call of
|
||||
// that signature, so when they are unreachable, it is
|
||||
// invariably because the parent is unreachable.
|
||||
var sourceFuncs []*ssa.Function
|
||||
generated := make(map[string]bool)
|
||||
packages.Visit(initial, nil, func(p *packages.Package) {
|
||||
for _, file := range p.Syntax {
|
||||
for _, decl := range file.Decls {
|
||||
if decl, ok := decl.(*ast.FuncDecl); ok {
|
||||
obj := p.TypesInfo.Defs[decl.Name].(*types.Func)
|
||||
fn := prog.FuncValue(obj)
|
||||
sourceFuncs = append(sourceFuncs, fn)
|
||||
}
|
||||
}
|
||||
|
||||
if isGenerated(file) {
|
||||
generated[p.Fset.File(file.Pos()).Name()] = true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Compute the reachabilty from main.
|
||||
// (Build a call graph only for -whylive.)
|
||||
res := rta.Analyze(roots, *whyLiveFlag != "")
|
||||
|
||||
// Subtle: the -test flag causes us to analyze test variants
|
||||
// such as "package p as compiled for p.test" or even "for q.test".
|
||||
// This leads to multiple distinct ssa.Function instances that
|
||||
// represent the same source declaration, and it is essentially
|
||||
// impossible to discover this from the SSA representation
|
||||
// (since it has lost the connection to go/packages.Package.ID).
|
||||
//
|
||||
// So, we de-duplicate such variants by position:
|
||||
// if any one of them is live, we consider all of them live.
|
||||
// (We use Position not Pos to avoid assuming that files common
|
||||
// to packages "p" and "p [p.test]" were parsed only once.)
|
||||
reachablePosn := make(map[token.Position]bool)
|
||||
for fn := range res.Reachable {
|
||||
if fn.Pos().IsValid() || fn.Name() == "init" {
|
||||
reachablePosn[prog.Fset.Position(fn.Pos())] = true
|
||||
}
|
||||
}
|
||||
|
||||
// The -whylive=fn flag causes deadcode to explain why a function
|
||||
// is not dead, by showing a path to it from some root.
|
||||
if *whyLiveFlag != "" {
|
||||
targets := make(map[*ssa.Function]bool)
|
||||
for _, fn := range sourceFuncs {
|
||||
if prettyName(fn, true) == *whyLiveFlag {
|
||||
targets[fn] = true
|
||||
}
|
||||
}
|
||||
if len(targets) == 0 {
|
||||
// Function is not part of the program.
|
||||
//
|
||||
// TODO(adonovan): improve the UX here in case
|
||||
// of spelling or syntax mistakes. Some ideas:
|
||||
// - a cmd/callgraph command to enumerate
|
||||
// available functions.
|
||||
// - a deadcode -live flag to compute the complement.
|
||||
// - a syntax hint: example.com/pkg.Func or (example.com/pkg.Type).Method
|
||||
// - report the element of AllFunctions with the smallest
|
||||
// Levenshtein distance from *whyLiveFlag.
|
||||
// - permit -whylive=regexp. But beware of spurious
|
||||
// matches (e.g. fmt.Print matches fmt.Println)
|
||||
// and the annoyance of having to quote parens (*T).f.
|
||||
log.Fatalf("function %q not found in program", *whyLiveFlag)
|
||||
}
|
||||
|
||||
// Opt: remove the unreachable ones.
|
||||
for fn := range targets {
|
||||
if !reachablePosn[prog.Fset.Position(fn.Pos())] {
|
||||
delete(targets, fn)
|
||||
}
|
||||
}
|
||||
if len(targets) == 0 {
|
||||
log.Fatalf("function %s is dead code", *whyLiveFlag)
|
||||
}
|
||||
|
||||
res.CallGraph.DeleteSyntheticNodes() // inline synthetic wrappers (except inits)
|
||||
root, path := pathSearch(roots, res, targets)
|
||||
if root == nil {
|
||||
// RTA doesn't add callgraph edges for reflective calls.
|
||||
log.Fatalf("%s is reachable only through reflection", *whyLiveFlag)
|
||||
}
|
||||
if len(path) == 0 {
|
||||
// No edges => one of the targets is a root.
|
||||
// Rather than (confusingly) print nothing, make this an error.
|
||||
log.Fatalf("%s is a root", root.Func)
|
||||
}
|
||||
|
||||
// Build a list of jsonEdge records
|
||||
// to print as -json or -f=template.
|
||||
var edges []any
|
||||
for _, edge := range path {
|
||||
edges = append(edges, jsonEdge{
|
||||
Initial: cond(len(edges) == 0, prettyName(edge.Caller.Func, true), ""),
|
||||
Kind: cond(isStaticCall(edge), "static", "dynamic"),
|
||||
Position: toJSONPosition(prog.Fset.Position(edge.Pos())),
|
||||
Callee: prettyName(edge.Callee.Func, true),
|
||||
})
|
||||
}
|
||||
format := `{{if .Initial}}{{printf "%19s%s\n" "" .Initial}}{{end}}{{printf "%8s@L%.4d --> %s" .Kind .Position.Line .Callee}}`
|
||||
if *formatFlag != "" {
|
||||
format = *formatFlag
|
||||
}
|
||||
printObjects(format, edges)
|
||||
return
|
||||
}
|
||||
|
||||
// Group unreachable functions by package path.
|
||||
byPkgPath := make(map[string]map[*ssa.Function]bool)
|
||||
for _, fn := range sourceFuncs {
|
||||
posn := prog.Fset.Position(fn.Pos())
|
||||
|
||||
if !reachablePosn[posn] {
|
||||
reachablePosn[posn] = true // suppress dups with same pos
|
||||
|
||||
pkgpath := fn.Pkg.Pkg.Path()
|
||||
m, ok := byPkgPath[pkgpath]
|
||||
if !ok {
|
||||
m = make(map[*ssa.Function]bool)
|
||||
byPkgPath[pkgpath] = m
|
||||
}
|
||||
m[fn] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Build array of jsonPackage objects.
|
||||
var packages []any
|
||||
pkgpaths := keys(byPkgPath)
|
||||
sort.Strings(pkgpaths)
|
||||
for _, pkgpath := range pkgpaths {
|
||||
if !filter.MatchString(pkgpath) {
|
||||
continue
|
||||
}
|
||||
|
||||
m := byPkgPath[pkgpath]
|
||||
|
||||
// Print functions that appear within the same file in
|
||||
// declaration order. This tends to keep related
|
||||
// methods such as (T).Marshal and (*T).Unmarshal
|
||||
// together better than sorting.
|
||||
fns := keys(m)
|
||||
sort.Slice(fns, func(i, j int) bool {
|
||||
xposn := prog.Fset.Position(fns[i].Pos())
|
||||
yposn := prog.Fset.Position(fns[j].Pos())
|
||||
if xposn.Filename != yposn.Filename {
|
||||
return xposn.Filename < yposn.Filename
|
||||
}
|
||||
return xposn.Line < yposn.Line
|
||||
})
|
||||
|
||||
var functions []jsonFunction
|
||||
for _, fn := range fns {
|
||||
posn := prog.Fset.Position(fn.Pos())
|
||||
|
||||
// Without -generated, skip functions declared in
|
||||
// generated Go files.
|
||||
// (Functions called by them may still be reported.)
|
||||
gen := generated[posn.Filename]
|
||||
if gen && !*generatedFlag {
|
||||
continue
|
||||
}
|
||||
|
||||
functions = append(functions, jsonFunction{
|
||||
Name: prettyName(fn, false),
|
||||
Position: toJSONPosition(posn),
|
||||
Generated: gen,
|
||||
})
|
||||
}
|
||||
if len(functions) > 0 {
|
||||
packages = append(packages, jsonPackage{
|
||||
Name: fns[0].Pkg.Pkg.Name(),
|
||||
Path: pkgpath,
|
||||
Funcs: functions,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Default line-oriented format: "a/b/c.go:1:2: unreachable func: T.f"
|
||||
format := `{{range .Funcs}}{{printf "%s: unreachable func: %s\n" .Position .Name}}{{end}}`
|
||||
if *formatFlag != "" {
|
||||
format = *formatFlag
|
||||
}
|
||||
printObjects(format, packages)
|
||||
}
|
||||
|
||||
// prettyName is a fork of Function.String designed to reduce
|
||||
// go/ssa's fussy punctuation symbols, e.g. "(*pkg.T).F" -> "pkg.T.F".
|
||||
//
|
||||
// It only works for functions that remain after
|
||||
// callgraph.Graph.DeleteSyntheticNodes: source-level named functions
|
||||
// and methods, their anonymous functions, and synthetic package
|
||||
// initializers.
|
||||
func prettyName(fn *ssa.Function, qualified bool) string {
|
||||
var buf strings.Builder
|
||||
|
||||
// optional package qualifier
|
||||
if qualified && fn.Pkg != nil {
|
||||
fmt.Fprintf(&buf, "%s.", fn.Pkg.Pkg.Path())
|
||||
}
|
||||
|
||||
var format func(*ssa.Function)
|
||||
format = func(fn *ssa.Function) {
|
||||
// anonymous?
|
||||
if fn.Parent() != nil {
|
||||
format(fn.Parent())
|
||||
i := index(fn.Parent().AnonFuncs, fn)
|
||||
fmt.Fprintf(&buf, "$%d", i+1)
|
||||
return
|
||||
}
|
||||
|
||||
// method receiver?
|
||||
if recv := fn.Signature.Recv(); recv != nil {
|
||||
_, named := typesinternal.ReceiverNamed(recv)
|
||||
buf.WriteString(named.Obj().Name())
|
||||
buf.WriteByte('.')
|
||||
}
|
||||
|
||||
// function/method name
|
||||
buf.WriteString(fn.Name())
|
||||
}
|
||||
format(fn)
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// printObjects formats an array of objects, either as JSON or using a
|
||||
// template, following the manner of 'go list (-json|-f=template)'.
|
||||
func printObjects(format string, objects []any) {
|
||||
if *jsonFlag {
|
||||
out, err := json.MarshalIndent(objects, "", "\t")
|
||||
if err != nil {
|
||||
log.Fatalf("internal error: %v", err)
|
||||
}
|
||||
os.Stdout.Write(out)
|
||||
return
|
||||
}
|
||||
|
||||
// -f=template. Parse can't fail: we checked it earlier.
|
||||
tmpl := template.Must(template.New("deadcode").Parse(format))
|
||||
for _, object := range objects {
|
||||
var buf bytes.Buffer
|
||||
if err := tmpl.Execute(&buf, object); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if n := buf.Len(); n == 0 || buf.Bytes()[n-1] != '\n' {
|
||||
buf.WriteByte('\n')
|
||||
}
|
||||
os.Stdout.Write(buf.Bytes())
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(adonovan): use go1.21's ast.IsGenerated.
|
||||
|
||||
// isGenerated reports whether the file was generated by a program,
|
||||
// not handwritten, by detecting the special comment described
|
||||
// at https://go.dev/s/generatedcode.
|
||||
//
|
||||
// The syntax tree must have been parsed with the ParseComments flag.
|
||||
// Example:
|
||||
//
|
||||
// f, err := parser.ParseFile(fset, filename, src, parser.ParseComments|parser.PackageClauseOnly)
|
||||
// if err != nil { ... }
|
||||
// gen := ast.IsGenerated(f)
|
||||
func isGenerated(file *ast.File) bool {
|
||||
_, ok := generator(file)
|
||||
return ok
|
||||
}
|
||||
|
||||
func generator(file *ast.File) (string, bool) {
|
||||
for _, group := range file.Comments {
|
||||
for _, comment := range group.List {
|
||||
if comment.Pos() > file.Package {
|
||||
break // after package declaration
|
||||
}
|
||||
// opt: check Contains first to avoid unnecessary array allocation in Split.
|
||||
const prefix = "// Code generated "
|
||||
if strings.Contains(comment.Text, prefix) {
|
||||
for _, line := range strings.Split(comment.Text, "\n") {
|
||||
if rest, ok := strings.CutPrefix(line, prefix); ok {
|
||||
if gen, ok := strings.CutSuffix(rest, " DO NOT EDIT."); ok {
|
||||
return gen, true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// pathSearch returns the shortest path from one of the roots to one
|
||||
// of the targets (along with the root itself), or zero if no path was found.
|
||||
func pathSearch(roots []*ssa.Function, res *rta.Result, targets map[*ssa.Function]bool) (*callgraph.Node, []*callgraph.Edge) {
|
||||
// Search breadth-first (for shortest path) from the root.
|
||||
//
|
||||
// We don't use the virtual CallGraph.Root node as we wish to
|
||||
// choose the order in which we search entrypoints:
|
||||
// non-test packages before test packages,
|
||||
// main functions before init functions.
|
||||
|
||||
// Sort roots into preferred order.
|
||||
importsTesting := func(fn *ssa.Function) bool {
|
||||
isTesting := func(p *types.Package) bool { return p.Path() == "testing" }
|
||||
return containsFunc(fn.Pkg.Pkg.Imports(), isTesting)
|
||||
}
|
||||
sort.Slice(roots, func(i, j int) bool {
|
||||
x, y := roots[i], roots[j]
|
||||
xtest := importsTesting(x)
|
||||
ytest := importsTesting(y)
|
||||
if xtest != ytest {
|
||||
return !xtest // non-tests before tests
|
||||
}
|
||||
xinit := x.Name() == "init"
|
||||
yinit := y.Name() == "init"
|
||||
if xinit != yinit {
|
||||
return !xinit // mains before inits
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
search := func(allowDynamic bool) (*callgraph.Node, []*callgraph.Edge) {
|
||||
// seen maps each encountered node to its predecessor on the
|
||||
// path to a root node, or to nil for root itself.
|
||||
seen := make(map[*callgraph.Node]*callgraph.Edge)
|
||||
bfs := func(root *callgraph.Node) []*callgraph.Edge {
|
||||
queue := []*callgraph.Node{root}
|
||||
seen[root] = nil
|
||||
for len(queue) > 0 {
|
||||
node := queue[0]
|
||||
queue = queue[1:]
|
||||
|
||||
// found a path?
|
||||
if targets[node.Func] {
|
||||
path := []*callgraph.Edge{} // non-nil in case len(path)=0
|
||||
for {
|
||||
edge := seen[node]
|
||||
if edge == nil {
|
||||
reverse(path)
|
||||
return path
|
||||
}
|
||||
path = append(path, edge)
|
||||
node = edge.Caller
|
||||
}
|
||||
}
|
||||
|
||||
for _, edge := range node.Out {
|
||||
if allowDynamic || isStaticCall(edge) {
|
||||
if _, ok := seen[edge.Callee]; !ok {
|
||||
seen[edge.Callee] = edge
|
||||
queue = append(queue, edge.Callee)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
for _, rootFn := range roots {
|
||||
root := res.CallGraph.Nodes[rootFn]
|
||||
if root == nil {
|
||||
// Missing call graph node for root.
|
||||
// TODO(adonovan): seems like a bug in rta.
|
||||
continue
|
||||
}
|
||||
if path := bfs(root); path != nil {
|
||||
return root, path
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
for _, allowDynamic := range []bool{false, true} {
|
||||
if root, path := search(allowDynamic); path != nil {
|
||||
return root, path
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// -- utilities --
|
||||
|
||||
func isStaticCall(edge *callgraph.Edge) bool {
|
||||
return edge.Site != nil && edge.Site.Common().StaticCallee() != nil
|
||||
}
|
||||
|
||||
var cwd, _ = os.Getwd()
|
||||
|
||||
func toJSONPosition(posn token.Position) jsonPosition {
|
||||
// Use cwd-relative filename if possible.
|
||||
filename := posn.Filename
|
||||
if rel, err := filepath.Rel(cwd, filename); err == nil && !strings.HasPrefix(rel, "..") {
|
||||
filename = rel
|
||||
}
|
||||
|
||||
return jsonPosition{filename, posn.Line, posn.Column}
|
||||
}
|
||||
|
||||
func cond[T any](cond bool, t, f T) T {
|
||||
if cond {
|
||||
return t
|
||||
} else {
|
||||
return f
|
||||
}
|
||||
}
|
||||
|
||||
// -- output protocol (for JSON or text/template) --
|
||||
|
||||
// Keep in sync with doc comment!
|
||||
|
||||
type jsonFunction struct {
|
||||
Name string // name (sans package qualifier)
|
||||
Position jsonPosition // file/line/column of declaration
|
||||
Generated bool // function is declared in a generated .go file
|
||||
}
|
||||
|
||||
func (f jsonFunction) String() string { return f.Name }
|
||||
|
||||
type jsonPackage struct {
|
||||
Name string // declared name
|
||||
Path string // full import path
|
||||
Funcs []jsonFunction // non-empty list of package's dead functions
|
||||
}
|
||||
|
||||
func (p jsonPackage) String() string { return p.Path }
|
||||
|
||||
// The Initial and Callee names are package-qualified.
|
||||
type jsonEdge struct {
|
||||
Initial string `json:",omitempty"` // initial entrypoint (main or init); first edge only
|
||||
Kind string // = static | dynamic
|
||||
Position jsonPosition
|
||||
Callee string
|
||||
}
|
||||
|
||||
type jsonPosition struct {
|
||||
File string
|
||||
Line, Col int
|
||||
}
|
||||
|
||||
func (p jsonPosition) String() string {
|
||||
return fmt.Sprintf("%s:%d:%d", p.File, p.Line, p.Col)
|
||||
}
|
||||
|
||||
// -- from the future --
|
||||
|
||||
// TODO(adonovan): use go1.22's slices and maps packages.
|
||||
|
||||
func containsFunc[S ~[]E, E any](s S, f func(E) bool) bool {
|
||||
return indexFunc(s, f) >= 0
|
||||
}
|
||||
|
||||
func indexFunc[S ~[]E, E any](s S, f func(E) bool) int {
|
||||
for i := range s {
|
||||
if f(s[i]) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func index[S ~[]E, E comparable](s S, v E) int {
|
||||
for i := range s {
|
||||
if v == s[i] {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func reverse[S ~[]E, E any](s S) {
|
||||
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
|
||||
s[i], s[j] = s[j], s[i]
|
||||
}
|
||||
}
|
||||
|
||||
func keys[M ~map[K]V, K comparable, V any](m M) []K {
|
||||
r := make([]K, 0, len(m))
|
||||
for k := range m {
|
||||
r = append(r, k)
|
||||
}
|
||||
return r
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
// 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.
|
||||
|
||||
//go:build go1.20
|
||||
|
||||
package main_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/internal/testenv"
|
||||
"golang.org/x/tools/txtar"
|
||||
)
|
||||
|
||||
// Test runs the deadcode command on each scenario
|
||||
// described by a testdata/*.txtar file.
|
||||
func Test(t *testing.T) {
|
||||
testenv.NeedsTool(t, "go")
|
||||
if runtime.GOOS == "android" {
|
||||
t.Skipf("the dependencies are not available on android")
|
||||
}
|
||||
|
||||
exe := buildDeadcode(t)
|
||||
|
||||
matches, err := filepath.Glob("testdata/*.txtar")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, filename := range matches {
|
||||
filename := filename
|
||||
t.Run(filename, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ar, err := txtar.ParseFile(filename)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Write the archive files to the temp directory.
|
||||
tmpdir := t.TempDir()
|
||||
for _, f := range ar.Files {
|
||||
filename := filepath.Join(tmpdir, f.Name)
|
||||
if err := os.MkdirAll(filepath.Dir(filename), 0777); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filename, f.Data, 0666); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse archive comment as directives of these forms:
|
||||
//
|
||||
// [!]deadcode args... command-line arguments
|
||||
// [!]want arg expected/unwanted string in output (or stderr)
|
||||
//
|
||||
// Args may be Go-quoted strings.
|
||||
type testcase struct {
|
||||
linenum int
|
||||
args []string
|
||||
wantErr bool
|
||||
want map[string]bool // string -> sense
|
||||
}
|
||||
var cases []*testcase
|
||||
var current *testcase
|
||||
for i, line := range strings.Split(string(ar.Comment), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || line[0] == '#' {
|
||||
continue // skip blanks and comments
|
||||
}
|
||||
|
||||
words, err := words(line)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot break line into words: %v (%s)", err, line)
|
||||
}
|
||||
switch kind := words[0]; kind {
|
||||
case "deadcode", "!deadcode":
|
||||
current = &testcase{
|
||||
linenum: i + 1,
|
||||
want: make(map[string]bool),
|
||||
args: words[1:],
|
||||
wantErr: kind[0] == '!',
|
||||
}
|
||||
cases = append(cases, current)
|
||||
case "want", "!want":
|
||||
if current == nil {
|
||||
t.Fatalf("'want' directive must be after 'deadcode'")
|
||||
}
|
||||
if len(words) != 2 {
|
||||
t.Fatalf("'want' directive needs argument <<%s>>", line)
|
||||
}
|
||||
current.want[words[1]] = kind[0] != '!'
|
||||
default:
|
||||
t.Fatalf("%s: invalid directive %q", filename, kind)
|
||||
}
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(fmt.Sprintf("L%d", tc.linenum), func(t *testing.T) {
|
||||
// Run the command.
|
||||
cmd := exec.Command(exe, tc.args...)
|
||||
cmd.Stdout = new(bytes.Buffer)
|
||||
cmd.Stderr = new(bytes.Buffer)
|
||||
cmd.Dir = tmpdir
|
||||
cmd.Env = append(os.Environ(), "GOPROXY=", "GO111MODULE=on")
|
||||
var got string
|
||||
if err := cmd.Run(); err != nil {
|
||||
if !tc.wantErr {
|
||||
t.Fatalf("deadcode failed: %v (stderr=%s)", err, cmd.Stderr)
|
||||
}
|
||||
got = fmt.Sprint(cmd.Stderr)
|
||||
} else {
|
||||
if tc.wantErr {
|
||||
t.Fatalf("deadcode succeeded unexpectedly (stdout=%s)", cmd.Stdout)
|
||||
}
|
||||
got = fmt.Sprint(cmd.Stdout)
|
||||
}
|
||||
|
||||
// Check each want directive.
|
||||
for str, sense := range tc.want {
|
||||
ok := true
|
||||
if strings.Contains(got, str) != sense {
|
||||
if sense {
|
||||
t.Errorf("missing %q", str)
|
||||
} else {
|
||||
t.Errorf("unwanted %q", str)
|
||||
}
|
||||
ok = false
|
||||
}
|
||||
if !ok {
|
||||
t.Errorf("got: <<%s>>", got)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// buildDeadcode builds the deadcode executable.
|
||||
// It returns its path, and a cleanup function.
|
||||
func buildDeadcode(t *testing.T) string {
|
||||
bin := filepath.Join(t.TempDir(), "deadcode")
|
||||
if runtime.GOOS == "windows" {
|
||||
bin += ".exe"
|
||||
}
|
||||
cmd := exec.Command("go", "build", "-o", bin)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("Building deadcode: %v\n%s", err, out)
|
||||
}
|
||||
return bin
|
||||
}
|
||||
|
||||
// words breaks a string into words, respecting
|
||||
// Go string quotations around words with spaces.
|
||||
func words(s string) ([]string, error) {
|
||||
var words []string
|
||||
for s != "" {
|
||||
s = strings.TrimSpace(s)
|
||||
var word string
|
||||
if s[0] == '"' || s[0] == '`' {
|
||||
prefix, err := strconv.QuotedPrefix(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s = s[len(prefix):]
|
||||
word, _ = strconv.Unquote(prefix)
|
||||
} else {
|
||||
prefix, rest, _ := strings.Cut(s, " ")
|
||||
s = rest
|
||||
word = prefix
|
||||
}
|
||||
words = append(words, word)
|
||||
}
|
||||
return words, nil
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
// 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 deadcode command reports unreachable functions in Go programs.
|
||||
|
||||
Usage: deadcode [flags] package...
|
||||
|
||||
The deadcode command loads a Go program from source then uses Rapid
|
||||
Type Analysis (RTA) to build a call graph of all the functions
|
||||
reachable from the program's main function. Any functions that are not
|
||||
reachable are reported as dead code, grouped by package.
|
||||
|
||||
Packages are expressed in the notation of 'go list' (or other
|
||||
underlying build system if you are using an alternative
|
||||
golang.org/x/go/packages driver). Only executable (main) packages are
|
||||
considered starting points for the analysis.
|
||||
|
||||
The -test flag causes it to analyze test executables too. Tests
|
||||
sometimes make use of functions that would otherwise appear to be dead
|
||||
code, and public API functions reported as dead with -test indicate
|
||||
possible gaps in your test coverage. Bear in mind that an Example test
|
||||
function without an "Output:" comment is merely documentation:
|
||||
it is dead code, and does not contribute coverage.
|
||||
|
||||
The -filter flag restricts results to packages that match the provided
|
||||
regular expression; its default value is the module name of the first
|
||||
package. Use -filter= to display all results.
|
||||
|
||||
Example: show all dead code within the gopls module:
|
||||
|
||||
$ deadcode -test golang.org/x/tools/gopls/...
|
||||
|
||||
The analysis can soundly analyze dynamic calls though func values,
|
||||
interface methods, and reflection. However, it does not currently
|
||||
understand the aliasing created by //go:linkname directives, so it
|
||||
will fail to recognize that calls to a linkname-annotated function
|
||||
with no body in fact dispatch to the function named in the annotation.
|
||||
This may result in the latter function being spuriously reported as dead.
|
||||
|
||||
By default, the tool does not report dead functions in generated files,
|
||||
as determined by the special comment described in
|
||||
https://go.dev/s/generatedcode. Use the -generated flag to include them.
|
||||
|
||||
In any case, just because a function is reported as dead does not mean
|
||||
it is unconditionally safe to delete it. For example, a dead function
|
||||
may be referenced by another dead function, and a dead method may be
|
||||
required to satisfy an interface that is never called.
|
||||
Some judgement is required.
|
||||
|
||||
The analysis is valid only for a single GOOS/GOARCH/-tags configuration,
|
||||
so a function reported as dead may be live in a different configuration.
|
||||
Consider running the tool once for each configuration of interest.
|
||||
Consider using a line-oriented output format (see below) to make it
|
||||
easier to compute the intersection of results across all runs.
|
||||
|
||||
# Output
|
||||
|
||||
The command supports three output formats.
|
||||
|
||||
With no flags, the command prints the name and location of each dead
|
||||
function in the form of a typical compiler diagnostic, for example:
|
||||
|
||||
$ deadcode -f='{{range .Funcs}}{{println .Position}}{{end}}' -test ./gopls/...
|
||||
gopls/internal/protocol/command.go:1206:6: unreachable func: openClientEditor
|
||||
gopls/internal/template/parse.go:414:18: unreachable func: Parsed.WriteNode
|
||||
gopls/internal/template/parse.go:419:18: unreachable func: wrNode.writeNode
|
||||
|
||||
With the -json flag, the command prints an array of Package
|
||||
objects, as defined by the JSON schema (see below).
|
||||
|
||||
With the -f=template flag, the command executes the specified template
|
||||
on each Package record. So, this template shows dead functions grouped
|
||||
by package:
|
||||
|
||||
$ deadcode -f='{{println .Path}}{{range .Funcs}}{{printf "\t%s\n" .Name}}{{end}}{{println}}' -test ./gopls/...
|
||||
golang.org/x/tools/gopls/internal/lsp
|
||||
openClientEditor
|
||||
|
||||
golang.org/x/tools/gopls/internal/template
|
||||
Parsed.WriteNode
|
||||
wrNode.writeNode
|
||||
|
||||
# Why is a function not dead?
|
||||
|
||||
The -whylive=function flag explain why the named function is not dead
|
||||
by showing an arbitrary shortest path to it from one of the main functions.
|
||||
(To enumerate the functions in a program, or for more sophisticated
|
||||
call graph queries, use golang.org/x/tools/cmd/callgraph.)
|
||||
|
||||
Fully static call paths are preferred over paths involving dynamic
|
||||
edges, even if longer. Paths starting from a non-test package are
|
||||
preferred over those from tests. Paths from main functions are
|
||||
preferred over paths from init functions.
|
||||
|
||||
The result is a list of Edge objects (see JSON schema below).
|
||||
Again, the -json and -f=template flags may be used to control
|
||||
the formatting of the list of Edge objects.
|
||||
The default format shows, for each edge in the path, whether the call
|
||||
is static or dynamic, and its source line number. For example:
|
||||
|
||||
$ deadcode -whylive=bytes.Buffer.String -test ./cmd/deadcode/...
|
||||
golang.org/x/tools/cmd/deadcode.main
|
||||
static@L0117 --> golang.org/x/tools/go/packages.Load
|
||||
static@L0262 --> golang.org/x/tools/go/packages.defaultDriver
|
||||
static@L0305 --> golang.org/x/tools/go/packages.goListDriver
|
||||
static@L0153 --> golang.org/x/tools/go/packages.goListDriver$1
|
||||
static@L0154 --> golang.org/x/tools/go/internal/packagesdriver.GetSizesForArgsGolist
|
||||
static@L0044 --> bytes.Buffer.String
|
||||
|
||||
# JSON schema
|
||||
|
||||
type Package struct {
|
||||
Name string // declared name
|
||||
Path string // full import path
|
||||
Funcs []Function // list of dead functions within it
|
||||
}
|
||||
|
||||
type Function struct {
|
||||
Name string // name (sans package qualifier)
|
||||
Position Position // file/line/column of function declaration
|
||||
Generated bool // function is declared in a generated .go file
|
||||
}
|
||||
|
||||
type Edge struct {
|
||||
Initial string // initial entrypoint (main or init); first edge only
|
||||
Kind string // = static | dynamic
|
||||
Position Position // file/line/column of call site
|
||||
Callee string // target of the call
|
||||
}
|
||||
|
||||
type Position struct {
|
||||
File string // name of file
|
||||
Line, Col int // line and byte index, both 1-based
|
||||
}
|
||||
*/
|
||||
package main
|
||||
@@ -0,0 +1,38 @@
|
||||
# Test of basic functionality.
|
||||
|
||||
deadcode -filter= example.com
|
||||
|
||||
want "T.Goodbye"
|
||||
want "T.Goodbye2"
|
||||
want "T.Goodbye3"
|
||||
!want "T.Hello"
|
||||
want "unreferenced"
|
||||
|
||||
want "Scanf"
|
||||
want "Printf"
|
||||
!want "Println"
|
||||
|
||||
-- go.mod --
|
||||
module example.com
|
||||
go 1.18
|
||||
|
||||
-- main.go --
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
type T int
|
||||
|
||||
func main() {
|
||||
var x T
|
||||
x.Hello()
|
||||
}
|
||||
|
||||
func (T) Hello() { fmt.Println("hello") }
|
||||
func (T) Goodbye() { fmt.Println("goodbye") }
|
||||
func (*T) Goodbye2() { fmt.Println("goodbye2") }
|
||||
func (*A) Goodbye3() { fmt.Println("goodbye3") }
|
||||
|
||||
type A = T
|
||||
|
||||
func unreferenced() {}
|
||||
@@ -0,0 +1,39 @@
|
||||
# Test of -filter flag.
|
||||
|
||||
deadcode -filter=other.net example.com
|
||||
|
||||
want `other.net`
|
||||
want `Dead`
|
||||
!want `Live`
|
||||
|
||||
!want `example.com`
|
||||
!want `unreferenced`
|
||||
|
||||
-- go.work --
|
||||
use example.com
|
||||
use other.net
|
||||
|
||||
-- example.com/go.mod --
|
||||
module example.com
|
||||
go 1.18
|
||||
|
||||
-- example.com/main.go --
|
||||
package main
|
||||
|
||||
import "other.net"
|
||||
|
||||
func main() {
|
||||
other.Live()
|
||||
}
|
||||
|
||||
func unreferenced() {}
|
||||
|
||||
-- other.net/go.mod --
|
||||
module other.net
|
||||
go 1.18
|
||||
|
||||
-- other.net/other.go --
|
||||
package other
|
||||
|
||||
func Live() {}
|
||||
func Dead() {}
|
||||
@@ -0,0 +1,28 @@
|
||||
# Test of -generated flag output.
|
||||
|
||||
deadcode "-f={{range .Funcs}}{{$.Name}}.{{.Name}}{{end}}" example.com
|
||||
!want "main.main"
|
||||
want "main.Dead1"
|
||||
!want "main.Dead2"
|
||||
|
||||
deadcode "-f={{range .Funcs}}{{$.Name}}.{{.Name}}{{end}}" -generated example.com
|
||||
!want "main.main"
|
||||
want "main.Dead1"
|
||||
want "main.Dead2"
|
||||
|
||||
-- go.mod --
|
||||
module example.com
|
||||
go 1.18
|
||||
|
||||
-- main.go --
|
||||
package main
|
||||
|
||||
func main() {}
|
||||
func Dead1() {}
|
||||
|
||||
-- gen.go --
|
||||
// Code generated by hand. DO NOT EDIT.
|
||||
|
||||
package main
|
||||
|
||||
func Dead2() {}
|
||||
@@ -0,0 +1,44 @@
|
||||
# Regression test for issue 65915: the enumeration of source-level
|
||||
# functions used the flawed ssautil.AllFunctions, causing it to
|
||||
# miss some unexported ones.
|
||||
|
||||
deadcode -filter= example.com
|
||||
|
||||
want "unreachable func: example.UnUsed"
|
||||
want "unreachable func: example.unUsed"
|
||||
want "unreachable func: PublicExample.UnUsed"
|
||||
want "unreachable func: PublicExample.unUsed"
|
||||
|
||||
-- go.mod --
|
||||
module example.com
|
||||
go 1.18
|
||||
|
||||
-- main.go --
|
||||
package main
|
||||
|
||||
type example struct{}
|
||||
|
||||
func (e example) UnUsed() {}
|
||||
|
||||
func (e example) Used() {}
|
||||
|
||||
func (e example) unUsed() {}
|
||||
|
||||
func (e example) used() {}
|
||||
|
||||
type PublicExample struct{}
|
||||
|
||||
func (p PublicExample) UnUsed() {}
|
||||
|
||||
func (p PublicExample) Used() {}
|
||||
|
||||
func (p PublicExample) unUsed() {}
|
||||
|
||||
func (p PublicExample) used() {}
|
||||
|
||||
func main() {
|
||||
example{}.Used()
|
||||
example{}.used()
|
||||
PublicExample{}.Used()
|
||||
PublicExample{}.used()
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
# Test of -whylive with reflective call
|
||||
# (regression test for golang/go#67915).
|
||||
|
||||
# The live function is reached via reflection:
|
||||
|
||||
deadcode example.com
|
||||
want "unreachable func: dead"
|
||||
!want "unreachable func: live"
|
||||
|
||||
# Reflective calls have Edge.Site=nil, which formerly led to a crash
|
||||
# when -whylive would compute its position. Now it has NoPos.
|
||||
|
||||
deadcode -whylive=example.com.live example.com
|
||||
want " example.com.main"
|
||||
want " static@L0006 --> reflect.Value.Call"
|
||||
want "dynamic@L0000 --> example.com.live"
|
||||
|
||||
-- go.mod --
|
||||
module example.com
|
||||
go 1.18
|
||||
|
||||
-- main.go --
|
||||
package main
|
||||
|
||||
import "reflect"
|
||||
|
||||
func main() {
|
||||
reflect.ValueOf(live).Call(nil)
|
||||
}
|
||||
|
||||
func live() {
|
||||
println("hello")
|
||||
}
|
||||
|
||||
func dead() {
|
||||
println("goodbye")
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
# Very minimal test of -json flag.
|
||||
|
||||
deadcode -json example.com/p
|
||||
|
||||
want `"Path": "example.com/p",`
|
||||
want `"Name": "DeadFunc",`
|
||||
want `"Generated": false`
|
||||
want `"Line": 5,`
|
||||
want `"Col": 6`
|
||||
|
||||
-- go.mod --
|
||||
module example.com
|
||||
go 1.18
|
||||
|
||||
-- p/p.go --
|
||||
package main
|
||||
|
||||
func main() {}
|
||||
|
||||
func DeadFunc() {}
|
||||
|
||||
type T int
|
||||
func (*T) DeadMethod() {}
|
||||
@@ -0,0 +1,32 @@
|
||||
# Test of line-oriented output.
|
||||
|
||||
deadcode `-f={{range .Funcs}}{{printf "%s: %s.%s\n" .Position $.Path .Name}}{{end}}` -filter= example.com
|
||||
|
||||
want "main.go:13:10: example.com.T.Goodbye"
|
||||
!want "example.com.T.Hello"
|
||||
want "main.go:15:6: example.com.unreferenced"
|
||||
|
||||
want "fmt.Scanf"
|
||||
want "fmt.Printf"
|
||||
!want "fmt.Println"
|
||||
|
||||
-- go.mod --
|
||||
module example.com
|
||||
go 1.18
|
||||
|
||||
-- main.go --
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
type T int
|
||||
|
||||
func main() {
|
||||
var x T
|
||||
x.Hello()
|
||||
}
|
||||
|
||||
func (T) Hello() { fmt.Println("hello") }
|
||||
func (T) Goodbye() { fmt.Println("goodbye") }
|
||||
|
||||
func unreferenced() {}
|
||||
@@ -0,0 +1,42 @@
|
||||
# Test of -test flag.
|
||||
|
||||
deadcode -test -filter=example.com example.com/p
|
||||
|
||||
want "Dead"
|
||||
!want "Live1"
|
||||
!want "Live2"
|
||||
|
||||
want "ExampleDead"
|
||||
!want "ExampleLive"
|
||||
|
||||
-- go.mod --
|
||||
module example.com
|
||||
go 1.18
|
||||
|
||||
-- p/p.go --
|
||||
package p
|
||||
|
||||
func Live1() {}
|
||||
func Live2() {}
|
||||
func Dead() {}
|
||||
|
||||
-- p/p_test.go --
|
||||
package p_test
|
||||
|
||||
import "example.com/p"
|
||||
|
||||
import "testing"
|
||||
|
||||
func Test(t *testing.T) {
|
||||
p.Live1()
|
||||
}
|
||||
|
||||
func ExampleLive() {
|
||||
p.Live2()
|
||||
// Output:
|
||||
}
|
||||
|
||||
// A test Example function without an "Output:" comment is never executed.
|
||||
func ExampleDead() {
|
||||
p.Dead()
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
# Test of -whylive flag.
|
||||
|
||||
# The -whylive argument must be live.
|
||||
|
||||
!deadcode -whylive=example.com.d example.com
|
||||
want "function example.com.d is dead code"
|
||||
|
||||
# A fully static path is preferred, even if longer.
|
||||
|
||||
deadcode -whylive=example.com.c example.com
|
||||
want " example.com.main"
|
||||
want " static@L0004 --> example.com.a"
|
||||
want " static@L0009 --> example.com.b"
|
||||
want " static@L0012 --> example.com.c"
|
||||
|
||||
# Dynamic edges are followed if necessary.
|
||||
# (Note that main is preferred over init.)
|
||||
|
||||
deadcode -whylive=example.com.f example.com
|
||||
want " example.com.main"
|
||||
want "dynamic@L0006 --> example.com.e"
|
||||
want " static@L0017 --> example.com.f"
|
||||
|
||||
# Degenerate case where target is itself a root.
|
||||
|
||||
!deadcode -whylive=example.com.main example.com
|
||||
want "example.com.main is a root"
|
||||
|
||||
# Test of path through (*T).m method wrapper.
|
||||
|
||||
deadcode -whylive=example.com/p.live example.com/p
|
||||
want " example.com/p.main"
|
||||
want "static@L0006 --> example.com/p.E.Error"
|
||||
want "static@L0010 --> example.com/p.live"
|
||||
|
||||
# Test of path through (I).m interface method wrapper (thunk).
|
||||
|
||||
deadcode -whylive=example.com/q.live example.com/q
|
||||
want " example.com/q.main"
|
||||
want "static@L0006 --> example.com/q.E.Error"
|
||||
want "static@L0010 --> example.com/q.live"
|
||||
|
||||
# Test of path through synthetic package initializer,
|
||||
# a declared package initializer, and its anonymous function.
|
||||
|
||||
deadcode -whylive=example.com/q.live2 example.com/q
|
||||
want " example.com/q.init"
|
||||
want "static@L0000 --> example.com/q.init#1"
|
||||
want "static@L0016 --> example.com/q.init#1$1"
|
||||
want "static@L0015 --> example.com/q.live2"
|
||||
|
||||
# Test of path through synthetic package initializer,
|
||||
# and a global var initializer.
|
||||
|
||||
deadcode -whylive=example.com/r.live example.com/r
|
||||
want " example.com/r.init"
|
||||
want "static@L0007 --> example.com/r.init$1"
|
||||
want "static@L0006 --> example.com/r.live"
|
||||
|
||||
-- go.mod --
|
||||
module example.com
|
||||
go 1.18
|
||||
|
||||
-- main.go --
|
||||
package main
|
||||
|
||||
func main() {
|
||||
a()
|
||||
println(c, e) // c, e are address-taken
|
||||
(func ())(nil)() // potential dynamic call to c, e
|
||||
}
|
||||
func a() {
|
||||
b()
|
||||
}
|
||||
func b() {
|
||||
c()
|
||||
}
|
||||
func c()
|
||||
func d()
|
||||
func e() {
|
||||
f()
|
||||
}
|
||||
func f()
|
||||
|
||||
func init() {
|
||||
(func ())(nil)() // potential dynamic call to c, e
|
||||
}
|
||||
|
||||
-- p/p.go --
|
||||
package main
|
||||
|
||||
func main() {
|
||||
f := (*E).Error
|
||||
var e E
|
||||
f(&e)
|
||||
}
|
||||
|
||||
type E int
|
||||
func (E) Error() string { return live() }
|
||||
|
||||
func live() string
|
||||
|
||||
-- q/q.go --
|
||||
package main
|
||||
|
||||
func main() {
|
||||
f := error.Error
|
||||
var e E
|
||||
f(e)
|
||||
}
|
||||
|
||||
type E int
|
||||
func (E) Error() string { return live() }
|
||||
|
||||
func live() string
|
||||
|
||||
func init() {
|
||||
f := func() { live2() }
|
||||
f()
|
||||
}
|
||||
|
||||
func live2()
|
||||
|
||||
-- r/r.go --
|
||||
package main
|
||||
|
||||
func main() {}
|
||||
|
||||
var x = func() int {
|
||||
return live()
|
||||
}()
|
||||
|
||||
func live() int
|
||||
@@ -0,0 +1,619 @@
|
||||
// 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 main // import "golang.org/x/tools/cmd/digraph"
|
||||
|
||||
// TODO(adonovan):
|
||||
// - support input files other than stdin
|
||||
// - support alternative formats (AT&T GraphViz, CSV, etc),
|
||||
// a comment syntax, etc.
|
||||
// - allow queries to nest, like Blaze query language.
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
func usage() {
|
||||
// Extract the content of the /* ... */ comment in doc.go.
|
||||
_, after, _ := strings.Cut(doc, "/*")
|
||||
doc, _, _ := strings.Cut(after, "*/")
|
||||
io.WriteString(flag.CommandLine.Output(), doc)
|
||||
flag.PrintDefaults()
|
||||
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
//go:embed doc.go
|
||||
var doc string
|
||||
|
||||
func main() {
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
|
||||
args := flag.Args()
|
||||
if len(args) == 0 {
|
||||
usage()
|
||||
}
|
||||
|
||||
if err := digraph(args[0], args[1:]); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "digraph: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
type nodelist []string
|
||||
|
||||
func (l nodelist) println(sep string) {
|
||||
for i, node := range l {
|
||||
if i > 0 {
|
||||
fmt.Fprint(stdout, sep)
|
||||
}
|
||||
fmt.Fprint(stdout, node)
|
||||
}
|
||||
fmt.Fprintln(stdout)
|
||||
}
|
||||
|
||||
type nodeset map[string]bool
|
||||
|
||||
func (s nodeset) sort() nodelist {
|
||||
nodes := make(nodelist, len(s))
|
||||
var i int
|
||||
for node := range s {
|
||||
nodes[i] = node
|
||||
i++
|
||||
}
|
||||
sort.Strings(nodes)
|
||||
return nodes
|
||||
}
|
||||
|
||||
func (s nodeset) addAll(x nodeset) {
|
||||
for node := range x {
|
||||
s[node] = true
|
||||
}
|
||||
}
|
||||
|
||||
// A graph maps nodes to the non-nil set of their immediate successors.
|
||||
type graph map[string]nodeset
|
||||
|
||||
func (g graph) addNode(node string) nodeset {
|
||||
edges := g[node]
|
||||
if edges == nil {
|
||||
edges = make(nodeset)
|
||||
g[node] = edges
|
||||
}
|
||||
return edges
|
||||
}
|
||||
|
||||
func (g graph) addEdges(from string, to ...string) {
|
||||
edges := g.addNode(from)
|
||||
for _, to := range to {
|
||||
g.addNode(to)
|
||||
edges[to] = true
|
||||
}
|
||||
}
|
||||
|
||||
func (g graph) nodelist() nodelist {
|
||||
nodes := make(nodeset)
|
||||
for node := range g {
|
||||
nodes[node] = true
|
||||
}
|
||||
return nodes.sort()
|
||||
}
|
||||
|
||||
func (g graph) reachableFrom(roots nodeset) nodeset {
|
||||
seen := make(nodeset)
|
||||
var visit func(node string)
|
||||
visit = func(node string) {
|
||||
if !seen[node] {
|
||||
seen[node] = true
|
||||
for e := range g[node] {
|
||||
visit(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
for root := range roots {
|
||||
visit(root)
|
||||
}
|
||||
return seen
|
||||
}
|
||||
|
||||
func (g graph) transpose() graph {
|
||||
rev := make(graph)
|
||||
for node, edges := range g {
|
||||
rev.addNode(node)
|
||||
for succ := range edges {
|
||||
rev.addEdges(succ, node)
|
||||
}
|
||||
}
|
||||
return rev
|
||||
}
|
||||
|
||||
func (g graph) sccs() []nodeset {
|
||||
// Kosaraju's algorithm---Tarjan is overkill here.
|
||||
|
||||
// Forward pass.
|
||||
S := make(nodelist, 0, len(g)) // postorder stack
|
||||
seen := make(nodeset)
|
||||
var visit func(node string)
|
||||
visit = func(node string) {
|
||||
if !seen[node] {
|
||||
seen[node] = true
|
||||
for e := range g[node] {
|
||||
visit(e)
|
||||
}
|
||||
S = append(S, node)
|
||||
}
|
||||
}
|
||||
for node := range g {
|
||||
visit(node)
|
||||
}
|
||||
|
||||
// Reverse pass.
|
||||
rev := g.transpose()
|
||||
var scc nodeset
|
||||
seen = make(nodeset)
|
||||
var rvisit func(node string)
|
||||
rvisit = func(node string) {
|
||||
if !seen[node] {
|
||||
seen[node] = true
|
||||
scc[node] = true
|
||||
for e := range rev[node] {
|
||||
rvisit(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
var sccs []nodeset
|
||||
for len(S) > 0 {
|
||||
top := S[len(S)-1]
|
||||
S = S[:len(S)-1] // pop
|
||||
if !seen[top] {
|
||||
scc = make(nodeset)
|
||||
rvisit(top)
|
||||
if len(scc) == 1 && !g[top][top] {
|
||||
continue
|
||||
}
|
||||
sccs = append(sccs, scc)
|
||||
}
|
||||
}
|
||||
return sccs
|
||||
}
|
||||
|
||||
func (g graph) allpaths(from, to string) error {
|
||||
// Mark all nodes to "to".
|
||||
seen := make(nodeset) // value of seen[x] indicates whether x is on some path to "to"
|
||||
var visit func(node string) bool
|
||||
visit = func(node string) bool {
|
||||
reachesTo, ok := seen[node]
|
||||
if !ok {
|
||||
reachesTo = node == to
|
||||
seen[node] = reachesTo
|
||||
for e := range g[node] {
|
||||
if visit(e) {
|
||||
reachesTo = true
|
||||
}
|
||||
}
|
||||
if reachesTo && node != to {
|
||||
seen[node] = true
|
||||
}
|
||||
}
|
||||
return reachesTo
|
||||
}
|
||||
visit(from)
|
||||
|
||||
// For each marked node, collect its marked successors.
|
||||
var edges []string
|
||||
for n := range seen {
|
||||
for succ := range g[n] {
|
||||
if seen[succ] {
|
||||
edges = append(edges, n+" "+succ)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort (so that this method is deterministic) and print edges.
|
||||
sort.Strings(edges)
|
||||
for _, e := range edges {
|
||||
fmt.Fprintln(stdout, e)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g graph) somepath(from, to string) error {
|
||||
// Search breadth-first so that we return a minimal path.
|
||||
|
||||
// A path is a linked list whose head is a candidate "to" node
|
||||
// and whose tail is the path ending in the "from" node.
|
||||
type path struct {
|
||||
node string
|
||||
tail *path
|
||||
}
|
||||
|
||||
seen := nodeset{from: true}
|
||||
|
||||
var queue []*path
|
||||
queue = append(queue, &path{node: from, tail: nil})
|
||||
for len(queue) > 0 {
|
||||
p := queue[0]
|
||||
queue = queue[1:]
|
||||
|
||||
if p.node == to {
|
||||
// Found a path. Print, tail first.
|
||||
var print func(p *path)
|
||||
print = func(p *path) {
|
||||
if p.tail != nil {
|
||||
print(p.tail)
|
||||
fmt.Fprintln(stdout, p.tail.node+" "+p.node)
|
||||
}
|
||||
}
|
||||
print(p)
|
||||
return nil
|
||||
}
|
||||
|
||||
for succ := range g[p.node] {
|
||||
if !seen[succ] {
|
||||
seen[succ] = true
|
||||
queue = append(queue, &path{node: succ, tail: p})
|
||||
}
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("no path from %q to %q", from, to)
|
||||
}
|
||||
|
||||
func (g graph) toDot(w *bytes.Buffer) {
|
||||
fmt.Fprintln(w, "digraph {")
|
||||
for _, src := range g.nodelist() {
|
||||
for _, dst := range g[src].sort() {
|
||||
// Dot's quoting rules appear to align with Go's for escString,
|
||||
// which is the syntax of node IDs. Labels require significantly
|
||||
// more quoting, but that appears not to be necessary if the node ID
|
||||
// is implicitly used as the label.
|
||||
fmt.Fprintf(w, "\t%q -> %q;\n", src, dst)
|
||||
}
|
||||
}
|
||||
fmt.Fprintln(w, "}")
|
||||
}
|
||||
|
||||
func parse(rd io.Reader) (graph, error) {
|
||||
g := make(graph)
|
||||
|
||||
var linenum int
|
||||
// We avoid bufio.Scanner as it imposes a (configurable) limit
|
||||
// on line length, whereas Reader.ReadString does not.
|
||||
in := bufio.NewReader(rd)
|
||||
for {
|
||||
linenum++
|
||||
line, err := in.ReadString('\n')
|
||||
eof := false
|
||||
if err == io.EOF {
|
||||
eof = true
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Split into words, honoring double-quotes per Go spec.
|
||||
words, err := split(line)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("at line %d: %v", linenum, err)
|
||||
}
|
||||
if len(words) > 0 {
|
||||
g.addEdges(words[0], words[1:]...)
|
||||
}
|
||||
if eof {
|
||||
break
|
||||
}
|
||||
}
|
||||
return g, nil
|
||||
}
|
||||
|
||||
// Overridable for redirection.
|
||||
var stdin io.Reader = os.Stdin
|
||||
var stdout io.Writer = os.Stdout
|
||||
|
||||
func digraph(cmd string, args []string) error {
|
||||
// Parse the input graph.
|
||||
g, err := parse(stdin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Parse the command line.
|
||||
switch cmd {
|
||||
case "nodes":
|
||||
if len(args) != 0 {
|
||||
return fmt.Errorf("usage: digraph nodes")
|
||||
}
|
||||
g.nodelist().println("\n")
|
||||
|
||||
case "degree":
|
||||
if len(args) != 0 {
|
||||
return fmt.Errorf("usage: digraph degree")
|
||||
}
|
||||
nodes := make(nodeset)
|
||||
for node := range g {
|
||||
nodes[node] = true
|
||||
}
|
||||
rev := g.transpose()
|
||||
for _, node := range nodes.sort() {
|
||||
fmt.Fprintf(stdout, "%d\t%d\t%s\n", len(rev[node]), len(g[node]), node)
|
||||
}
|
||||
|
||||
case "transpose":
|
||||
if len(args) != 0 {
|
||||
return fmt.Errorf("usage: digraph transpose")
|
||||
}
|
||||
var revEdges []string
|
||||
for node, succs := range g.transpose() {
|
||||
for succ := range succs {
|
||||
revEdges = append(revEdges, fmt.Sprintf("%s %s", node, succ))
|
||||
}
|
||||
}
|
||||
sort.Strings(revEdges) // make output deterministic
|
||||
for _, e := range revEdges {
|
||||
fmt.Fprintln(stdout, e)
|
||||
}
|
||||
|
||||
case "succs", "preds":
|
||||
if len(args) == 0 {
|
||||
return fmt.Errorf("usage: digraph %s <node> ... ", cmd)
|
||||
}
|
||||
g := g
|
||||
if cmd == "preds" {
|
||||
g = g.transpose()
|
||||
}
|
||||
result := make(nodeset)
|
||||
for _, root := range args {
|
||||
edges := g[root]
|
||||
if edges == nil {
|
||||
return fmt.Errorf("no such node %q", root)
|
||||
}
|
||||
result.addAll(edges)
|
||||
}
|
||||
result.sort().println("\n")
|
||||
|
||||
case "forward", "reverse":
|
||||
if len(args) == 0 {
|
||||
return fmt.Errorf("usage: digraph %s <node> ... ", cmd)
|
||||
}
|
||||
roots := make(nodeset)
|
||||
for _, root := range args {
|
||||
if g[root] == nil {
|
||||
return fmt.Errorf("no such node %q", root)
|
||||
}
|
||||
roots[root] = true
|
||||
}
|
||||
g := g
|
||||
if cmd == "reverse" {
|
||||
g = g.transpose()
|
||||
}
|
||||
g.reachableFrom(roots).sort().println("\n")
|
||||
|
||||
case "somepath":
|
||||
if len(args) != 2 {
|
||||
return fmt.Errorf("usage: digraph somepath <from> <to>")
|
||||
}
|
||||
from, to := args[0], args[1]
|
||||
if g[from] == nil {
|
||||
return fmt.Errorf("no such 'from' node %q", from)
|
||||
}
|
||||
if g[to] == nil {
|
||||
return fmt.Errorf("no such 'to' node %q", to)
|
||||
}
|
||||
if err := g.somepath(from, to); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case "allpaths":
|
||||
if len(args) != 2 {
|
||||
return fmt.Errorf("usage: digraph allpaths <from> <to>")
|
||||
}
|
||||
from, to := args[0], args[1]
|
||||
if g[from] == nil {
|
||||
return fmt.Errorf("no such 'from' node %q", from)
|
||||
}
|
||||
if g[to] == nil {
|
||||
return fmt.Errorf("no such 'to' node %q", to)
|
||||
}
|
||||
if err := g.allpaths(from, to); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case "sccs":
|
||||
if len(args) != 0 {
|
||||
return fmt.Errorf("usage: digraph sccs")
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
oldStdout := stdout
|
||||
stdout = buf
|
||||
for _, scc := range g.sccs() {
|
||||
scc.sort().println(" ")
|
||||
}
|
||||
lines := strings.SplitAfter(buf.String(), "\n")
|
||||
sort.Strings(lines)
|
||||
stdout = oldStdout
|
||||
io.WriteString(stdout, strings.Join(lines, ""))
|
||||
|
||||
case "scc":
|
||||
if len(args) != 1 {
|
||||
return fmt.Errorf("usage: digraph scc <node>")
|
||||
}
|
||||
node := args[0]
|
||||
if g[node] == nil {
|
||||
return fmt.Errorf("no such node %q", node)
|
||||
}
|
||||
for _, scc := range g.sccs() {
|
||||
if scc[node] {
|
||||
scc.sort().println("\n")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
case "focus":
|
||||
if len(args) != 1 {
|
||||
return fmt.Errorf("usage: digraph focus <node>")
|
||||
}
|
||||
node := args[0]
|
||||
if g[node] == nil {
|
||||
return fmt.Errorf("no such node %q", node)
|
||||
}
|
||||
|
||||
edges := make(map[string]struct{})
|
||||
for from := range g.reachableFrom(nodeset{node: true}) {
|
||||
for to := range g[from] {
|
||||
edges[fmt.Sprintf("%s %s", from, to)] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
gtrans := g.transpose()
|
||||
for from := range gtrans.reachableFrom(nodeset{node: true}) {
|
||||
for to := range gtrans[from] {
|
||||
edges[fmt.Sprintf("%s %s", to, from)] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
edgesSorted := make([]string, 0, len(edges))
|
||||
for e := range edges {
|
||||
edgesSorted = append(edgesSorted, e)
|
||||
}
|
||||
sort.Strings(edgesSorted)
|
||||
fmt.Fprintln(stdout, strings.Join(edgesSorted, "\n"))
|
||||
|
||||
case "to":
|
||||
if len(args) != 1 || args[0] != "dot" {
|
||||
return fmt.Errorf("usage: digraph to dot")
|
||||
}
|
||||
var b bytes.Buffer
|
||||
g.toDot(&b)
|
||||
stdout.Write(b.Bytes())
|
||||
|
||||
default:
|
||||
return fmt.Errorf("no such command %q", cmd)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// -- Utilities --------------------------------------------------------
|
||||
|
||||
// split splits a line into words, which are generally separated by
|
||||
// spaces, but Go-style double-quoted string literals are also supported.
|
||||
// (This approximates the behaviour of the Bourne shell.)
|
||||
//
|
||||
// `one "two three"` -> ["one" "two three"]
|
||||
// `a"\n"b` -> ["a\nb"]
|
||||
func split(line string) ([]string, error) {
|
||||
var (
|
||||
words []string
|
||||
inWord bool
|
||||
current bytes.Buffer
|
||||
)
|
||||
|
||||
for len(line) > 0 {
|
||||
r, size := utf8.DecodeRuneInString(line)
|
||||
if unicode.IsSpace(r) {
|
||||
if inWord {
|
||||
words = append(words, current.String())
|
||||
current.Reset()
|
||||
inWord = false
|
||||
}
|
||||
} else if r == '"' {
|
||||
var ok bool
|
||||
size, ok = quotedLength(line)
|
||||
if !ok {
|
||||
return nil, errors.New("invalid quotation")
|
||||
}
|
||||
s, err := strconv.Unquote(line[:size])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
current.WriteString(s)
|
||||
inWord = true
|
||||
} else {
|
||||
current.WriteRune(r)
|
||||
inWord = true
|
||||
}
|
||||
line = line[size:]
|
||||
}
|
||||
if inWord {
|
||||
words = append(words, current.String())
|
||||
}
|
||||
return words, nil
|
||||
}
|
||||
|
||||
// quotedLength returns the length in bytes of the prefix of input that
|
||||
// contain a possibly-valid double-quoted Go string literal.
|
||||
//
|
||||
// On success, n is at least two (""); input[:n] may be passed to
|
||||
// strconv.Unquote to interpret its value, and input[n:] contains the
|
||||
// rest of the input.
|
||||
//
|
||||
// On failure, quotedLength returns false, and the entire input can be
|
||||
// passed to strconv.Unquote if an informative error message is desired.
|
||||
//
|
||||
// quotedLength does not and need not detect all errors, such as
|
||||
// invalid hex or octal escape sequences, since it assumes
|
||||
// strconv.Unquote will be applied to the prefix. It guarantees only
|
||||
// that if there is a prefix of input containing a valid string literal,
|
||||
// its length is returned.
|
||||
//
|
||||
// TODO(adonovan): move this into a strconv-like utility package.
|
||||
func quotedLength(input string) (n int, ok bool) {
|
||||
var offset int
|
||||
|
||||
// next returns the rune at offset, or -1 on EOF.
|
||||
// offset advances to just after that rune.
|
||||
next := func() rune {
|
||||
if offset < len(input) {
|
||||
r, size := utf8.DecodeRuneInString(input[offset:])
|
||||
offset += size
|
||||
return r
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
if next() != '"' {
|
||||
return // error: not a quotation
|
||||
}
|
||||
|
||||
for {
|
||||
r := next()
|
||||
if r == '\n' || r < 0 {
|
||||
return // error: string literal not terminated
|
||||
}
|
||||
if r == '"' {
|
||||
return offset, true // success
|
||||
}
|
||||
if r == '\\' {
|
||||
var skip int
|
||||
switch next() {
|
||||
case 'a', 'b', 'f', 'n', 'r', 't', 'v', '\\', '"':
|
||||
skip = 0
|
||||
case '0', '1', '2', '3', '4', '5', '6', '7':
|
||||
skip = 2
|
||||
case 'x':
|
||||
skip = 2
|
||||
case 'u':
|
||||
skip = 4
|
||||
case 'U':
|
||||
skip = 8
|
||||
default:
|
||||
return // error: invalid escape
|
||||
}
|
||||
|
||||
for i := 0; i < skip; i++ {
|
||||
next()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
// 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 main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDigraph(t *testing.T) {
|
||||
const g1 = `
|
||||
socks shoes
|
||||
shorts pants
|
||||
pants belt shoes
|
||||
shirt tie sweater
|
||||
sweater jacket
|
||||
hat
|
||||
`
|
||||
|
||||
const g2 = `
|
||||
a b c
|
||||
b d
|
||||
c d
|
||||
d c
|
||||
e e
|
||||
`
|
||||
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
input string
|
||||
cmd string
|
||||
args []string
|
||||
want string
|
||||
}{
|
||||
{"nodes", g1, "nodes", nil, "belt\nhat\njacket\npants\nshirt\nshoes\nshorts\nsocks\nsweater\ntie\n"},
|
||||
{"reverse", g1, "reverse", []string{"jacket"}, "jacket\nshirt\nsweater\n"},
|
||||
{"transpose", g1, "transpose", nil, "belt pants\njacket sweater\npants shorts\nshoes pants\nshoes socks\nsweater shirt\ntie shirt\n"},
|
||||
{"forward", g1, "forward", []string{"socks"}, "shoes\nsocks\n"},
|
||||
{"forward multiple args", g1, "forward", []string{"socks", "sweater"}, "jacket\nshoes\nsocks\nsweater\n"},
|
||||
{"scss", g2, "sccs", nil, "c d\ne\n"},
|
||||
{"scc", g2, "scc", []string{"d"}, "c\nd\n"},
|
||||
{"succs", g2, "succs", []string{"a"}, "b\nc\n"},
|
||||
{"succs-long-token", g2 + "x " + strings.Repeat("x", 96*1024), "succs", []string{"x"}, strings.Repeat("x", 96*1024) + "\n"},
|
||||
{"preds", g2, "preds", []string{"c"}, "a\nd\n"},
|
||||
{"preds multiple args", g2, "preds", []string{"c", "d"}, "a\nb\nc\nd\n"},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
stdin = strings.NewReader(test.input)
|
||||
stdout = new(bytes.Buffer)
|
||||
if err := digraph(test.cmd, test.args); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got := stdout.(fmt.Stringer).String()
|
||||
if got != test.want {
|
||||
t.Errorf("digraph(%s, %s) = got %q, want %q", test.cmd, test.args, got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TODO(adonovan):
|
||||
// - test errors
|
||||
}
|
||||
|
||||
func TestAllpaths(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
in string
|
||||
to string // from is always "A"
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "Basic",
|
||||
in: "A B\nB C",
|
||||
to: "B",
|
||||
want: "A B\n",
|
||||
},
|
||||
{
|
||||
name: "Long",
|
||||
in: "A B\nB C\n",
|
||||
to: "C",
|
||||
want: "A B\nB C\n",
|
||||
},
|
||||
{
|
||||
name: "Cycle Basic",
|
||||
in: "A B\nB A",
|
||||
to: "B",
|
||||
want: "A B\nB A\n",
|
||||
},
|
||||
{
|
||||
name: "Cycle Path Out",
|
||||
// A <-> B -> C -> D
|
||||
in: "A B\nB A\nB C\nC D",
|
||||
to: "C",
|
||||
want: "A B\nB A\nB C\n",
|
||||
},
|
||||
{
|
||||
name: "Cycle Path Out Further Out",
|
||||
// A -> B <-> C -> D -> E
|
||||
in: "A B\nB C\nC D\nC B\nD E",
|
||||
to: "D",
|
||||
want: "A B\nB C\nC B\nC D\n",
|
||||
},
|
||||
{
|
||||
name: "Two Paths Basic",
|
||||
// /-> C --\
|
||||
// A -> B -- -> E -> F
|
||||
// \-> D --/
|
||||
in: "A B\nB C\nC E\nB D\nD E\nE F",
|
||||
to: "E",
|
||||
want: "A B\nB C\nB D\nC E\nD E\n",
|
||||
},
|
||||
{
|
||||
name: "Two Paths With One Immediately From Start",
|
||||
// /-> B -+ -> D
|
||||
// A -- |
|
||||
// \-> C <+
|
||||
in: "A B\nA C\nB C\nB D",
|
||||
to: "C",
|
||||
want: "A B\nA C\nB C\n",
|
||||
},
|
||||
{
|
||||
name: "Two Paths Further Up",
|
||||
// /-> B --\
|
||||
// A -- -> D -> E -> F
|
||||
// \-> C --/
|
||||
in: "A B\nA C\nB D\nC D\nD E\nE F",
|
||||
to: "E",
|
||||
want: "A B\nA C\nB D\nC D\nD E\n",
|
||||
},
|
||||
{
|
||||
// We should include A - C - D even though it's further up the
|
||||
// second path than D (which would already be in the graph by
|
||||
// the time we get around to integrating the second path).
|
||||
name: "Two Splits",
|
||||
// /-> B --\ /-> E --\
|
||||
// A -- -> D -- -> G -> H
|
||||
// \-> C --/ \-> F --/
|
||||
in: "A B\nA C\nB D\nC D\nD E\nD F\nE G\nF G\nG H",
|
||||
to: "G",
|
||||
want: "A B\nA C\nB D\nC D\nD E\nD F\nE G\nF G\n",
|
||||
},
|
||||
{
|
||||
// D - E should not be duplicated.
|
||||
name: "Two Paths - Two Splits With Gap",
|
||||
// /-> B --\ /-> F --\
|
||||
// A -- -> D -> E -- -> H -> I
|
||||
// \-> C --/ \-> G --/
|
||||
in: "A B\nA C\nB D\nC D\nD E\nE F\nE G\nF H\nG H\nH I",
|
||||
to: "H",
|
||||
want: "A B\nA C\nB D\nC D\nD E\nE F\nE G\nF H\nG H\n",
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
stdin = strings.NewReader(test.in)
|
||||
stdout = new(bytes.Buffer)
|
||||
if err := digraph("allpaths", []string{"A", test.to}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got := stdout.(fmt.Stringer).String()
|
||||
if got != test.want {
|
||||
t.Errorf("digraph(allpaths, A, %s) = got %q, want %q", test.to, got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSomepath(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
in string
|
||||
to string
|
||||
// somepath is non-deterministic, so we have to provide all the
|
||||
// possible options. Each option is separated with |.
|
||||
wantAnyOf string
|
||||
}{
|
||||
{
|
||||
name: "Basic",
|
||||
in: "A B\n",
|
||||
to: "B",
|
||||
wantAnyOf: "A B",
|
||||
},
|
||||
{
|
||||
name: "Basic With Cycle",
|
||||
in: "A B\nB A",
|
||||
to: "B",
|
||||
wantAnyOf: "A B",
|
||||
},
|
||||
{
|
||||
name: "Two Paths",
|
||||
// /-> B --\
|
||||
// A -- -> D
|
||||
// \-> C --/
|
||||
in: "A B\nA C\nB D\nC D",
|
||||
to: "D",
|
||||
wantAnyOf: "A B\nB D|A C\nC D",
|
||||
},
|
||||
{
|
||||
name: "Printed path is minimal",
|
||||
// A -> B1->B2->B3 -> E
|
||||
// A -> C1->C2 -> E
|
||||
// A -> D -> E
|
||||
in: "A D C1 B1\nD E\nC1 C2\nC2 E\nB1 B2\nB2 B3\nB3 E",
|
||||
to: "E",
|
||||
wantAnyOf: "A D\nD E",
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
stdin = strings.NewReader(test.in)
|
||||
stdout = new(bytes.Buffer)
|
||||
if err := digraph("somepath", []string{"A", test.to}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got := stdout.(fmt.Stringer).String()
|
||||
lines := strings.Split(got, "\n")
|
||||
sort.Strings(lines)
|
||||
got = strings.Join(lines[1:], "\n")
|
||||
|
||||
var oneMatch bool
|
||||
for _, want := range strings.Split(test.wantAnyOf, "|") {
|
||||
if got == want {
|
||||
oneMatch = true
|
||||
}
|
||||
}
|
||||
if !oneMatch {
|
||||
t.Errorf("digraph(somepath, A, %s) = got %q, want any of\n%s", test.to, got, test.wantAnyOf)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplit(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
line string
|
||||
want []string
|
||||
}{
|
||||
{`one "2a 2b" three`, []string{"one", "2a 2b", "three"}},
|
||||
{`one tw"\n\x0a\u000a\012"o three`, []string{"one", "tw\n\n\n\no", "three"}},
|
||||
} {
|
||||
got, err := split(test.line)
|
||||
if err != nil {
|
||||
t.Errorf("split(%s) failed: %v", test.line, err)
|
||||
}
|
||||
if !reflect.DeepEqual(got, test.want) {
|
||||
t.Errorf("split(%s) = %v, want %v", test.line, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuotedLength(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
input string
|
||||
want int
|
||||
}{
|
||||
{`"abc"`, 5},
|
||||
{`"abc"def`, 5},
|
||||
{`"abc\"d"ef`, 8}, // "abc\"d" is consumed, ef is residue
|
||||
{`"\012\n\x0a\u000a\U0000000a"`, 28},
|
||||
{"\"\xff\"", 3}, // bad UTF-8 is ok
|
||||
{`"\xff"`, 6}, // hex escape for bad UTF-8 is ok
|
||||
} {
|
||||
got, ok := quotedLength(test.input)
|
||||
if !ok {
|
||||
got = 0
|
||||
}
|
||||
if got != test.want {
|
||||
t.Errorf("quotedLength(%s) = %d, want %d", test.input, got, test.want)
|
||||
}
|
||||
}
|
||||
|
||||
// errors
|
||||
for _, input := range []string{
|
||||
``, // not a quotation
|
||||
`a`, // not a quotation
|
||||
`'a'`, // not a quotation
|
||||
`"a`, // not terminated
|
||||
`"\0"`, // short octal escape
|
||||
`"\x1"`, // short hex escape
|
||||
`"\u000"`, // short \u escape
|
||||
`"\U0000000"`, // short \U escape
|
||||
`"\k"`, // invalid escape
|
||||
"\"ab\nc\"", // newline
|
||||
} {
|
||||
if n, ok := quotedLength(input); ok {
|
||||
t.Errorf("quotedLength(%s) = %d, want !ok", input, n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFocus(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
in string
|
||||
focus string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "Basic",
|
||||
in: "A B",
|
||||
focus: "B",
|
||||
want: "A B\n",
|
||||
},
|
||||
{
|
||||
name: "Some Nodes Not Included",
|
||||
// C does not have a path involving B, and should not be included
|
||||
// in the output.
|
||||
in: "A B\nA C",
|
||||
focus: "B",
|
||||
want: "A B\n",
|
||||
},
|
||||
{
|
||||
name: "Cycle In Path",
|
||||
// A <-> B -> C
|
||||
in: "A B\nB A\nB C",
|
||||
focus: "C",
|
||||
want: "A B\nB A\nB C\n",
|
||||
},
|
||||
{
|
||||
name: "Cycle Out Of Path",
|
||||
// C <- A <->B
|
||||
in: "A B\nB A\nB C",
|
||||
focus: "C",
|
||||
want: "A B\nB A\nB C\n",
|
||||
},
|
||||
{
|
||||
name: "Complex",
|
||||
// Paths in and out from focus.
|
||||
// /-> F
|
||||
// /-> B -> D --
|
||||
// A -- \-> E
|
||||
// \-> C
|
||||
in: "A B\nA C\nB D\nD F\nD E",
|
||||
focus: "D",
|
||||
want: "A B\nB D\nD E\nD F\n",
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
stdin = strings.NewReader(test.in)
|
||||
stdout = new(bytes.Buffer)
|
||||
if err := digraph("focus", []string{test.focus}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := stdout.(fmt.Stringer).String()
|
||||
if got != test.want {
|
||||
t.Errorf("digraph(focus, %s) = got %q, want %q", test.focus, got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToDot(t *testing.T) {
|
||||
in := `a b c
|
||||
b "d\"\\d"
|
||||
c "d\"\\d"`
|
||||
want := `digraph {
|
||||
"a" -> "b";
|
||||
"a" -> "c";
|
||||
"b" -> "d\"\\d";
|
||||
"c" -> "d\"\\d";
|
||||
}
|
||||
`
|
||||
defer func(in io.Reader, out io.Writer) { stdin, stdout = in, out }(stdin, stdout)
|
||||
stdin = strings.NewReader(in)
|
||||
stdout = new(bytes.Buffer)
|
||||
if err := digraph("to", []string{"dot"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := stdout.(fmt.Stringer).String()
|
||||
if got != want {
|
||||
t.Errorf("digraph(to, dot) = got %q, want %q", got, want)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// 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.
|
||||
|
||||
/*
|
||||
The digraph command performs queries over unlabelled directed graphs
|
||||
represented in text form. It is intended to integrate nicely with
|
||||
typical UNIX command pipelines.
|
||||
|
||||
Usage:
|
||||
|
||||
your-application | digraph [command]
|
||||
|
||||
The supported commands are:
|
||||
|
||||
nodes
|
||||
the set of all nodes
|
||||
degree
|
||||
the in-degree and out-degree of each node
|
||||
transpose
|
||||
the reverse of the input edges
|
||||
preds <node> ...
|
||||
the set of immediate predecessors of the specified nodes
|
||||
succs <node> ...
|
||||
the set of immediate successors of the specified nodes
|
||||
forward <node> ...
|
||||
the set of nodes transitively reachable from the specified nodes
|
||||
reverse <node> ...
|
||||
the set of nodes that transitively reach the specified nodes
|
||||
somepath <node> <node>
|
||||
the list of nodes on some arbitrary path from the first node to the second
|
||||
allpaths <node> <node>
|
||||
the set of nodes on all paths from the first node to the second
|
||||
sccs
|
||||
all strongly connected components (one per line)
|
||||
scc <node>
|
||||
the set of nodes strongly connected to the specified one
|
||||
focus <node>
|
||||
the subgraph containing all directed paths that pass through the specified node
|
||||
to dot
|
||||
print the graph in Graphviz dot format (other formats may be supported in the future)
|
||||
|
||||
Input format:
|
||||
|
||||
Each line contains zero or more words. Words are separated by unquoted
|
||||
whitespace; words may contain Go-style double-quoted portions, allowing spaces
|
||||
and other characters to be expressed.
|
||||
|
||||
Each word declares a node, and if there are more than one, an edge from the
|
||||
first to each subsequent one. The graph is provided on the standard input.
|
||||
|
||||
For instance, the following (acyclic) graph specifies a partial order among the
|
||||
subtasks of getting dressed:
|
||||
|
||||
$ cat clothes.txt
|
||||
socks shoes
|
||||
"boxer shorts" pants
|
||||
pants belt shoes
|
||||
shirt tie sweater
|
||||
sweater jacket
|
||||
hat
|
||||
|
||||
The line "shirt tie sweater" indicates the two edges shirt -> tie and
|
||||
shirt -> sweater, not shirt -> tie -> sweater.
|
||||
|
||||
Example usage:
|
||||
|
||||
Show which clothes (see above) must be donned before a jacket:
|
||||
|
||||
$ digraph reverse jacket
|
||||
|
||||
Many tools can be persuaded to produce output in digraph format,
|
||||
as in the following examples.
|
||||
|
||||
Using an import graph produced by go list, show a path that indicates
|
||||
why the gopls application depends on the cmp package:
|
||||
|
||||
$ go list -f '{{.ImportPath}} {{join .Imports " "}}' -deps golang.org/x/tools/gopls |
|
||||
digraph somepath golang.org/x/tools/gopls github.com/google/go-cmp/cmp
|
||||
|
||||
Show which packages in x/tools depend, perhaps indirectly, on the callgraph package:
|
||||
|
||||
$ go list -f '{{.ImportPath}} {{join .Imports " "}}' -deps golang.org/x/tools/... |
|
||||
digraph reverse golang.org/x/tools/go/callgraph
|
||||
|
||||
Visualize the package dependency graph of the current package:
|
||||
|
||||
$ go list -f '{{.ImportPath}} {{join .Imports " "}}' -deps |
|
||||
digraph to dot | dot -Tpng -o x.png
|
||||
|
||||
Using a module graph produced by go mod, show all dependencies of the current module:
|
||||
|
||||
$ go mod graph | digraph forward $(go list -m)
|
||||
*/
|
||||
package main
|
||||
@@ -0,0 +1,191 @@
|
||||
// Copyright 2014 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 eg command performs example-based refactoring.
|
||||
// For documentation, run the command, or see Help in
|
||||
// golang.org/x/tools/refactor/eg.
|
||||
package main // import "golang.org/x/tools/cmd/eg"
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/format"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/tools/go/packages"
|
||||
"golang.org/x/tools/refactor/eg"
|
||||
)
|
||||
|
||||
var (
|
||||
beforeeditFlag = flag.String("beforeedit", "", "A command to exec before each file is edited (e.g. chmod, checkout). Whitespace delimits argument words. The string '{}' is replaced by the file name.")
|
||||
helpFlag = flag.Bool("help", false, "show detailed help message")
|
||||
templateFlag = flag.String("t", "", "template.go file specifying the refactoring")
|
||||
transitiveFlag = flag.Bool("transitive", false, "apply refactoring to all dependencies too")
|
||||
writeFlag = flag.Bool("w", false, "rewrite input files in place (by default, the results are printed to standard output)")
|
||||
verboseFlag = flag.Bool("v", false, "show verbose matcher diagnostics")
|
||||
)
|
||||
|
||||
const usage = `eg: an example-based refactoring tool.
|
||||
|
||||
Usage: eg -t template.go [-w] [-transitive] <packages>
|
||||
|
||||
-help show detailed help message
|
||||
-t template.go specifies the template file (use -help to see explanation)
|
||||
-w causes files to be re-written in place.
|
||||
-transitive causes all dependencies to be refactored too.
|
||||
-v show verbose matcher diagnostics
|
||||
-beforeedit cmd a command to exec before each file is modified.
|
||||
"{}" represents the name of the file.
|
||||
`
|
||||
|
||||
func main() {
|
||||
if err := doMain(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "eg: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func doMain() error {
|
||||
flag.Parse()
|
||||
args := flag.Args()
|
||||
|
||||
if *helpFlag {
|
||||
help := eg.Help // hide %s from vet
|
||||
fmt.Fprint(os.Stderr, help)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
if len(args) == 0 {
|
||||
fmt.Fprint(os.Stderr, usage)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if *templateFlag == "" {
|
||||
return fmt.Errorf("no -t template.go file specified")
|
||||
}
|
||||
|
||||
tAbs, err := filepath.Abs(*templateFlag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
template, err := os.ReadFile(tAbs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg := &packages.Config{
|
||||
Fset: token.NewFileSet(),
|
||||
Mode: packages.NeedTypesInfo | packages.NeedName | packages.NeedTypes | packages.NeedSyntax | packages.NeedImports | packages.NeedDeps | packages.NeedCompiledGoFiles,
|
||||
Tests: true,
|
||||
}
|
||||
|
||||
pkgs, err := packages.Load(cfg, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tFile, err := parser.ParseFile(cfg.Fset, tAbs, template, parser.ParseComments)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Type-check the template.
|
||||
tInfo := types.Info{
|
||||
Types: make(map[ast.Expr]types.TypeAndValue),
|
||||
Defs: make(map[*ast.Ident]types.Object),
|
||||
Uses: make(map[*ast.Ident]types.Object),
|
||||
Implicits: make(map[ast.Node]types.Object),
|
||||
Selections: make(map[*ast.SelectorExpr]*types.Selection),
|
||||
Scopes: make(map[ast.Node]*types.Scope),
|
||||
}
|
||||
conf := types.Config{
|
||||
Importer: pkgsImporter(pkgs),
|
||||
}
|
||||
tPkg, err := conf.Check("egtemplate", cfg.Fset, []*ast.File{tFile}, &tInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Analyze the template.
|
||||
xform, err := eg.NewTransformer(cfg.Fset, tPkg, tFile, &tInfo, *verboseFlag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Apply it to the input packages.
|
||||
var all []*packages.Package
|
||||
if *transitiveFlag {
|
||||
packages.Visit(pkgs, nil, func(p *packages.Package) { all = append(all, p) })
|
||||
} else {
|
||||
all = pkgs
|
||||
}
|
||||
var hadErrors bool
|
||||
for _, pkg := range pkgs {
|
||||
for i, filename := range pkg.CompiledGoFiles {
|
||||
if filename == tAbs {
|
||||
// Don't rewrite the template file.
|
||||
continue
|
||||
}
|
||||
file := pkg.Syntax[i]
|
||||
n := xform.Transform(pkg.TypesInfo, pkg.Types, file)
|
||||
if n == 0 {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "=== %s (%d matches)\n", filename, n)
|
||||
if *writeFlag {
|
||||
// Run the before-edit command (e.g. "chmod +w", "checkout") if any.
|
||||
if *beforeeditFlag != "" {
|
||||
args := strings.Fields(*beforeeditFlag)
|
||||
// Replace "{}" with the filename, like find(1).
|
||||
for i := range args {
|
||||
if i > 0 {
|
||||
args[i] = strings.Replace(args[i], "{}", filename, -1)
|
||||
}
|
||||
}
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Warning: edit hook %q failed (%s)\n",
|
||||
args, err)
|
||||
}
|
||||
}
|
||||
if err := eg.WriteAST(cfg.Fset, filename, file); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "eg: %s\n", err)
|
||||
hadErrors = true
|
||||
}
|
||||
} else {
|
||||
format.Node(os.Stdout, cfg.Fset, file)
|
||||
}
|
||||
}
|
||||
}
|
||||
if hadErrors {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type pkgsImporter []*packages.Package
|
||||
|
||||
func (p pkgsImporter) Import(path string) (tpkg *types.Package, err error) {
|
||||
packages.Visit([]*packages.Package(p), func(pkg *packages.Package) bool {
|
||||
if pkg.PkgPath == path {
|
||||
tpkg = pkg.Types
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}, nil)
|
||||
if tpkg != nil {
|
||||
return tpkg, nil
|
||||
}
|
||||
return nil, fmt.Errorf("package %q not found", path)
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
// Copyright 2021 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.
|
||||
|
||||
// file2fuzz converts binary files, such as those used by go-fuzz, to the Go
|
||||
// fuzzing corpus format.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// file2fuzz [-o output] [input...]
|
||||
//
|
||||
// The default behavior is to read input from stdin and write the converted
|
||||
// output to stdout. If any position arguments are provided stdin is ignored
|
||||
// and the arguments are assumed to be input files to convert.
|
||||
//
|
||||
// The -o flag provides an path to write output files to. If only one positional
|
||||
// argument is specified it may be a file path or an existing directory, if there are
|
||||
// multiple inputs specified it must be a directory. If a directory is provided
|
||||
// the name of the file will be the SHA-256 hash of its contents.
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// encVersion1 is version 1 Go fuzzer corpus encoding.
|
||||
var encVersion1 = "go test fuzz v1"
|
||||
|
||||
func encodeByteSlice(b []byte) []byte {
|
||||
return []byte(fmt.Sprintf("%s\n[]byte(%q)", encVersion1, b))
|
||||
}
|
||||
|
||||
func usage() {
|
||||
fmt.Fprintf(os.Stderr, "usage: file2fuzz [-o output] [input...]\nconverts files to Go fuzzer corpus format\n")
|
||||
fmt.Fprintf(os.Stderr, "\tinput: files to convert\n")
|
||||
fmt.Fprintf(os.Stderr, "\t-o: where to write converted file(s)\n")
|
||||
os.Exit(2)
|
||||
}
|
||||
func dirWriter(dir string) func([]byte) error {
|
||||
return func(b []byte) error {
|
||||
sum := fmt.Sprintf("%x", sha256.Sum256(b))
|
||||
name := filepath.Join(dir, sum)
|
||||
if err := os.MkdirAll(dir, 0777); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(name, b, 0666); err != nil {
|
||||
os.Remove(name)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func convert(inputArgs []string, outputArg string) error {
|
||||
var input []io.Reader
|
||||
if args := inputArgs; len(args) == 0 {
|
||||
input = []io.Reader{os.Stdin}
|
||||
} else {
|
||||
for _, a := range args {
|
||||
f, err := os.Open(a)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to open %q: %s", a, err)
|
||||
}
|
||||
defer f.Close()
|
||||
if fi, err := f.Stat(); err != nil {
|
||||
return fmt.Errorf("unable to open %q: %s", a, err)
|
||||
} else if fi.IsDir() {
|
||||
return fmt.Errorf("%q is a directory, not a file", a)
|
||||
}
|
||||
input = append(input, f)
|
||||
}
|
||||
}
|
||||
|
||||
var output func([]byte) error
|
||||
if outputArg == "" {
|
||||
if len(inputArgs) > 1 {
|
||||
return errors.New("-o required with multiple input files")
|
||||
}
|
||||
output = func(b []byte) error {
|
||||
_, err := os.Stdout.Write(b)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if len(inputArgs) > 1 {
|
||||
output = dirWriter(outputArg)
|
||||
} else {
|
||||
if fi, err := os.Stat(outputArg); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("unable to open %q for writing: %s", outputArg, err)
|
||||
} else if err == nil && fi.IsDir() {
|
||||
output = dirWriter(outputArg)
|
||||
} else {
|
||||
output = func(b []byte) error {
|
||||
return os.WriteFile(outputArg, b, 0666)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, f := range input {
|
||||
b, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read input: %s", err)
|
||||
}
|
||||
if err := output(encodeByteSlice(b)); err != nil {
|
||||
return fmt.Errorf("unable to write output: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
log.SetPrefix("file2fuzz: ")
|
||||
|
||||
output := flag.String("o", "", "where to write converted file(s)")
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
|
||||
if err := convert(flag.Args(), *output); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
// Copyright 2021 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 main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/internal/testenv"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if os.Getenv("GO_FILE2FUZZ_TEST_IS_FILE2FUZZ") != "" {
|
||||
main()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
var f2f struct {
|
||||
once sync.Once
|
||||
path string
|
||||
err error
|
||||
}
|
||||
|
||||
func file2fuzz(t *testing.T, dir string, args []string, stdin string) (string, bool) {
|
||||
testenv.NeedsExec(t)
|
||||
|
||||
f2f.once.Do(func() {
|
||||
f2f.path, f2f.err = os.Executable()
|
||||
})
|
||||
if f2f.err != nil {
|
||||
t.Fatal(f2f.err)
|
||||
}
|
||||
|
||||
cmd := exec.Command(f2f.path, args...)
|
||||
cmd.Dir = dir
|
||||
cmd.Env = append(os.Environ(), "PWD="+dir, "GO_FILE2FUZZ_TEST_IS_FILE2FUZZ=1")
|
||||
if stdin != "" {
|
||||
cmd.Stdin = strings.NewReader(stdin)
|
||||
}
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return string(out), true
|
||||
}
|
||||
return string(out), false
|
||||
}
|
||||
|
||||
func TestFile2Fuzz(t *testing.T) {
|
||||
type file struct {
|
||||
name string
|
||||
dir bool
|
||||
content string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
stdin string
|
||||
inputFiles []file
|
||||
expectedStdout string
|
||||
expectedFiles []file
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "stdin, stdout",
|
||||
stdin: "hello",
|
||||
expectedStdout: "go test fuzz v1\n[]byte(\"hello\")",
|
||||
},
|
||||
{
|
||||
name: "stdin, output file",
|
||||
stdin: "hello",
|
||||
args: []string{"-o", "output"},
|
||||
expectedFiles: []file{{name: "output", content: "go test fuzz v1\n[]byte(\"hello\")"}},
|
||||
},
|
||||
{
|
||||
name: "stdin, output directory",
|
||||
stdin: "hello",
|
||||
args: []string{"-o", "output"},
|
||||
inputFiles: []file{{name: "output", dir: true}},
|
||||
expectedFiles: []file{{name: "output/ffc7b87a0377262d4f77926bd235551d78e6037bbe970d81ec39ac1d95542f7b", content: "go test fuzz v1\n[]byte(\"hello\")"}},
|
||||
},
|
||||
{
|
||||
name: "input file, output file",
|
||||
args: []string{"-o", "output", "input"},
|
||||
inputFiles: []file{{name: "input", content: "hello"}},
|
||||
expectedFiles: []file{{name: "output", content: "go test fuzz v1\n[]byte(\"hello\")"}},
|
||||
},
|
||||
{
|
||||
name: "input file, output directory",
|
||||
args: []string{"-o", "output", "input"},
|
||||
inputFiles: []file{{name: "output", dir: true}, {name: "input", content: "hello"}},
|
||||
expectedFiles: []file{{name: "output/ffc7b87a0377262d4f77926bd235551d78e6037bbe970d81ec39ac1d95542f7b", content: "go test fuzz v1\n[]byte(\"hello\")"}},
|
||||
},
|
||||
{
|
||||
name: "input files, output directory",
|
||||
args: []string{"-o", "output", "input", "input-2"},
|
||||
inputFiles: []file{{name: "output", dir: true}, {name: "input", content: "hello"}, {name: "input-2", content: "hello :)"}},
|
||||
expectedFiles: []file{
|
||||
{name: "output/ffc7b87a0377262d4f77926bd235551d78e6037bbe970d81ec39ac1d95542f7b", content: "go test fuzz v1\n[]byte(\"hello\")"},
|
||||
{name: "output/28059db30ce420ff65b2c29b749804c69c601aeca21b3cbf0644244ff080d7a5", content: "go test fuzz v1\n[]byte(\"hello :)\")"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "input files, no output",
|
||||
args: []string{"input", "input-2"},
|
||||
inputFiles: []file{{name: "output", dir: true}, {name: "input", content: "hello"}, {name: "input-2", content: "hello :)"}},
|
||||
expectedError: "file2fuzz: -o required with multiple input files\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
tmp, err := os.MkdirTemp(os.TempDir(), "file2fuzz")
|
||||
if err != nil {
|
||||
t.Fatalf("os.MkdirTemp failed: %s", err)
|
||||
}
|
||||
defer os.RemoveAll(tmp)
|
||||
for _, f := range tc.inputFiles {
|
||||
if f.dir {
|
||||
if err := os.Mkdir(filepath.Join(tmp, f.name), 0777); err != nil {
|
||||
t.Fatalf("failed to create test directory: %s", err)
|
||||
}
|
||||
} else {
|
||||
if err := os.WriteFile(filepath.Join(tmp, f.name), []byte(f.content), 0666); err != nil {
|
||||
t.Fatalf("failed to create test input file: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out, failed := file2fuzz(t, tmp, tc.args, tc.stdin)
|
||||
if failed && tc.expectedError == "" {
|
||||
t.Fatalf("file2fuzz failed unexpectedly: %s", out)
|
||||
} else if failed && out != tc.expectedError {
|
||||
t.Fatalf("file2fuzz returned unexpected error: got %q, want %q", out, tc.expectedError)
|
||||
}
|
||||
if !failed && out != tc.expectedStdout {
|
||||
t.Fatalf("file2fuzz unexpected stdout: got %q, want %q", out, tc.expectedStdout)
|
||||
}
|
||||
|
||||
for _, f := range tc.expectedFiles {
|
||||
c, err := os.ReadFile(filepath.Join(tmp, f.name))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read expected output file %q: %s", f.name, err)
|
||||
}
|
||||
if string(c) != f.content {
|
||||
t.Fatalf("expected output file %q contains unexpected content: got %s, want %s", f.name, string(c), f.content)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,525 @@
|
||||
// Copyright 2015 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 fiximports command fixes import declarations to use the canonical
|
||||
// import path for packages that have an "import comment" as defined by
|
||||
// https://golang.org/s/go14customimport.
|
||||
//
|
||||
// # Background
|
||||
//
|
||||
// The Go 1 custom import path mechanism lets the maintainer of a
|
||||
// package give it a stable name by which clients may import and "go
|
||||
// get" it, independent of the underlying version control system (such
|
||||
// as Git) or server (such as github.com) that hosts it. Requests for
|
||||
// the custom name are redirected to the underlying name. This allows
|
||||
// packages to be migrated from one underlying server or system to
|
||||
// another without breaking existing clients.
|
||||
//
|
||||
// Because this redirect mechanism creates aliases for existing
|
||||
// packages, it's possible for a single program to import the same
|
||||
// package by its canonical name and by an alias. The resulting
|
||||
// executable will contain two copies of the package, which is wasteful
|
||||
// at best and incorrect at worst.
|
||||
//
|
||||
// To avoid this, "go build" reports an error if it encounters a special
|
||||
// comment like the one below, and if the import path in the comment
|
||||
// does not match the path of the enclosing package relative to
|
||||
// GOPATH/src:
|
||||
//
|
||||
// $ grep ^package $GOPATH/src/github.com/bob/vanity/foo/foo.go
|
||||
// package foo // import "vanity.com/foo"
|
||||
//
|
||||
// The error from "go build" indicates that the package canonically
|
||||
// known as "vanity.com/foo" is locally installed under the
|
||||
// non-canonical name "github.com/bob/vanity/foo".
|
||||
//
|
||||
// # Usage
|
||||
//
|
||||
// When a package that you depend on introduces a custom import comment,
|
||||
// and your workspace imports it by the non-canonical name, your build
|
||||
// will stop working as soon as you update your copy of that package
|
||||
// using "go get -u".
|
||||
//
|
||||
// The purpose of the fiximports tool is to fix up all imports of the
|
||||
// non-canonical path within a Go workspace, replacing them with imports
|
||||
// of the canonical path. Following a run of fiximports, the workspace
|
||||
// will no longer depend on the non-canonical copy of the package, so it
|
||||
// should be safe to delete. It may be necessary to run "go get -u"
|
||||
// again to ensure that the package is locally installed under its
|
||||
// canonical path, if it was not already.
|
||||
//
|
||||
// The fiximports tool operates locally; it does not make HTTP requests
|
||||
// and does not discover new custom import comments. It only operates
|
||||
// on non-canonical packages present in your workspace.
|
||||
//
|
||||
// The -baddomains flag is a list of domain names that should always be
|
||||
// considered non-canonical. You can use this if you wish to make sure
|
||||
// that you no longer have any dependencies on packages from that
|
||||
// domain, even those that do not yet provide a canonical import path
|
||||
// comment. For example, the default value of -baddomains includes the
|
||||
// moribund code hosting site code.google.com, so fiximports will report
|
||||
// an error for each import of a package from this domain remaining
|
||||
// after canonicalization.
|
||||
//
|
||||
// To see the changes fiximports would make without applying them, use
|
||||
// the -n flag.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/format"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// flags
|
||||
var (
|
||||
dryrun = flag.Bool("n", false, "dry run: show changes, but don't apply them")
|
||||
badDomains = flag.String("baddomains", "code.google.com",
|
||||
"a comma-separated list of domains from which packages should not be imported")
|
||||
replaceFlag = flag.String("replace", "",
|
||||
"a comma-separated list of noncanonical=canonical pairs of package paths. If both items in a pair end with '...', they are treated as path prefixes.")
|
||||
)
|
||||
|
||||
// seams for testing
|
||||
var (
|
||||
stderr io.Writer = os.Stderr
|
||||
writeFile = os.WriteFile
|
||||
)
|
||||
|
||||
const usage = `fiximports: rewrite import paths to use canonical package names.
|
||||
|
||||
Usage: fiximports [-n] package...
|
||||
|
||||
The package... arguments specify a list of packages
|
||||
in the style of the go tool; see "go help packages".
|
||||
Hint: use "all" or "..." to match the entire workspace.
|
||||
|
||||
For details, see https://pkg.go.dev/golang.org/x/tools/cmd/fiximports
|
||||
|
||||
Flags:
|
||||
-n: dry run: show changes, but don't apply them
|
||||
-baddomains a comma-separated list of domains from which packages
|
||||
should not be imported
|
||||
`
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
if len(flag.Args()) == 0 {
|
||||
fmt.Fprint(stderr, usage)
|
||||
os.Exit(1)
|
||||
}
|
||||
if !fiximports(flag.Args()...) {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
type canonicalName struct{ path, name string }
|
||||
|
||||
// fiximports fixes imports in the specified packages.
|
||||
// Invariant: a false result implies an error was already printed.
|
||||
func fiximports(packages ...string) bool {
|
||||
// importedBy is the transpose of the package import graph.
|
||||
importedBy := make(map[string]map[*listPackage]bool)
|
||||
|
||||
// addEdge adds an edge to the import graph.
|
||||
addEdge := func(from *listPackage, to string) {
|
||||
if to == "C" || to == "unsafe" {
|
||||
return // fake
|
||||
}
|
||||
pkgs := importedBy[to]
|
||||
if pkgs == nil {
|
||||
pkgs = make(map[*listPackage]bool)
|
||||
importedBy[to] = pkgs
|
||||
}
|
||||
pkgs[from] = true
|
||||
}
|
||||
|
||||
// List metadata for all packages in the workspace.
|
||||
pkgs, err := list("...")
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "importfix: %v\n", err)
|
||||
return false
|
||||
}
|
||||
|
||||
// packageName maps each package's path to its name.
|
||||
packageName := make(map[string]string)
|
||||
for _, p := range pkgs {
|
||||
packageName[p.ImportPath] = p.Name
|
||||
}
|
||||
|
||||
// canonical maps each non-canonical package path to
|
||||
// its canonical path and name.
|
||||
// A present nil value indicates that the canonical package
|
||||
// is unknown: hosted on a bad domain with no redirect.
|
||||
canonical := make(map[string]canonicalName)
|
||||
domains := strings.Split(*badDomains, ",")
|
||||
|
||||
type replaceItem struct {
|
||||
old, new string
|
||||
matchPrefix bool
|
||||
}
|
||||
var replace []replaceItem
|
||||
for _, pair := range strings.Split(*replaceFlag, ",") {
|
||||
if pair == "" {
|
||||
continue
|
||||
}
|
||||
words := strings.Split(pair, "=")
|
||||
if len(words) != 2 {
|
||||
fmt.Fprintf(stderr, "importfix: -replace: %q is not of the form \"canonical=noncanonical\".\n", pair)
|
||||
return false
|
||||
}
|
||||
replace = append(replace, replaceItem{
|
||||
old: strings.TrimSuffix(words[0], "..."),
|
||||
new: strings.TrimSuffix(words[1], "..."),
|
||||
matchPrefix: strings.HasSuffix(words[0], "...") &&
|
||||
strings.HasSuffix(words[1], "..."),
|
||||
})
|
||||
}
|
||||
|
||||
// Find non-canonical packages and populate importedBy graph.
|
||||
for _, p := range pkgs {
|
||||
if p.Error != nil {
|
||||
msg := p.Error.Err
|
||||
if strings.Contains(msg, "code in directory") &&
|
||||
strings.Contains(msg, "expects import") {
|
||||
// don't show the very errors we're trying to fix
|
||||
} else {
|
||||
fmt.Fprintln(stderr, p.Error)
|
||||
}
|
||||
}
|
||||
|
||||
for _, imp := range p.Imports {
|
||||
addEdge(p, imp)
|
||||
}
|
||||
for _, imp := range p.TestImports {
|
||||
addEdge(p, imp)
|
||||
}
|
||||
for _, imp := range p.XTestImports {
|
||||
addEdge(p, imp)
|
||||
}
|
||||
|
||||
// Does package have an explicit import comment?
|
||||
if p.ImportComment != "" {
|
||||
if p.ImportComment != p.ImportPath {
|
||||
canonical[p.ImportPath] = canonicalName{
|
||||
path: p.ImportComment,
|
||||
name: p.Name,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Is package matched by a -replace item?
|
||||
var newPath string
|
||||
for _, item := range replace {
|
||||
if item.matchPrefix {
|
||||
if strings.HasPrefix(p.ImportPath, item.old) {
|
||||
newPath = item.new + p.ImportPath[len(item.old):]
|
||||
break
|
||||
}
|
||||
} else if p.ImportPath == item.old {
|
||||
newPath = item.new
|
||||
break
|
||||
}
|
||||
}
|
||||
if newPath != "" {
|
||||
newName := packageName[newPath]
|
||||
if newName == "" {
|
||||
newName = filepath.Base(newPath) // a guess
|
||||
}
|
||||
canonical[p.ImportPath] = canonicalName{
|
||||
path: newPath,
|
||||
name: newName,
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Is package matched by a -baddomains item?
|
||||
for _, domain := range domains {
|
||||
slash := strings.Index(p.ImportPath, "/")
|
||||
if slash < 0 {
|
||||
continue // no slash: standard package
|
||||
}
|
||||
if p.ImportPath[:slash] == domain {
|
||||
// Package comes from bad domain and has no import comment.
|
||||
// Report an error each time this package is imported.
|
||||
canonical[p.ImportPath] = canonicalName{}
|
||||
|
||||
// TODO(adonovan): should we make an HTTP request to
|
||||
// see if there's an HTTP redirect, a "go-import" meta tag,
|
||||
// or an import comment in the latest revision?
|
||||
// It would duplicate a lot of logic from "go get".
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find all clients (direct importers) of canonical packages.
|
||||
// These are the packages that need fixing up.
|
||||
clients := make(map[*listPackage]bool)
|
||||
for path := range canonical {
|
||||
for client := range importedBy[path] {
|
||||
clients[client] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Restrict rewrites to the set of packages specified by the user.
|
||||
if len(packages) == 1 && (packages[0] == "all" || packages[0] == "...") {
|
||||
// no restriction
|
||||
} else {
|
||||
pkgs, err := list(packages...)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "importfix: %v\n", err)
|
||||
return false
|
||||
}
|
||||
seen := make(map[string]bool)
|
||||
for _, p := range pkgs {
|
||||
seen[p.ImportPath] = true
|
||||
}
|
||||
for client := range clients {
|
||||
if !seen[client.ImportPath] {
|
||||
delete(clients, client)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rewrite selected client packages.
|
||||
ok := true
|
||||
for client := range clients {
|
||||
if !rewritePackage(client, canonical) {
|
||||
ok = false
|
||||
|
||||
// There were errors.
|
||||
// Show direct and indirect imports of client.
|
||||
seen := make(map[string]bool)
|
||||
var direct, indirect []string
|
||||
for p := range importedBy[client.ImportPath] {
|
||||
direct = append(direct, p.ImportPath)
|
||||
seen[p.ImportPath] = true
|
||||
}
|
||||
|
||||
var visit func(path string)
|
||||
visit = func(path string) {
|
||||
for q := range importedBy[path] {
|
||||
qpath := q.ImportPath
|
||||
if !seen[qpath] {
|
||||
seen[qpath] = true
|
||||
indirect = append(indirect, qpath)
|
||||
visit(qpath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if direct != nil {
|
||||
fmt.Fprintf(stderr, "\timported directly by:\n")
|
||||
sort.Strings(direct)
|
||||
for _, path := range direct {
|
||||
fmt.Fprintf(stderr, "\t\t%s\n", path)
|
||||
visit(path)
|
||||
}
|
||||
|
||||
if indirect != nil {
|
||||
fmt.Fprintf(stderr, "\timported indirectly by:\n")
|
||||
sort.Strings(indirect)
|
||||
for _, path := range indirect {
|
||||
fmt.Fprintf(stderr, "\t\t%s\n", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ok
|
||||
}
|
||||
|
||||
// Invariant: false result => error already printed.
|
||||
func rewritePackage(client *listPackage, canonical map[string]canonicalName) bool {
|
||||
ok := true
|
||||
|
||||
used := make(map[string]bool)
|
||||
var filenames []string
|
||||
filenames = append(filenames, client.GoFiles...)
|
||||
filenames = append(filenames, client.TestGoFiles...)
|
||||
filenames = append(filenames, client.XTestGoFiles...)
|
||||
var first bool
|
||||
for _, filename := range filenames {
|
||||
if !first {
|
||||
first = true
|
||||
fmt.Fprintf(stderr, "%s\n", client.ImportPath)
|
||||
}
|
||||
err := rewriteFile(filepath.Join(client.Dir, filename), canonical, used)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "\tERROR: %v\n", err)
|
||||
ok = false
|
||||
}
|
||||
}
|
||||
|
||||
// Show which imports were renamed in this package.
|
||||
var keys []string
|
||||
for key := range used {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
if p := canonical[key]; p.path != "" {
|
||||
fmt.Fprintf(stderr, "\tfixed: %s -> %s\n", key, p.path)
|
||||
} else {
|
||||
fmt.Fprintf(stderr, "\tERROR: %s has no import comment\n", key)
|
||||
ok = false
|
||||
}
|
||||
}
|
||||
|
||||
return ok
|
||||
}
|
||||
|
||||
// rewriteFile reads, modifies, and writes filename, replacing all imports
|
||||
// of packages P in canonical by canonical[P].
|
||||
// It records in used which canonical packages were imported.
|
||||
// used[P]=="" indicates that P was imported but its canonical path is unknown.
|
||||
func rewriteFile(filename string, canonical map[string]canonicalName, used map[string]bool) error {
|
||||
fset := token.NewFileSet()
|
||||
f, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var changed bool
|
||||
for _, imp := range f.Imports {
|
||||
impPath, err := strconv.Unquote(imp.Path.Value)
|
||||
if err != nil {
|
||||
log.Printf("%s: bad import spec %q: %v",
|
||||
fset.Position(imp.Pos()), imp.Path.Value, err)
|
||||
continue
|
||||
}
|
||||
canon, ok := canonical[impPath]
|
||||
if !ok {
|
||||
continue // import path is canonical
|
||||
}
|
||||
|
||||
used[impPath] = true
|
||||
|
||||
if canon.path == "" {
|
||||
// The canonical path is unknown (a -baddomain).
|
||||
// Show the offending import.
|
||||
// TODO(adonovan): should we show the actual source text?
|
||||
fmt.Fprintf(stderr, "\t%s:%d: import %q\n",
|
||||
shortPath(filename),
|
||||
fset.Position(imp.Pos()).Line, impPath)
|
||||
continue
|
||||
}
|
||||
|
||||
changed = true
|
||||
|
||||
imp.Path.Value = strconv.Quote(canon.path)
|
||||
|
||||
// Add a renaming import if necessary.
|
||||
//
|
||||
// This is a guess at best. We can't see whether a 'go
|
||||
// get' of the canonical import path would have the same
|
||||
// name or not. Assume it's the last segment.
|
||||
newBase := path.Base(canon.path)
|
||||
if imp.Name == nil && newBase != canon.name {
|
||||
imp.Name = &ast.Ident{Name: canon.name}
|
||||
}
|
||||
}
|
||||
|
||||
if changed && !*dryrun {
|
||||
var buf bytes.Buffer
|
||||
if err := format.Node(&buf, fset, f); err != nil {
|
||||
return fmt.Errorf("%s: couldn't format file: %v", filename, err)
|
||||
}
|
||||
return writeFile(filename, buf.Bytes(), 0644)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// listPackage corresponds to the output of go list -json,
|
||||
// but only the fields we need.
|
||||
type listPackage struct {
|
||||
Name string
|
||||
Dir string
|
||||
ImportPath string
|
||||
GoFiles []string
|
||||
TestGoFiles []string
|
||||
XTestGoFiles []string
|
||||
Imports []string
|
||||
TestImports []string
|
||||
XTestImports []string
|
||||
ImportComment string
|
||||
Error *packageError // error loading package
|
||||
}
|
||||
|
||||
// A packageError describes an error loading information about a package.
|
||||
type packageError struct {
|
||||
ImportStack []string // shortest path from package named on command line to this one
|
||||
Pos string // position of error
|
||||
Err string // the error itself
|
||||
}
|
||||
|
||||
func (e packageError) Error() string {
|
||||
if e.Pos != "" {
|
||||
return e.Pos + ": " + e.Err
|
||||
}
|
||||
return e.Err
|
||||
}
|
||||
|
||||
// list runs 'go list' with the specified arguments and returns the
|
||||
// metadata for matching packages.
|
||||
func list(args ...string) ([]*listPackage, error) {
|
||||
cmd := exec.Command("go", append([]string{"list", "-e", "-json"}, args...)...)
|
||||
cmd.Stdout = new(bytes.Buffer)
|
||||
cmd.Stderr = stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dec := json.NewDecoder(cmd.Stdout.(io.Reader))
|
||||
var pkgs []*listPackage
|
||||
for {
|
||||
var p listPackage
|
||||
if err := dec.Decode(&p); err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pkgs = append(pkgs, &p)
|
||||
}
|
||||
return pkgs, nil
|
||||
}
|
||||
|
||||
// cwd contains the current working directory of the tool.
|
||||
//
|
||||
// It is initialized directly so that its value will be set for any other
|
||||
// package variables or init functions that depend on it, such as the gopath
|
||||
// variable in main_test.go.
|
||||
var cwd string = func() string {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatalf("os.Getwd: %v", err)
|
||||
}
|
||||
return cwd
|
||||
}()
|
||||
|
||||
// shortPath returns an absolute or relative name for path, whatever is shorter.
|
||||
// Plundered from $GOROOT/src/cmd/go/build.go.
|
||||
func shortPath(path string) string {
|
||||
if rel, err := filepath.Rel(cwd, path); err == nil && len(rel) < len(path) {
|
||||
return rel
|
||||
}
|
||||
return path
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
// Copyright 2015 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.
|
||||
|
||||
// No testdata on Android.
|
||||
|
||||
//go:build !android
|
||||
// +build !android
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/internal/testenv"
|
||||
)
|
||||
|
||||
// TODO(adonovan):
|
||||
// - test introduction of renaming imports.
|
||||
// - test induced failures of rewriteFile.
|
||||
|
||||
// Guide to the test packages:
|
||||
//
|
||||
// new.com/one -- canonical name for old.com/one
|
||||
// old.com/one -- non-canonical; has import comment "new.com/one"
|
||||
// old.com/bad -- has a parse error
|
||||
// fruit.io/orange \
|
||||
// fruit.io/banana } orange -> pear -> banana -> titanic.biz/bar
|
||||
// fruit.io/pear /
|
||||
// titanic.biz/bar -- domain is sinking; package has jumped ship to new.com/bar
|
||||
// titanic.biz/foo -- domain is sinking but package has no import comment yet
|
||||
|
||||
var gopath = filepath.Join(cwd, "testdata")
|
||||
|
||||
func init() {
|
||||
if err := os.Setenv("GOPATH", gopath); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// This test currently requires GOPATH mode.
|
||||
// Explicitly disabling module mode should suffix, but
|
||||
// we'll also turn off GOPROXY just for good measure.
|
||||
if err := os.Setenv("GO111MODULE", "off"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := os.Setenv("GOPROXY", "off"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFixImports(t *testing.T) {
|
||||
if os.Getenv("GO_BUILDER_NAME") == "plan9-arm" {
|
||||
t.Skipf("skipping test that times out on plan9-arm; see https://go.dev/issue/50775")
|
||||
}
|
||||
testenv.NeedsTool(t, "go")
|
||||
|
||||
defer func() {
|
||||
stderr = os.Stderr
|
||||
*badDomains = "code.google.com"
|
||||
*replaceFlag = ""
|
||||
}()
|
||||
|
||||
for i, test := range []struct {
|
||||
packages []string // packages to rewrite, "go list" syntax
|
||||
badDomains string // -baddomains flag
|
||||
replaceFlag string // -replace flag
|
||||
wantOK bool
|
||||
wantStderr string
|
||||
wantRewrite map[string]string
|
||||
}{
|
||||
// #0. No errors.
|
||||
{
|
||||
packages: []string{"all"},
|
||||
badDomains: "code.google.com",
|
||||
wantOK: true,
|
||||
wantStderr: `
|
||||
testdata/src/old.com/bad/bad.go:2:43: expected 'package', found 'EOF'
|
||||
fruit.io/banana
|
||||
fixed: old.com/one -> new.com/one
|
||||
fixed: titanic.biz/bar -> new.com/bar
|
||||
`,
|
||||
wantRewrite: map[string]string{
|
||||
"$GOPATH/src/fruit.io/banana/banana.go": `package banana
|
||||
|
||||
import (
|
||||
_ "new.com/bar"
|
||||
_ "new.com/one"
|
||||
_ "titanic.biz/foo"
|
||||
)`,
|
||||
},
|
||||
},
|
||||
// #1. No packages needed rewriting.
|
||||
{
|
||||
packages: []string{"titanic.biz/...", "old.com/...", "new.com/..."},
|
||||
badDomains: "code.google.com",
|
||||
wantOK: true,
|
||||
wantStderr: `
|
||||
testdata/src/old.com/bad/bad.go:2:43: expected 'package', found 'EOF'
|
||||
`,
|
||||
},
|
||||
// #2. Some packages without import comments matched bad domains.
|
||||
{
|
||||
packages: []string{"all"},
|
||||
badDomains: "titanic.biz",
|
||||
wantOK: false,
|
||||
wantStderr: `
|
||||
testdata/src/old.com/bad/bad.go:2:43: expected 'package', found 'EOF'
|
||||
fruit.io/banana
|
||||
testdata/src/fruit.io/banana/banana.go:6: import "titanic.biz/foo"
|
||||
fixed: old.com/one -> new.com/one
|
||||
fixed: titanic.biz/bar -> new.com/bar
|
||||
ERROR: titanic.biz/foo has no import comment
|
||||
imported directly by:
|
||||
fruit.io/pear
|
||||
imported indirectly by:
|
||||
fruit.io/orange
|
||||
`,
|
||||
wantRewrite: map[string]string{
|
||||
"$GOPATH/src/fruit.io/banana/banana.go": `package banana
|
||||
|
||||
import (
|
||||
_ "new.com/bar"
|
||||
_ "new.com/one"
|
||||
_ "titanic.biz/foo"
|
||||
)`,
|
||||
},
|
||||
},
|
||||
// #3. The -replace flag lets user supply missing import comments.
|
||||
{
|
||||
packages: []string{"all"},
|
||||
replaceFlag: "titanic.biz/foo=new.com/foo",
|
||||
wantOK: true,
|
||||
wantStderr: `
|
||||
testdata/src/old.com/bad/bad.go:2:43: expected 'package', found 'EOF'
|
||||
fruit.io/banana
|
||||
fixed: old.com/one -> new.com/one
|
||||
fixed: titanic.biz/bar -> new.com/bar
|
||||
fixed: titanic.biz/foo -> new.com/foo
|
||||
`,
|
||||
wantRewrite: map[string]string{
|
||||
"$GOPATH/src/fruit.io/banana/banana.go": `package banana
|
||||
|
||||
import (
|
||||
_ "new.com/bar"
|
||||
_ "new.com/foo"
|
||||
_ "new.com/one"
|
||||
)`,
|
||||
},
|
||||
},
|
||||
// #4. The -replace flag supports wildcards.
|
||||
// An explicit import comment takes precedence.
|
||||
{
|
||||
packages: []string{"all"},
|
||||
replaceFlag: "titanic.biz/...=new.com/...",
|
||||
wantOK: true,
|
||||
wantStderr: `
|
||||
testdata/src/old.com/bad/bad.go:2:43: expected 'package', found 'EOF'
|
||||
fruit.io/banana
|
||||
fixed: old.com/one -> new.com/one
|
||||
fixed: titanic.biz/bar -> new.com/bar
|
||||
fixed: titanic.biz/foo -> new.com/foo
|
||||
`,
|
||||
wantRewrite: map[string]string{
|
||||
"$GOPATH/src/fruit.io/banana/banana.go": `package banana
|
||||
|
||||
import (
|
||||
_ "new.com/bar"
|
||||
_ "new.com/foo"
|
||||
_ "new.com/one"
|
||||
)`,
|
||||
},
|
||||
},
|
||||
// #5. The -replace flag trumps -baddomains.
|
||||
{
|
||||
packages: []string{"all"},
|
||||
badDomains: "titanic.biz",
|
||||
replaceFlag: "titanic.biz/foo=new.com/foo",
|
||||
wantOK: true,
|
||||
wantStderr: `
|
||||
testdata/src/old.com/bad/bad.go:2:43: expected 'package', found 'EOF'
|
||||
fruit.io/banana
|
||||
fixed: old.com/one -> new.com/one
|
||||
fixed: titanic.biz/bar -> new.com/bar
|
||||
fixed: titanic.biz/foo -> new.com/foo
|
||||
`,
|
||||
wantRewrite: map[string]string{
|
||||
"$GOPATH/src/fruit.io/banana/banana.go": `package banana
|
||||
|
||||
import (
|
||||
_ "new.com/bar"
|
||||
_ "new.com/foo"
|
||||
_ "new.com/one"
|
||||
)`,
|
||||
},
|
||||
},
|
||||
} {
|
||||
*badDomains = test.badDomains
|
||||
*replaceFlag = test.replaceFlag
|
||||
|
||||
stderr = new(bytes.Buffer)
|
||||
gotRewrite := make(map[string]string)
|
||||
writeFile = func(filename string, content []byte, mode os.FileMode) error {
|
||||
filename = strings.Replace(filename, gopath, "$GOPATH", 1)
|
||||
filename = filepath.ToSlash(filename)
|
||||
gotRewrite[filename] = string(bytes.TrimSpace(content))
|
||||
return nil
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
test.wantStderr = strings.Replace(test.wantStderr, `testdata/src/old.com/bad/bad.go`, `testdata\src\old.com\bad\bad.go`, -1)
|
||||
test.wantStderr = strings.Replace(test.wantStderr, `testdata/src/fruit.io/banana/banana.go`, `testdata\src\fruit.io\banana\banana.go`, -1)
|
||||
}
|
||||
test.wantStderr = strings.TrimSpace(test.wantStderr)
|
||||
|
||||
// Check status code.
|
||||
if fiximports(test.packages...) != test.wantOK {
|
||||
t.Errorf("#%d. fiximports() = %t", i, !test.wantOK)
|
||||
}
|
||||
|
||||
// Compare stderr output.
|
||||
if got := strings.TrimSpace(stderr.(*bytes.Buffer).String()); got != test.wantStderr {
|
||||
if strings.Contains(got, "vendor/golang_org/x/text/unicode/norm") {
|
||||
t.Skip("skipping known-broken test; see golang.org/issue/17417")
|
||||
}
|
||||
t.Errorf("#%d. stderr: got <<\n%s\n>>, want <<\n%s\n>>",
|
||||
i, got, test.wantStderr)
|
||||
}
|
||||
|
||||
// Compare rewrites.
|
||||
for k, v := range gotRewrite {
|
||||
if test.wantRewrite[k] != v {
|
||||
t.Errorf("#%d. rewrite[%s] = <<%s>>, want <<%s>>",
|
||||
i, k, v, test.wantRewrite[k])
|
||||
}
|
||||
delete(test.wantRewrite, k)
|
||||
}
|
||||
for k, v := range test.wantRewrite {
|
||||
t.Errorf("#%d. rewrite[%s] missing, want <<%s>>", i, k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestDryRun tests that the -n flag suppresses calls to writeFile.
|
||||
func TestDryRun(t *testing.T) {
|
||||
if os.Getenv("GO_BUILDER_NAME") == "plan9-arm" {
|
||||
t.Skipf("skipping test that times out on plan9-arm; see https://go.dev/issue/50775")
|
||||
}
|
||||
testenv.NeedsTool(t, "go")
|
||||
|
||||
*dryrun = true
|
||||
defer func() { *dryrun = false }() // restore
|
||||
stderr = new(bytes.Buffer)
|
||||
writeFile = func(filename string, content []byte, mode os.FileMode) error {
|
||||
t.Fatalf("writeFile(%s) called in dryrun mode", filename)
|
||||
return nil
|
||||
}
|
||||
|
||||
if !fiximports("all") {
|
||||
t.Fatalf("fiximports failed: %s", stderr)
|
||||
}
|
||||
}
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
package banana
|
||||
|
||||
import (
|
||||
_ "old.com/one"
|
||||
_ "titanic.biz/bar"
|
||||
_ "titanic.biz/foo"
|
||||
)
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
package orange
|
||||
|
||||
import _ "fruit.io/pear"
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package pear
|
||||
|
||||
import _ "fruit.io/banana"
|
||||
+1
@@ -0,0 +1 @@
|
||||
package one // import "new.com/one"
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
// This ill-formed Go source file is here to ensure the tool is robust
|
||||
// against bad packages in the workspace.
|
||||
+1
@@ -0,0 +1 @@
|
||||
package one // import "new.com/one"
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
// This package is moving to new.com too.
|
||||
package bar // import "new.com/bar"
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
// This package hasn't jumped ship yet.
|
||||
package foo
|
||||
@@ -0,0 +1,288 @@
|
||||
// Copyright 2017 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 go-contrib-init command helps new Go contributors get their development
|
||||
// environment set up for the Go contribution process.
|
||||
//
|
||||
// It aims to be a complement or alternative to https://golang.org/doc/contribute.html.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/build"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
repo = flag.String("repo", detectrepo(), "Which go repo you want to contribute to. Use \"go\" for the core, or e.g. \"net\" for golang.org/x/net/*")
|
||||
dry = flag.Bool("dry-run", false, "Fail with problems instead of trying to fix things.")
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
flag.Parse()
|
||||
|
||||
checkCLA()
|
||||
checkGoroot()
|
||||
checkWorkingDir()
|
||||
checkGitOrigin()
|
||||
checkGitCodeReview()
|
||||
fmt.Print("All good. Happy hacking!\n" +
|
||||
"Remember to squash your revised commits and preserve the magic Change-Id lines.\n" +
|
||||
"Next steps: https://golang.org/doc/contribute.html#commit_changes\n")
|
||||
}
|
||||
|
||||
func detectrepo() string {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "go"
|
||||
}
|
||||
|
||||
for _, path := range filepath.SplitList(build.Default.GOPATH) {
|
||||
rightdir := filepath.Join(path, "src", "golang.org", "x") + string(os.PathSeparator)
|
||||
if strings.HasPrefix(wd, rightdir) {
|
||||
tail := wd[len(rightdir):]
|
||||
end := strings.Index(tail, string(os.PathSeparator))
|
||||
if end > 0 {
|
||||
repo := tail[:end]
|
||||
return repo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "go"
|
||||
}
|
||||
|
||||
var googleSourceRx = regexp.MustCompile(`(?m)^(go|go-review)?\.googlesource.com\b`)
|
||||
|
||||
func checkCLA() {
|
||||
slurp, err := os.ReadFile(cookiesFile())
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if googleSourceRx.Match(slurp) {
|
||||
// Probably good.
|
||||
return
|
||||
}
|
||||
log.Fatal("Your .gitcookies file isn't configured.\n" +
|
||||
"Next steps:\n" +
|
||||
" * Submit a CLA (https://golang.org/doc/contribute.html#cla) if not done\n" +
|
||||
" * Go to https://go.googlesource.com/ and click \"Generate Password\" at the top,\n" +
|
||||
" then follow instructions.\n" +
|
||||
" * Run go-contrib-init again.\n")
|
||||
}
|
||||
|
||||
func expandUser(s string) string {
|
||||
env := "HOME"
|
||||
if runtime.GOOS == "windows" {
|
||||
env = "USERPROFILE"
|
||||
} else if runtime.GOOS == "plan9" {
|
||||
env = "home"
|
||||
}
|
||||
home := os.Getenv(env)
|
||||
if home == "" {
|
||||
return s
|
||||
}
|
||||
|
||||
if len(s) >= 2 && s[0] == '~' && os.IsPathSeparator(s[1]) {
|
||||
if runtime.GOOS == "windows" {
|
||||
s = filepath.ToSlash(filepath.Join(home, s[2:]))
|
||||
} else {
|
||||
s = filepath.Join(home, s[2:])
|
||||
}
|
||||
}
|
||||
return os.Expand(s, func(env string) string {
|
||||
if env == "HOME" {
|
||||
return home
|
||||
}
|
||||
return os.Getenv(env)
|
||||
})
|
||||
}
|
||||
|
||||
func cookiesFile() string {
|
||||
out, _ := exec.Command("git", "config", "http.cookiefile").Output()
|
||||
if s := strings.TrimSpace(string(out)); s != "" {
|
||||
if strings.HasPrefix(s, "~") {
|
||||
s = expandUser(s)
|
||||
}
|
||||
return s
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
return filepath.Join(os.Getenv("USERPROFILE"), ".gitcookies")
|
||||
}
|
||||
return filepath.Join(os.Getenv("HOME"), ".gitcookies")
|
||||
}
|
||||
|
||||
func checkGoroot() {
|
||||
v := os.Getenv("GOROOT")
|
||||
if v == "" {
|
||||
return
|
||||
}
|
||||
if *repo == "go" {
|
||||
if strings.HasPrefix(v, "/usr/") {
|
||||
log.Fatalf("Your GOROOT environment variable is set to %q\n"+
|
||||
"This is almost certainly not what you want. Either unset\n"+
|
||||
"your GOROOT or set it to the path of your development version\n"+
|
||||
"of Go.", v)
|
||||
}
|
||||
slurp, err := os.ReadFile(filepath.Join(v, "VERSION"))
|
||||
if err == nil {
|
||||
slurp = bytes.TrimSpace(slurp)
|
||||
log.Fatalf("Your GOROOT environment variable is set to %q\n"+
|
||||
"But that path is to a binary release of Go, with VERSION file %q.\n"+
|
||||
"You should hack on Go in a fresh checkout of Go. Fix or unset your GOROOT.\n",
|
||||
v, slurp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkWorkingDir() {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if *repo == "go" {
|
||||
if inGoPath(wd) {
|
||||
log.Fatalf(`You can't work on Go from within your GOPATH. Please checkout Go outside of your GOPATH
|
||||
|
||||
Current directory: %s
|
||||
GOPATH: %s
|
||||
`, wd, os.Getenv("GOPATH"))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
gopath := firstGoPath()
|
||||
if gopath == "" {
|
||||
log.Fatal("Your GOPATH is not set, please set it")
|
||||
}
|
||||
|
||||
rightdir := filepath.Join(gopath, "src", "golang.org", "x", *repo)
|
||||
if !strings.HasPrefix(wd, rightdir) {
|
||||
dirExists, err := exists(rightdir)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if !dirExists {
|
||||
log.Fatalf("The repo you want to work on is currently not on your system.\n"+
|
||||
"Run %q to obtain this repo\n"+
|
||||
"then go to the directory %q\n",
|
||||
"go get -d golang.org/x/"+*repo, rightdir)
|
||||
}
|
||||
log.Fatalf("Your current directory is:%q\n"+
|
||||
"Working on golang/x/%v requires you be in %q\n",
|
||||
wd, *repo, rightdir)
|
||||
}
|
||||
}
|
||||
|
||||
func firstGoPath() string {
|
||||
list := filepath.SplitList(build.Default.GOPATH)
|
||||
if len(list) < 1 {
|
||||
return ""
|
||||
}
|
||||
return list[0]
|
||||
}
|
||||
|
||||
func exists(path string) (bool, error) {
|
||||
_, err := os.Stat(path)
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
return true, err
|
||||
}
|
||||
|
||||
func inGoPath(wd string) bool {
|
||||
if os.Getenv("GOPATH") == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, path := range filepath.SplitList(os.Getenv("GOPATH")) {
|
||||
if strings.HasPrefix(wd, filepath.Join(path, "src")) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// mostly check that they didn't clone from github
|
||||
func checkGitOrigin() {
|
||||
if _, err := exec.LookPath("git"); err != nil {
|
||||
log.Fatalf("You don't appear to have git installed. Do that.")
|
||||
}
|
||||
wantRemote := "https://go.googlesource.com/" + *repo
|
||||
remotes, err := exec.Command("git", "remote", "-v").Output()
|
||||
if err != nil {
|
||||
msg := cmdErr(err)
|
||||
if strings.Contains(msg, "Not a git repository") {
|
||||
log.Fatalf("Your current directory is not in a git checkout of %s", wantRemote)
|
||||
}
|
||||
log.Fatalf("Error running git remote -v: %v", msg)
|
||||
}
|
||||
matches := 0
|
||||
for _, line := range strings.Split(string(remotes), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if !strings.HasPrefix(line, "origin") {
|
||||
continue
|
||||
}
|
||||
if !strings.Contains(line, wantRemote) {
|
||||
curRemote := strings.Fields(strings.TrimPrefix(line, "origin"))[0]
|
||||
// TODO: if not in dryRun mode, just fix it?
|
||||
log.Fatalf("Current directory's git was cloned from %q; origin should be %q", curRemote, wantRemote)
|
||||
}
|
||||
matches++
|
||||
}
|
||||
if matches == 0 {
|
||||
log.Fatalf("git remote -v output didn't contain expected %q. Got:\n%s", wantRemote, remotes)
|
||||
}
|
||||
}
|
||||
|
||||
func cmdErr(err error) string {
|
||||
if ee, ok := err.(*exec.ExitError); ok && len(ee.Stderr) > 0 {
|
||||
return fmt.Sprintf("%s: %s", err, ee.Stderr)
|
||||
}
|
||||
return fmt.Sprint(err)
|
||||
}
|
||||
|
||||
func checkGitCodeReview() {
|
||||
if _, err := exec.LookPath("git-codereview"); err != nil {
|
||||
if *dry {
|
||||
log.Fatalf("You don't appear to have git-codereview tool. While this is technically optional,\n" +
|
||||
"almost all Go contributors use it. Our documentation and this tool assume it is used.\n" +
|
||||
"To install it, run:\n\n\t$ go get golang.org/x/review/git-codereview\n\n(Then run go-contrib-init again)")
|
||||
}
|
||||
err := exec.Command("go", "get", "golang.org/x/review/git-codereview").Run()
|
||||
if err != nil {
|
||||
log.Fatalf("Error running go get golang.org/x/review/git-codereview: %v", cmdErr(err))
|
||||
}
|
||||
log.Printf("Installed git-codereview (ran `go get golang.org/x/review/git-codereview`)")
|
||||
}
|
||||
missing := false
|
||||
for _, cmd := range []string{"change", "gofmt", "mail", "pending", "submit", "sync"} {
|
||||
v, _ := exec.Command("git", "config", "alias."+cmd).Output()
|
||||
if strings.Contains(string(v), "codereview") {
|
||||
continue
|
||||
}
|
||||
if *dry {
|
||||
log.Printf("Missing alias. Run:\n\t$ git config alias.%s \"codereview %s\"", cmd, cmd)
|
||||
missing = true
|
||||
} else {
|
||||
err := exec.Command("git", "config", "alias."+cmd, "codereview "+cmd).Run()
|
||||
if err != nil {
|
||||
log.Fatalf("Error setting alias.%s: %v", cmd, cmdErr(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
if missing {
|
||||
log.Fatalf("Missing aliases. (While optional, this tool assumes you use them.)")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright 2017 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 main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExpandUser(t *testing.T) {
|
||||
env := "HOME"
|
||||
if runtime.GOOS == "windows" {
|
||||
env = "USERPROFILE"
|
||||
} else if runtime.GOOS == "plan9" {
|
||||
env = "home"
|
||||
}
|
||||
|
||||
oldenv := os.Getenv(env)
|
||||
os.Setenv(env, "/home/gopher")
|
||||
defer os.Setenv(env, oldenv)
|
||||
|
||||
tests := []struct {
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{input: "~/foo", want: "/home/gopher/foo"},
|
||||
{input: "${HOME}/foo", want: "/home/gopher/foo"},
|
||||
{input: "/~/foo", want: "/~/foo"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := expandUser(tt.input)
|
||||
if got != tt.want {
|
||||
t.Fatalf("want %q, but %q", tt.want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCmdErr(t *testing.T) {
|
||||
tests := []struct {
|
||||
input error
|
||||
want string
|
||||
}{
|
||||
{input: errors.New("cmd error"), want: "cmd error"},
|
||||
{input: &exec.ExitError{ProcessState: nil, Stderr: nil}, want: "<nil>"},
|
||||
{input: &exec.ExitError{ProcessState: nil, Stderr: []byte("test")}, want: "<nil>: test"},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
got := cmdErr(tt.input)
|
||||
if got != tt.want {
|
||||
t.Fatalf("%d. got %q, want %q", i, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright 2014 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 godex command prints (dumps) exported information of packages
|
||||
// or selected package objects.
|
||||
//
|
||||
// In contrast to godoc, godex extracts this information from compiled
|
||||
// object files. Hence the exported data is truly what a compiler will
|
||||
// see, at the cost of missing commentary.
|
||||
//
|
||||
// Usage: godex [flags] {path[.name]}
|
||||
//
|
||||
// Each argument must be a (possibly partial) package path, optionally
|
||||
// followed by a dot and the name of a package object:
|
||||
//
|
||||
// godex math
|
||||
// godex math.Sin
|
||||
// godex math.Sin fmt.Printf
|
||||
// godex go/types
|
||||
//
|
||||
// godex automatically tries all possible package path prefixes if only a
|
||||
// partial package path is given. For instance, for the path "go/types",
|
||||
// godex prepends "golang.org/x/tools".
|
||||
//
|
||||
// The prefixes are computed by searching the directories specified by
|
||||
// the GOROOT and GOPATH environment variables (and by excluding the
|
||||
// build OS- and architecture-specific directory names from the path).
|
||||
// The search order is depth-first and alphabetic; for a partial path
|
||||
// "foo", a package "a/foo" is found before "b/foo".
|
||||
//
|
||||
// Absolute and relative paths may be provided, which disable automatic
|
||||
// prefix generation:
|
||||
//
|
||||
// godex $GOROOT/pkg/darwin_amd64/sort
|
||||
// godex ./sort
|
||||
//
|
||||
// All but the last path element may contain dots; a dot in the last path
|
||||
// element separates the package path from the package object name. If the
|
||||
// last path element contains a dot, terminate the argument with another
|
||||
// dot (indicating an empty object name). For instance, the path for a
|
||||
// package foo.bar would be specified as in:
|
||||
//
|
||||
// godex foo.bar.
|
||||
//
|
||||
// The flags are:
|
||||
//
|
||||
// -s=""
|
||||
// only consider packages from src, where src is one of the supported compilers
|
||||
// -v=false
|
||||
// verbose mode
|
||||
//
|
||||
// The following sources (-s arguments) are supported:
|
||||
//
|
||||
// gc
|
||||
// gc-generated object files
|
||||
// gccgo
|
||||
// gccgo-generated object files
|
||||
// gccgo-new
|
||||
// gccgo-generated object files using a condensed format (experimental)
|
||||
// source
|
||||
// (uncompiled) source code (not yet implemented)
|
||||
//
|
||||
// If no -s argument is provided, godex will try to find a matching source.
|
||||
package main // import "golang.org/x/tools/cmd/godex"
|
||||
|
||||
// BUG(gri): support for -s=source is not yet implemented
|
||||
// BUG(gri): gccgo-importing appears to have occasional problems stalling godex; try -s=gc as work-around
|
||||
@@ -0,0 +1,16 @@
|
||||
// Copyright 2014 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 implements access to gc-generated export data.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"go/importer"
|
||||
"go/token"
|
||||
)
|
||||
|
||||
func init() {
|
||||
register("gc", importer.ForCompiler(token.NewFileSet(), "gc", nil))
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright 2014 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 implements access to gccgo-generated export data.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"go/importer"
|
||||
"go/token"
|
||||
"go/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
register("gccgo", importer.ForCompiler(token.NewFileSet(), "gccgo", nil))
|
||||
}
|
||||
|
||||
// Print the extra gccgo compiler data for this package, if it exists.
|
||||
func (p *printer) printGccgoExtra(pkg *types.Package) {
|
||||
// Disabled for now.
|
||||
// TODO(gri) address this at some point.
|
||||
|
||||
// if initdata, ok := initmap[pkg]; ok {
|
||||
// p.printf("/*\npriority %d\n", initdata.Priority)
|
||||
|
||||
// p.printDecl("init", len(initdata.Inits), func() {
|
||||
// for _, init := range initdata.Inits {
|
||||
// p.printf("%s %s %d\n", init.Name, init.InitFunc, init.Priority)
|
||||
// }
|
||||
// })
|
||||
|
||||
// p.print("*/\n")
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
// Copyright 2014 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 main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/build"
|
||||
"go/types"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
source = flag.String("s", "", "only consider packages from src, where src is one of the supported compilers")
|
||||
verbose = flag.Bool("v", false, "verbose mode")
|
||||
)
|
||||
|
||||
// lists of registered sources and corresponding importers
|
||||
var (
|
||||
sources []string
|
||||
importers []types.Importer
|
||||
errImportFailed = errors.New("import failed")
|
||||
)
|
||||
|
||||
func usage() {
|
||||
fmt.Fprintln(os.Stderr, "usage: godex [flags] {path|qualifiedIdent}")
|
||||
flag.PrintDefaults()
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
func report(msg string) {
|
||||
fmt.Fprintln(os.Stderr, "error: "+msg)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
|
||||
if flag.NArg() == 0 {
|
||||
report("no package name, path, or file provided")
|
||||
}
|
||||
|
||||
var imp types.Importer = new(tryImporters)
|
||||
if *source != "" {
|
||||
imp = lookup(*source)
|
||||
if imp == nil {
|
||||
report("source (-s argument) must be one of: " + strings.Join(sources, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
for _, arg := range flag.Args() {
|
||||
path, name := splitPathIdent(arg)
|
||||
logf("\tprocessing %q: path = %q, name = %s\n", arg, path, name)
|
||||
|
||||
// generate possible package path prefixes
|
||||
// (at the moment we do this for each argument - should probably cache the generated prefixes)
|
||||
prefixes := make(chan string)
|
||||
go genPrefixes(prefixes, !filepath.IsAbs(path) && !build.IsLocalImport(path))
|
||||
|
||||
// import package
|
||||
pkg, err := tryPrefixes(prefixes, path, imp)
|
||||
if err != nil {
|
||||
logf("\t=> ignoring %q: %s\n", path, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// filter objects if needed
|
||||
var filter func(types.Object) bool
|
||||
if name != "" {
|
||||
filter = func(obj types.Object) bool {
|
||||
// TODO(gri) perhaps use regular expression matching here?
|
||||
return obj.Name() == name
|
||||
}
|
||||
}
|
||||
|
||||
// print contents
|
||||
print(os.Stdout, pkg, filter)
|
||||
}
|
||||
}
|
||||
|
||||
func logf(format string, args ...interface{}) {
|
||||
if *verbose {
|
||||
fmt.Fprintf(os.Stderr, format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// splitPathIdent splits a path.name argument into its components.
|
||||
// All but the last path element may contain dots.
|
||||
func splitPathIdent(arg string) (path, name string) {
|
||||
if i := strings.LastIndex(arg, "."); i >= 0 {
|
||||
if j := strings.LastIndex(arg, "/"); j < i {
|
||||
// '.' is not part of path
|
||||
path = arg[:i]
|
||||
name = arg[i+1:]
|
||||
return
|
||||
}
|
||||
}
|
||||
path = arg
|
||||
return
|
||||
}
|
||||
|
||||
// tryPrefixes tries to import the package given by (the possibly partial) path using the given importer imp
|
||||
// by prepending all possible prefixes to path. It returns with the first package that it could import, or
|
||||
// with an error.
|
||||
func tryPrefixes(prefixes chan string, path string, imp types.Importer) (pkg *types.Package, err error) {
|
||||
for prefix := range prefixes {
|
||||
actual := path
|
||||
if prefix == "" {
|
||||
// don't use filepath.Join as it will sanitize the path and remove
|
||||
// a leading dot and then the path is not recognized as a relative
|
||||
// package path by the importers anymore
|
||||
logf("\ttrying no prefix\n")
|
||||
} else {
|
||||
actual = filepath.Join(prefix, path)
|
||||
logf("\ttrying prefix %q\n", prefix)
|
||||
}
|
||||
pkg, err = imp.Import(actual)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
logf("\t=> importing %q failed: %s\n", actual, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// tryImporters is an importer that tries all registered importers
|
||||
// successively until one of them succeeds or all of them failed.
|
||||
type tryImporters struct{}
|
||||
|
||||
func (t *tryImporters) Import(path string) (pkg *types.Package, err error) {
|
||||
for i, imp := range importers {
|
||||
logf("\t\ttrying %s import\n", sources[i])
|
||||
pkg, err = imp.Import(path)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
logf("\t\t=> %s import failed: %s\n", sources[i], err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type protector struct {
|
||||
imp types.Importer
|
||||
}
|
||||
|
||||
func (p *protector) Import(path string) (pkg *types.Package, err error) {
|
||||
defer func() {
|
||||
if recover() != nil {
|
||||
pkg = nil
|
||||
err = errImportFailed
|
||||
}
|
||||
}()
|
||||
return p.imp.Import(path)
|
||||
}
|
||||
|
||||
// protect protects an importer imp from panics and returns the protected importer.
|
||||
func protect(imp types.Importer) types.Importer {
|
||||
return &protector{imp}
|
||||
}
|
||||
|
||||
// register registers an importer imp for a given source src.
|
||||
func register(src string, imp types.Importer) {
|
||||
if lookup(src) != nil {
|
||||
panic(src + " importer already registered")
|
||||
}
|
||||
sources = append(sources, src)
|
||||
importers = append(importers, protect(imp))
|
||||
}
|
||||
|
||||
// lookup returns the importer imp for a given source src.
|
||||
func lookup(src string) types.Importer {
|
||||
for i, s := range sources {
|
||||
if s == src {
|
||||
return importers[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func genPrefixes(out chan string, all bool) {
|
||||
out <- ""
|
||||
if all {
|
||||
platform := build.Default.GOOS + "_" + build.Default.GOARCH
|
||||
dirnames := append([]string{build.Default.GOROOT}, filepath.SplitList(build.Default.GOPATH)...)
|
||||
for _, dirname := range dirnames {
|
||||
walkDir(filepath.Join(dirname, "pkg", platform), "", out)
|
||||
}
|
||||
}
|
||||
close(out)
|
||||
}
|
||||
|
||||
func walkDir(dirname, prefix string, out chan string) {
|
||||
fiList, err := os.ReadDir(dirname)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, fi := range fiList {
|
||||
if fi.IsDir() && !strings.HasPrefix(fi.Name(), ".") {
|
||||
prefix := filepath.Join(prefix, fi.Name())
|
||||
out <- prefix
|
||||
walkDir(filepath.Join(dirname, fi.Name()), prefix, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright 2017 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.
|
||||
|
||||
//go:build !go1.9
|
||||
// +build !go1.9
|
||||
|
||||
package main
|
||||
|
||||
import "go/types"
|
||||
|
||||
func isAlias(obj *types.TypeName) bool {
|
||||
return false // there are no type aliases before Go 1.9
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright 2017 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.
|
||||
|
||||
//go:build go1.9
|
||||
// +build go1.9
|
||||
|
||||
package main
|
||||
|
||||
import "go/types"
|
||||
|
||||
func isAlias(obj *types.TypeName) bool {
|
||||
return obj.IsAlias()
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
// Copyright 2014 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 main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/constant"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"io"
|
||||
"math/big"
|
||||
|
||||
"golang.org/x/tools/internal/aliases"
|
||||
)
|
||||
|
||||
// TODO(gri) use tabwriter for alignment?
|
||||
|
||||
func print(w io.Writer, pkg *types.Package, filter func(types.Object) bool) {
|
||||
var p printer
|
||||
p.pkg = pkg
|
||||
p.printPackage(pkg, filter)
|
||||
p.printGccgoExtra(pkg)
|
||||
io.Copy(w, &p.buf)
|
||||
}
|
||||
|
||||
type printer struct {
|
||||
pkg *types.Package
|
||||
buf bytes.Buffer
|
||||
indent int // current indentation level
|
||||
last byte // last byte written
|
||||
}
|
||||
|
||||
func (p *printer) print(s string) {
|
||||
// Write the string one byte at a time. We care about the presence of
|
||||
// newlines for indentation which we will see even in the presence of
|
||||
// (non-corrupted) Unicode; no need to read one rune at a time.
|
||||
for i := 0; i < len(s); i++ {
|
||||
ch := s[i]
|
||||
if ch != '\n' && p.last == '\n' {
|
||||
// Note: This could lead to a range overflow for very large
|
||||
// indentations, but it's extremely unlikely to happen for
|
||||
// non-pathological code.
|
||||
p.buf.WriteString("\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t"[:p.indent])
|
||||
}
|
||||
p.buf.WriteByte(ch)
|
||||
p.last = ch
|
||||
}
|
||||
}
|
||||
|
||||
func (p *printer) printf(format string, args ...interface{}) {
|
||||
p.print(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// methodsFor returns the named type and corresponding methods if the type
|
||||
// denoted by obj is not an interface and has methods. Otherwise it returns
|
||||
// the zero value.
|
||||
func methodsFor(obj *types.TypeName) (*types.Named, []*types.Selection) {
|
||||
named, _ := aliases.Unalias(obj.Type()).(*types.Named)
|
||||
if named == nil {
|
||||
// A type name's type can also be the
|
||||
// exported basic type unsafe.Pointer.
|
||||
return nil, nil
|
||||
}
|
||||
if _, ok := named.Underlying().(*types.Interface); ok {
|
||||
// ignore interfaces
|
||||
return nil, nil
|
||||
}
|
||||
methods := combinedMethodSet(named)
|
||||
if len(methods) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return named, methods
|
||||
}
|
||||
|
||||
func (p *printer) printPackage(pkg *types.Package, filter func(types.Object) bool) {
|
||||
// collect objects by kind
|
||||
var (
|
||||
consts []*types.Const
|
||||
typem []*types.Named // non-interface types with methods
|
||||
typez []*types.TypeName // interfaces or types without methods
|
||||
vars []*types.Var
|
||||
funcs []*types.Func
|
||||
builtins []*types.Builtin
|
||||
methods = make(map[*types.Named][]*types.Selection) // method sets for named types
|
||||
)
|
||||
scope := pkg.Scope()
|
||||
for _, name := range scope.Names() {
|
||||
obj := scope.Lookup(name)
|
||||
if obj.Exported() {
|
||||
// collect top-level exported and possibly filtered objects
|
||||
if filter == nil || filter(obj) {
|
||||
switch obj := obj.(type) {
|
||||
case *types.Const:
|
||||
consts = append(consts, obj)
|
||||
case *types.TypeName:
|
||||
// group into types with methods and types without
|
||||
if named, m := methodsFor(obj); named != nil {
|
||||
typem = append(typem, named)
|
||||
methods[named] = m
|
||||
} else {
|
||||
typez = append(typez, obj)
|
||||
}
|
||||
case *types.Var:
|
||||
vars = append(vars, obj)
|
||||
case *types.Func:
|
||||
funcs = append(funcs, obj)
|
||||
case *types.Builtin:
|
||||
// for unsafe.Sizeof, etc.
|
||||
builtins = append(builtins, obj)
|
||||
}
|
||||
}
|
||||
} else if filter == nil {
|
||||
// no filtering: collect top-level unexported types with methods
|
||||
if obj, _ := obj.(*types.TypeName); obj != nil {
|
||||
// see case *types.TypeName above
|
||||
if named, m := methodsFor(obj); named != nil {
|
||||
typem = append(typem, named)
|
||||
methods[named] = m
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p.printf("package %s // %q\n", pkg.Name(), pkg.Path())
|
||||
|
||||
p.printDecl("const", len(consts), func() {
|
||||
for _, obj := range consts {
|
||||
p.printObj(obj)
|
||||
p.print("\n")
|
||||
}
|
||||
})
|
||||
|
||||
p.printDecl("var", len(vars), func() {
|
||||
for _, obj := range vars {
|
||||
p.printObj(obj)
|
||||
p.print("\n")
|
||||
}
|
||||
})
|
||||
|
||||
p.printDecl("type", len(typez), func() {
|
||||
for _, obj := range typez {
|
||||
p.printf("%s ", obj.Name())
|
||||
typ := obj.Type()
|
||||
if isAlias(obj) {
|
||||
p.print("= ")
|
||||
p.writeType(p.pkg, typ)
|
||||
} else {
|
||||
p.writeType(p.pkg, typ.Underlying())
|
||||
}
|
||||
p.print("\n")
|
||||
}
|
||||
})
|
||||
|
||||
// non-interface types with methods
|
||||
for _, named := range typem {
|
||||
first := true
|
||||
if obj := named.Obj(); obj.Exported() {
|
||||
if first {
|
||||
p.print("\n")
|
||||
first = false
|
||||
}
|
||||
p.printf("type %s ", obj.Name())
|
||||
p.writeType(p.pkg, named.Underlying())
|
||||
p.print("\n")
|
||||
}
|
||||
for _, m := range methods[named] {
|
||||
if obj := m.Obj(); obj.Exported() {
|
||||
if first {
|
||||
p.print("\n")
|
||||
first = false
|
||||
}
|
||||
p.printFunc(m.Recv(), obj.(*types.Func))
|
||||
p.print("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(funcs) > 0 {
|
||||
p.print("\n")
|
||||
for _, obj := range funcs {
|
||||
p.printFunc(nil, obj)
|
||||
p.print("\n")
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(gri) better handling of builtins (package unsafe only)
|
||||
if len(builtins) > 0 {
|
||||
p.print("\n")
|
||||
for _, obj := range builtins {
|
||||
p.printf("func %s() // builtin\n", obj.Name())
|
||||
}
|
||||
}
|
||||
|
||||
p.print("\n")
|
||||
}
|
||||
|
||||
func (p *printer) printDecl(keyword string, n int, printGroup func()) {
|
||||
switch n {
|
||||
case 0:
|
||||
// nothing to do
|
||||
case 1:
|
||||
p.printf("\n%s ", keyword)
|
||||
printGroup()
|
||||
default:
|
||||
p.printf("\n%s (\n", keyword)
|
||||
p.indent++
|
||||
printGroup()
|
||||
p.indent--
|
||||
p.print(")\n")
|
||||
}
|
||||
}
|
||||
|
||||
// absInt returns the absolute value of v as a *big.Int.
|
||||
// v must be a numeric value.
|
||||
func absInt(v constant.Value) *big.Int {
|
||||
// compute big-endian representation of v
|
||||
b := constant.Bytes(v) // little-endian
|
||||
for i, j := 0, len(b)-1; i < j; i, j = i+1, j-1 {
|
||||
b[i], b[j] = b[j], b[i]
|
||||
}
|
||||
return new(big.Int).SetBytes(b)
|
||||
}
|
||||
|
||||
var (
|
||||
one = big.NewRat(1, 1)
|
||||
ten = big.NewRat(10, 1)
|
||||
)
|
||||
|
||||
// floatString returns the string representation for a
|
||||
// numeric value v in normalized floating-point format.
|
||||
func floatString(v constant.Value) string {
|
||||
if constant.Sign(v) == 0 {
|
||||
return "0.0"
|
||||
}
|
||||
// x != 0
|
||||
|
||||
// convert |v| into a big.Rat x
|
||||
x := new(big.Rat).SetFrac(absInt(constant.Num(v)), absInt(constant.Denom(v)))
|
||||
|
||||
// normalize x and determine exponent e
|
||||
// (This is not very efficient, but also not speed-critical.)
|
||||
var e int
|
||||
for x.Cmp(ten) >= 0 {
|
||||
x.Quo(x, ten)
|
||||
e++
|
||||
}
|
||||
for x.Cmp(one) < 0 {
|
||||
x.Mul(x, ten)
|
||||
e--
|
||||
}
|
||||
|
||||
// TODO(gri) Values such as 1/2 are easier to read in form 0.5
|
||||
// rather than 5.0e-1. Similarly, 1.0e1 is easier to read as
|
||||
// 10.0. Fine-tune best exponent range for readability.
|
||||
|
||||
s := x.FloatString(100) // good-enough precision
|
||||
|
||||
// trim trailing 0's
|
||||
i := len(s)
|
||||
for i > 0 && s[i-1] == '0' {
|
||||
i--
|
||||
}
|
||||
s = s[:i]
|
||||
|
||||
// add a 0 if the number ends in decimal point
|
||||
if len(s) > 0 && s[len(s)-1] == '.' {
|
||||
s += "0"
|
||||
}
|
||||
|
||||
// add exponent and sign
|
||||
if e != 0 {
|
||||
s += fmt.Sprintf("e%+d", e)
|
||||
}
|
||||
if constant.Sign(v) < 0 {
|
||||
s = "-" + s
|
||||
}
|
||||
|
||||
// TODO(gri) If v is a "small" fraction (i.e., numerator and denominator
|
||||
// are just a small number of decimal digits), add the exact fraction as
|
||||
// a comment. For instance: 3.3333...e-1 /* = 1/3 */
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// valString returns the string representation for the value v.
|
||||
// Setting floatFmt forces an integer value to be formatted in
|
||||
// normalized floating-point format.
|
||||
// TODO(gri) Move this code into package constant.
|
||||
func valString(v constant.Value, floatFmt bool) string {
|
||||
switch v.Kind() {
|
||||
case constant.Int:
|
||||
if floatFmt {
|
||||
return floatString(v)
|
||||
}
|
||||
case constant.Float:
|
||||
return floatString(v)
|
||||
case constant.Complex:
|
||||
re := constant.Real(v)
|
||||
im := constant.Imag(v)
|
||||
var s string
|
||||
if constant.Sign(re) != 0 {
|
||||
s = floatString(re)
|
||||
if constant.Sign(im) >= 0 {
|
||||
s += " + "
|
||||
} else {
|
||||
s += " - "
|
||||
im = constant.UnaryOp(token.SUB, im, 0) // negate im
|
||||
}
|
||||
}
|
||||
// im != 0, otherwise v would be constant.Int or constant.Float
|
||||
return s + floatString(im) + "i"
|
||||
}
|
||||
return v.String()
|
||||
}
|
||||
|
||||
func (p *printer) printObj(obj types.Object) {
|
||||
p.print(obj.Name())
|
||||
|
||||
typ, basic := obj.Type().Underlying().(*types.Basic)
|
||||
if basic && typ.Info()&types.IsUntyped != 0 {
|
||||
// don't write untyped types
|
||||
} else {
|
||||
p.print(" ")
|
||||
p.writeType(p.pkg, obj.Type())
|
||||
}
|
||||
|
||||
if obj, ok := obj.(*types.Const); ok {
|
||||
floatFmt := basic && typ.Info()&(types.IsFloat|types.IsComplex) != 0
|
||||
p.print(" = ")
|
||||
p.print(valString(obj.Val(), floatFmt))
|
||||
}
|
||||
}
|
||||
|
||||
func (p *printer) printFunc(recvType types.Type, obj *types.Func) {
|
||||
p.print("func ")
|
||||
sig := obj.Type().(*types.Signature)
|
||||
if recvType != nil {
|
||||
p.print("(")
|
||||
p.writeType(p.pkg, recvType)
|
||||
p.print(") ")
|
||||
}
|
||||
p.print(obj.Name())
|
||||
p.writeSignature(p.pkg, sig)
|
||||
}
|
||||
|
||||
// combinedMethodSet returns the method set for a named type T
|
||||
// merged with all the methods of *T that have different names than
|
||||
// the methods of T.
|
||||
//
|
||||
// combinedMethodSet is analogous to types/typeutil.IntuitiveMethodSet
|
||||
// but doesn't require a MethodSetCache.
|
||||
// TODO(gri) If this functionality doesn't change over time, consider
|
||||
// just calling IntuitiveMethodSet eventually.
|
||||
func combinedMethodSet(T *types.Named) []*types.Selection {
|
||||
// method set for T
|
||||
mset := types.NewMethodSet(T)
|
||||
var res []*types.Selection
|
||||
for i, n := 0, mset.Len(); i < n; i++ {
|
||||
res = append(res, mset.At(i))
|
||||
}
|
||||
|
||||
// add all *T methods with names different from T methods
|
||||
pmset := types.NewMethodSet(types.NewPointer(T))
|
||||
for i, n := 0, pmset.Len(); i < n; i++ {
|
||||
pm := pmset.At(i)
|
||||
if obj := pm.Obj(); mset.Lookup(obj.Pkg(), obj.Name()) == nil {
|
||||
res = append(res, pm)
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright 2014 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 implements access to export data from source.
|
||||
|
||||
package main
|
||||
|
||||
import "go/types"
|
||||
|
||||
func init() {
|
||||
register("source", sourceImporter{})
|
||||
}
|
||||
|
||||
type sourceImporter struct{}
|
||||
|
||||
func (sourceImporter) Import(path string) (*types.Package, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
// Copyright 2014 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 implements writing of types. The functionality is lifted
|
||||
// directly from go/types, but now contains various modifications for
|
||||
// nicer output.
|
||||
//
|
||||
// TODO(gri) back-port once we have a fixed interface and once the
|
||||
// go/types API is not frozen anymore for the 1.3 release; and remove
|
||||
// this implementation if possible.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"go/types"
|
||||
|
||||
"golang.org/x/tools/internal/aliases"
|
||||
)
|
||||
|
||||
func (p *printer) writeType(this *types.Package, typ types.Type) {
|
||||
p.writeTypeInternal(this, typ, make([]types.Type, 8))
|
||||
}
|
||||
|
||||
// From go/types - leave for now to ease back-porting this code.
|
||||
const GcCompatibilityMode = false
|
||||
|
||||
func (p *printer) writeTypeInternal(this *types.Package, typ types.Type, visited []types.Type) {
|
||||
// Theoretically, this is a quadratic lookup algorithm, but in
|
||||
// practice deeply nested composite types with unnamed component
|
||||
// types are uncommon. This code is likely more efficient than
|
||||
// using a map.
|
||||
for _, t := range visited {
|
||||
if t == typ {
|
||||
p.printf("○%T", typ) // cycle to typ
|
||||
return
|
||||
}
|
||||
}
|
||||
visited = append(visited, typ)
|
||||
|
||||
switch t := typ.(type) {
|
||||
case nil:
|
||||
p.print("<nil>")
|
||||
|
||||
case *types.Basic:
|
||||
if t.Kind() == types.UnsafePointer {
|
||||
p.print("unsafe.")
|
||||
}
|
||||
if GcCompatibilityMode {
|
||||
// forget the alias names
|
||||
switch t.Kind() {
|
||||
case types.Byte:
|
||||
t = types.Typ[types.Uint8]
|
||||
case types.Rune:
|
||||
t = types.Typ[types.Int32]
|
||||
}
|
||||
}
|
||||
p.print(t.Name())
|
||||
|
||||
case *types.Array:
|
||||
p.printf("[%d]", t.Len())
|
||||
p.writeTypeInternal(this, t.Elem(), visited)
|
||||
|
||||
case *types.Slice:
|
||||
p.print("[]")
|
||||
p.writeTypeInternal(this, t.Elem(), visited)
|
||||
|
||||
case *types.Struct:
|
||||
n := t.NumFields()
|
||||
if n == 0 {
|
||||
p.print("struct{}")
|
||||
return
|
||||
}
|
||||
|
||||
p.print("struct {\n")
|
||||
p.indent++
|
||||
for i := 0; i < n; i++ {
|
||||
f := t.Field(i)
|
||||
if !f.Anonymous() {
|
||||
p.printf("%s ", f.Name())
|
||||
}
|
||||
p.writeTypeInternal(this, f.Type(), visited)
|
||||
if tag := t.Tag(i); tag != "" {
|
||||
p.printf(" %q", tag)
|
||||
}
|
||||
p.print("\n")
|
||||
}
|
||||
p.indent--
|
||||
p.print("}")
|
||||
|
||||
case *types.Pointer:
|
||||
p.print("*")
|
||||
p.writeTypeInternal(this, t.Elem(), visited)
|
||||
|
||||
case *types.Tuple:
|
||||
p.writeTuple(this, t, false, visited)
|
||||
|
||||
case *types.Signature:
|
||||
p.print("func")
|
||||
p.writeSignatureInternal(this, t, visited)
|
||||
|
||||
case *types.Interface:
|
||||
// We write the source-level methods and embedded types rather
|
||||
// than the actual method set since resolved method signatures
|
||||
// may have non-printable cycles if parameters have anonymous
|
||||
// interface types that (directly or indirectly) embed the
|
||||
// current interface. For instance, consider the result type
|
||||
// of m:
|
||||
//
|
||||
// type T interface{
|
||||
// m() interface{ T }
|
||||
// }
|
||||
//
|
||||
n := t.NumMethods()
|
||||
if n == 0 {
|
||||
p.print("interface{}")
|
||||
return
|
||||
}
|
||||
|
||||
p.print("interface {\n")
|
||||
p.indent++
|
||||
if GcCompatibilityMode {
|
||||
// print flattened interface
|
||||
// (useful to compare against gc-generated interfaces)
|
||||
for i := 0; i < n; i++ {
|
||||
m := t.Method(i)
|
||||
p.print(m.Name())
|
||||
p.writeSignatureInternal(this, m.Type().(*types.Signature), visited)
|
||||
p.print("\n")
|
||||
}
|
||||
} else {
|
||||
// print explicit interface methods and embedded types
|
||||
for i, n := 0, t.NumExplicitMethods(); i < n; i++ {
|
||||
m := t.ExplicitMethod(i)
|
||||
p.print(m.Name())
|
||||
p.writeSignatureInternal(this, m.Type().(*types.Signature), visited)
|
||||
p.print("\n")
|
||||
}
|
||||
for i, n := 0, t.NumEmbeddeds(); i < n; i++ {
|
||||
typ := t.EmbeddedType(i)
|
||||
p.writeTypeInternal(this, typ, visited)
|
||||
p.print("\n")
|
||||
}
|
||||
}
|
||||
p.indent--
|
||||
p.print("}")
|
||||
|
||||
case *types.Map:
|
||||
p.print("map[")
|
||||
p.writeTypeInternal(this, t.Key(), visited)
|
||||
p.print("]")
|
||||
p.writeTypeInternal(this, t.Elem(), visited)
|
||||
|
||||
case *types.Chan:
|
||||
var s string
|
||||
var parens bool
|
||||
switch t.Dir() {
|
||||
case types.SendRecv:
|
||||
s = "chan "
|
||||
// chan (<-chan T) requires parentheses
|
||||
if c, _ := t.Elem().(*types.Chan); c != nil && c.Dir() == types.RecvOnly {
|
||||
parens = true
|
||||
}
|
||||
case types.SendOnly:
|
||||
s = "chan<- "
|
||||
case types.RecvOnly:
|
||||
s = "<-chan "
|
||||
default:
|
||||
panic("unreachable")
|
||||
}
|
||||
p.print(s)
|
||||
if parens {
|
||||
p.print("(")
|
||||
}
|
||||
p.writeTypeInternal(this, t.Elem(), visited)
|
||||
if parens {
|
||||
p.print(")")
|
||||
}
|
||||
|
||||
case *aliases.Alias:
|
||||
// TODO(adonovan): display something aliasy.
|
||||
p.writeTypeInternal(this, aliases.Unalias(t), visited)
|
||||
|
||||
case *types.Named:
|
||||
s := "<Named w/o object>"
|
||||
if obj := t.Obj(); obj != nil {
|
||||
if pkg := obj.Pkg(); pkg != nil {
|
||||
if pkg != this {
|
||||
p.print(pkg.Path())
|
||||
p.print(".")
|
||||
}
|
||||
// TODO(gri): function-local named types should be displayed
|
||||
// differently from named types at package level to avoid
|
||||
// ambiguity.
|
||||
}
|
||||
s = obj.Name()
|
||||
}
|
||||
p.print(s)
|
||||
|
||||
default:
|
||||
// For externally defined implementations of Type.
|
||||
p.print(t.String())
|
||||
}
|
||||
}
|
||||
|
||||
func (p *printer) writeTuple(this *types.Package, tup *types.Tuple, variadic bool, visited []types.Type) {
|
||||
p.print("(")
|
||||
for i, n := 0, tup.Len(); i < n; i++ {
|
||||
if i > 0 {
|
||||
p.print(", ")
|
||||
}
|
||||
v := tup.At(i)
|
||||
if name := v.Name(); name != "" {
|
||||
p.print(name)
|
||||
p.print(" ")
|
||||
}
|
||||
typ := v.Type()
|
||||
if variadic && i == n-1 {
|
||||
p.print("...")
|
||||
typ = typ.(*types.Slice).Elem()
|
||||
}
|
||||
p.writeTypeInternal(this, typ, visited)
|
||||
}
|
||||
p.print(")")
|
||||
}
|
||||
|
||||
func (p *printer) writeSignature(this *types.Package, sig *types.Signature) {
|
||||
p.writeSignatureInternal(this, sig, make([]types.Type, 8))
|
||||
}
|
||||
|
||||
func (p *printer) writeSignatureInternal(this *types.Package, sig *types.Signature, visited []types.Type) {
|
||||
p.writeTuple(this, sig.Params(), sig.Variadic(), visited)
|
||||
|
||||
res := sig.Results()
|
||||
n := res.Len()
|
||||
if n == 0 {
|
||||
// no result
|
||||
return
|
||||
}
|
||||
|
||||
p.print(" ")
|
||||
if n == 1 && res.At(0).Name() == "" {
|
||||
// single unnamed result
|
||||
p.writeTypeInternal(this, res.At(0).Type(), visited)
|
||||
return
|
||||
}
|
||||
|
||||
// multiple or named result(s)
|
||||
p.writeTuple(this, res, false, visited)
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
// Copyright 2009 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.
|
||||
|
||||
/*
|
||||
Godoc extracts and generates documentation for Go programs.
|
||||
|
||||
It runs as a web server and presents the documentation as a
|
||||
web page.
|
||||
|
||||
godoc -http=:6060
|
||||
|
||||
Usage:
|
||||
|
||||
godoc [flag]
|
||||
|
||||
The flags are:
|
||||
|
||||
-v
|
||||
verbose mode
|
||||
-timestamps=true
|
||||
show timestamps with directory listings
|
||||
-index
|
||||
enable identifier and full text search index
|
||||
(no search box is shown if -index is not set)
|
||||
-index_files=""
|
||||
glob pattern specifying index files; if not empty,
|
||||
the index is read from these files in sorted order
|
||||
-index_throttle=0.75
|
||||
index throttle value; a value of 0 means no time is allocated
|
||||
to the indexer (the indexer will never finish), a value of 1.0
|
||||
means that index creation is running at full throttle (other
|
||||
goroutines may get no time while the index is built)
|
||||
-index_interval=0
|
||||
interval of indexing; a value of 0 sets it to 5 minutes, a
|
||||
negative value indexes only once at startup
|
||||
-play=false
|
||||
enable playground
|
||||
-links=true
|
||||
link identifiers to their declarations
|
||||
-write_index=false
|
||||
write index to a file; the file name must be specified with
|
||||
-index_files
|
||||
-maxresults=10000
|
||||
maximum number of full text search results shown
|
||||
(no full text index is built if maxresults <= 0)
|
||||
-notes="BUG"
|
||||
regular expression matching note markers to show
|
||||
(e.g., "BUG|TODO", ".*")
|
||||
-goroot=$GOROOT
|
||||
Go root directory
|
||||
-http=addr
|
||||
HTTP service address (e.g., '127.0.0.1:6060' or just ':6060')
|
||||
-templates=""
|
||||
directory containing alternate template files; if set,
|
||||
the directory may provide alternative template files
|
||||
for the files in $GOROOT/lib/godoc
|
||||
-url=path
|
||||
print to standard output the data that would be served by
|
||||
an HTTP request for path
|
||||
-zip=""
|
||||
zip file providing the file system to serve; disabled if empty
|
||||
|
||||
By default, godoc looks at the packages it finds via $GOROOT and $GOPATH (if set).
|
||||
This behavior can be altered by providing an alternative $GOROOT with the -goroot
|
||||
flag.
|
||||
|
||||
When the -index flag is set, a search index is maintained.
|
||||
The index is created at startup.
|
||||
|
||||
The index contains both identifier and full text search information (searchable
|
||||
via regular expressions). The maximum number of full text search results shown
|
||||
can be set with the -maxresults flag; if set to 0, no full text results are
|
||||
shown, and only an identifier index but no full text search index is created.
|
||||
|
||||
By default, godoc uses the system's GOOS/GOARCH. You can provide the URL parameters
|
||||
"GOOS" and "GOARCH" to set the output on the web page for the target system.
|
||||
|
||||
The presentation mode of web pages served by godoc can be controlled with the
|
||||
"m" URL parameter; it accepts a comma-separated list of flag names as value:
|
||||
|
||||
all show documentation for all declarations, not just the exported ones
|
||||
methods show all embedded methods, not just those of unexported anonymous fields
|
||||
src show the original source code rather than the extracted documentation
|
||||
flat present flat (not indented) directory listings using full paths
|
||||
|
||||
For instance, https://golang.org/pkg/math/big/?m=all shows the documentation
|
||||
for all (not just the exported) declarations of package big.
|
||||
|
||||
By default, godoc serves files from the file system of the underlying OS.
|
||||
Instead, a .zip file may be provided via the -zip flag, which contains
|
||||
the file system to serve. The file paths stored in the .zip file must use
|
||||
slash ('/') as path separator; and they must be unrooted. $GOROOT (or -goroot)
|
||||
must be set to the .zip file directory path containing the Go root directory.
|
||||
For instance, for a .zip file created by the command:
|
||||
|
||||
zip -r go.zip $HOME/go
|
||||
|
||||
one may run godoc as follows:
|
||||
|
||||
godoc -http=:6060 -zip=go.zip -goroot=$HOME/go
|
||||
|
||||
Godoc documentation is converted to HTML or to text using the go/doc package;
|
||||
see https://golang.org/pkg/go/doc/#ToHTML for the exact rules.
|
||||
Godoc also shows example code that is runnable by the testing package;
|
||||
see https://golang.org/pkg/testing/#hdr-Examples for the conventions.
|
||||
See "Godoc: documenting Go code" for how to write good comments for godoc:
|
||||
https://golang.org/doc/articles/godoc_documenting_go_code.html
|
||||
|
||||
Deprecated: godoc cannot select what version of a package is displayed.
|
||||
Instead, use golang.org/x/pkgsite/cmd/pkgsite.
|
||||
*/
|
||||
package main // import "golang.org/x/tools/cmd/godoc"
|
||||
@@ -0,0 +1,464 @@
|
||||
// Copyright 2013 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 main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"go/build"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/tools/go/packages/packagestest"
|
||||
"golang.org/x/tools/internal/testenv"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if os.Getenv("GODOC_TEST_IS_GODOC") != "" {
|
||||
main()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Inform subprocesses that they should run the cmd/godoc main instead of
|
||||
// running tests. It's a close approximation to building and running the real
|
||||
// command, and much less complicated and expensive to build and clean up.
|
||||
os.Setenv("GODOC_TEST_IS_GODOC", "1")
|
||||
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
var exe struct {
|
||||
path string
|
||||
err error
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
func godocPath(t *testing.T) string {
|
||||
if !testenv.HasExec() {
|
||||
t.Skipf("skipping test: exec not supported on %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||
}
|
||||
|
||||
exe.once.Do(func() {
|
||||
exe.path, exe.err = os.Executable()
|
||||
})
|
||||
if exe.err != nil {
|
||||
t.Fatal(exe.err)
|
||||
}
|
||||
return exe.path
|
||||
}
|
||||
|
||||
func serverAddress(t *testing.T) string {
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
ln, err = net.Listen("tcp6", "[::1]:0")
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln.Close()
|
||||
return ln.Addr().String()
|
||||
}
|
||||
|
||||
func waitForServerReady(t *testing.T, ctx context.Context, cmd *exec.Cmd, addr string) {
|
||||
waitForServer(t, ctx,
|
||||
fmt.Sprintf("http://%v/", addr),
|
||||
"Go Documentation Server",
|
||||
false)
|
||||
}
|
||||
|
||||
func waitForSearchReady(t *testing.T, ctx context.Context, cmd *exec.Cmd, addr string) {
|
||||
waitForServer(t, ctx,
|
||||
fmt.Sprintf("http://%v/search?q=FALLTHROUGH", addr),
|
||||
"The list of tokens.",
|
||||
false)
|
||||
}
|
||||
|
||||
func waitUntilScanComplete(t *testing.T, ctx context.Context, addr string) {
|
||||
waitForServer(t, ctx,
|
||||
fmt.Sprintf("http://%v/pkg", addr),
|
||||
"Scan is not yet complete",
|
||||
// setting reverse as true, which means this waits
|
||||
// until the string is not returned in the response anymore
|
||||
true)
|
||||
}
|
||||
|
||||
const pollInterval = 50 * time.Millisecond
|
||||
|
||||
// waitForServer waits for server to meet the required condition,
|
||||
// failing the test if ctx is canceled before that occurs.
|
||||
func waitForServer(t *testing.T, ctx context.Context, url, match string, reverse bool) {
|
||||
start := time.Now()
|
||||
for {
|
||||
if ctx.Err() != nil {
|
||||
t.Helper()
|
||||
t.Fatalf("server failed to respond in %v", time.Since(start))
|
||||
}
|
||||
|
||||
time.Sleep(pollInterval)
|
||||
res, err := http.Get(url)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
body, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
if err != nil || res.StatusCode != http.StatusOK {
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case !reverse && bytes.Contains(body, []byte(match)),
|
||||
reverse && !bytes.Contains(body, []byte(match)):
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// hasTag checks whether a given release tag is contained in the current version
|
||||
// of the go binary.
|
||||
func hasTag(t string) bool {
|
||||
for _, v := range build.Default.ReleaseTags {
|
||||
if t == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestURL(t *testing.T) {
|
||||
if runtime.GOOS == "plan9" {
|
||||
t.Skip("skipping on plan9; fails to start up quickly enough")
|
||||
}
|
||||
bin := godocPath(t)
|
||||
|
||||
testcase := func(url string, contents string) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
stdout, stderr := new(bytes.Buffer), new(bytes.Buffer)
|
||||
|
||||
args := []string{fmt.Sprintf("-url=%s", url)}
|
||||
cmd := testenv.Command(t, bin, args...)
|
||||
cmd.Stdout = stdout
|
||||
cmd.Stderr = stderr
|
||||
cmd.Args[0] = "godoc"
|
||||
|
||||
// Set GOPATH variable to a non-existing absolute path
|
||||
// and GOPROXY=off to disable module fetches.
|
||||
// We cannot just unset GOPATH variable because godoc would default it to ~/go.
|
||||
// (We don't want the indexer looking at the local workspace during tests.)
|
||||
cmd.Env = append(os.Environ(),
|
||||
"GOPATH=/does_not_exist",
|
||||
"GOPROXY=off",
|
||||
"GO111MODULE=off")
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
t.Fatalf("failed to run godoc -url=%q: %s\nstderr:\n%s", url, err, stderr)
|
||||
}
|
||||
|
||||
if !strings.Contains(stdout.String(), contents) {
|
||||
t.Errorf("did not find substring %q in output of godoc -url=%q:\n%s", contents, url, stdout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("index", testcase("/", "These packages are part of the Go Project but outside the main Go tree."))
|
||||
t.Run("fmt", testcase("/pkg/fmt", "Package fmt implements formatted I/O"))
|
||||
}
|
||||
|
||||
// Basic integration test for godoc HTTP interface.
|
||||
func TestWeb(t *testing.T) {
|
||||
bin := godocPath(t)
|
||||
|
||||
for _, x := range packagestest.All {
|
||||
t.Run(x.Name(), func(t *testing.T) {
|
||||
testWeb(t, x, bin, false)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Basic integration test for godoc HTTP interface.
|
||||
func TestWebIndex(t *testing.T) {
|
||||
t.Skip("slow test of to-be-deleted code (golang/go#59056)")
|
||||
if testing.Short() {
|
||||
t.Skip("skipping slow test in -short mode")
|
||||
}
|
||||
bin := godocPath(t)
|
||||
testWeb(t, packagestest.GOPATH, bin, true)
|
||||
}
|
||||
|
||||
// Basic integration test for godoc HTTP interface.
|
||||
func testWeb(t *testing.T, x packagestest.Exporter, bin string, withIndex bool) {
|
||||
switch runtime.GOOS {
|
||||
case "plan9":
|
||||
t.Skip("skipping on plan9: fails to start up quickly enough")
|
||||
case "android", "ios":
|
||||
t.Skip("skipping on mobile: lacks GOROOT/api in test environment")
|
||||
}
|
||||
|
||||
// Write a fake GOROOT/GOPATH with some third party packages.
|
||||
e := packagestest.Export(t, x, []packagestest.Module{
|
||||
{
|
||||
Name: "godoc.test/repo1",
|
||||
Files: map[string]interface{}{
|
||||
"a/a.go": `// Package a is a package in godoc.test/repo1.
|
||||
package a; import _ "godoc.test/repo2/a"; const Name = "repo1a"`,
|
||||
"b/b.go": `package b; const Name = "repo1b"`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "godoc.test/repo2",
|
||||
Files: map[string]interface{}{
|
||||
"a/a.go": `package a; const Name = "repo2a"`,
|
||||
"b/b.go": `package b; const Name = "repo2b"`,
|
||||
},
|
||||
},
|
||||
})
|
||||
defer e.Cleanup()
|
||||
|
||||
// Start the server.
|
||||
addr := serverAddress(t)
|
||||
args := []string{fmt.Sprintf("-http=%s", addr)}
|
||||
if withIndex {
|
||||
args = append(args, "-index", "-index_interval=-1s")
|
||||
}
|
||||
cmd := testenv.Command(t, bin, args...)
|
||||
cmd.Dir = e.Config.Dir
|
||||
cmd.Env = e.Config.Env
|
||||
cmdOut := new(strings.Builder)
|
||||
cmd.Stdout = cmdOut
|
||||
cmd.Stderr = cmdOut
|
||||
cmd.Args[0] = "godoc"
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
t.Fatalf("failed to start godoc: %s", err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
err := cmd.Wait()
|
||||
t.Logf("%v: %v", cmd, err)
|
||||
cancel()
|
||||
}()
|
||||
defer func() {
|
||||
// Shut down the server cleanly if possible.
|
||||
if runtime.GOOS == "windows" {
|
||||
cmd.Process.Kill() // Windows doesn't support os.Interrupt.
|
||||
} else {
|
||||
cmd.Process.Signal(os.Interrupt)
|
||||
}
|
||||
<-ctx.Done()
|
||||
t.Logf("server output:\n%s", cmdOut)
|
||||
}()
|
||||
|
||||
if withIndex {
|
||||
waitForSearchReady(t, ctx, cmd, addr)
|
||||
} else {
|
||||
waitForServerReady(t, ctx, cmd, addr)
|
||||
waitUntilScanComplete(t, ctx, addr)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
path string
|
||||
contains []string // substring
|
||||
match []string // regexp
|
||||
notContains []string
|
||||
needIndex bool
|
||||
releaseTag string // optional release tag that must be in go/build.ReleaseTags
|
||||
}{
|
||||
{
|
||||
path: "/",
|
||||
contains: []string{
|
||||
"Go Documentation Server",
|
||||
"Standard library",
|
||||
"These packages are part of the Go Project but outside the main Go tree.",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/pkg/fmt/",
|
||||
contains: []string{"Package fmt implements formatted I/O"},
|
||||
},
|
||||
{
|
||||
path: "/src/fmt/",
|
||||
contains: []string{"scan_test.go"},
|
||||
},
|
||||
{
|
||||
path: "/src/fmt/print.go",
|
||||
contains: []string{"// Println formats using"},
|
||||
},
|
||||
{
|
||||
path: "/pkg",
|
||||
contains: []string{
|
||||
"Standard library",
|
||||
"Package fmt implements formatted I/O",
|
||||
"Third party",
|
||||
"Package a is a package in godoc.test/repo1.",
|
||||
},
|
||||
notContains: []string{
|
||||
"internal/syscall",
|
||||
"cmd/gc",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/pkg/?m=all",
|
||||
contains: []string{
|
||||
"Standard library",
|
||||
"Package fmt implements formatted I/O",
|
||||
"internal/syscall/?m=all",
|
||||
},
|
||||
notContains: []string{
|
||||
"cmd/gc",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/search?q=ListenAndServe",
|
||||
contains: []string{
|
||||
"/src",
|
||||
},
|
||||
notContains: []string{
|
||||
"/pkg/bootstrap",
|
||||
},
|
||||
needIndex: true,
|
||||
},
|
||||
{
|
||||
path: "/pkg/strings/",
|
||||
contains: []string{
|
||||
`href="/src/strings/strings.go"`,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/cmd/compile/internal/amd64/",
|
||||
contains: []string{
|
||||
`href="/src/cmd/compile/internal/amd64/ssa.go"`,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/pkg/math/bits/",
|
||||
contains: []string{
|
||||
`Added in Go 1.9`,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/pkg/net/",
|
||||
contains: []string{
|
||||
`// IPv6 scoped addressing zone; added in Go 1.1`,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/pkg/net/http/httptrace/",
|
||||
match: []string{
|
||||
`Got1xxResponse.*// Go 1\.11`,
|
||||
},
|
||||
releaseTag: "go1.11",
|
||||
},
|
||||
// Verify we don't add version info to a struct field added the same time
|
||||
// as the struct itself:
|
||||
{
|
||||
path: "/pkg/net/http/httptrace/",
|
||||
match: []string{
|
||||
`(?m)GotFirstResponseByte func\(\)\s*$`,
|
||||
},
|
||||
},
|
||||
// Remove trailing periods before adding semicolons:
|
||||
{
|
||||
path: "/pkg/database/sql/",
|
||||
contains: []string{
|
||||
"The number of connections currently in use; added in Go 1.11",
|
||||
"The number of idle connections; added in Go 1.11",
|
||||
},
|
||||
releaseTag: "go1.11",
|
||||
},
|
||||
|
||||
// Third party packages.
|
||||
{
|
||||
path: "/pkg/godoc.test/repo1/a",
|
||||
contains: []string{`const <span id="Name">Name</span> = "repo1a"`},
|
||||
},
|
||||
{
|
||||
path: "/pkg/godoc.test/repo2/b",
|
||||
contains: []string{`const <span id="Name">Name</span> = "repo2b"`},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
if test.needIndex && !withIndex {
|
||||
continue
|
||||
}
|
||||
url := fmt.Sprintf("http://%s%s", addr, test.path)
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
t.Errorf("GET %s failed: %s", url, err)
|
||||
continue
|
||||
}
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
strBody := string(body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
t.Errorf("GET %s: failed to read body: %s (response: %v)", url, err, resp)
|
||||
}
|
||||
isErr := false
|
||||
for _, substr := range test.contains {
|
||||
if test.releaseTag != "" && !hasTag(test.releaseTag) {
|
||||
continue
|
||||
}
|
||||
if !bytes.Contains(body, []byte(substr)) {
|
||||
t.Errorf("GET %s: wanted substring %q in body", url, substr)
|
||||
isErr = true
|
||||
}
|
||||
}
|
||||
for _, re := range test.match {
|
||||
if test.releaseTag != "" && !hasTag(test.releaseTag) {
|
||||
continue
|
||||
}
|
||||
if ok, err := regexp.MatchString(re, strBody); !ok || err != nil {
|
||||
if err != nil {
|
||||
t.Fatalf("Bad regexp %q: %v", re, err)
|
||||
}
|
||||
t.Errorf("GET %s: wanted to match %s in body", url, re)
|
||||
isErr = true
|
||||
}
|
||||
}
|
||||
for _, substr := range test.notContains {
|
||||
if bytes.Contains(body, []byte(substr)) {
|
||||
t.Errorf("GET %s: didn't want substring %q in body", url, substr)
|
||||
isErr = true
|
||||
}
|
||||
}
|
||||
if isErr {
|
||||
t.Errorf("GET %s: got:\n%s", url, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test for golang.org/issue/35476.
|
||||
func TestNoMainModule(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in -short mode")
|
||||
}
|
||||
if runtime.GOOS == "plan9" {
|
||||
t.Skip("skipping on plan9; for consistency with other tests that build godoc binary")
|
||||
}
|
||||
bin := godocPath(t)
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Run godoc in an empty directory with module mode explicitly on,
|
||||
// so that 'go env GOMOD' reports os.DevNull.
|
||||
cmd := testenv.Command(t, bin, "-url=/")
|
||||
cmd.Dir = tempDir
|
||||
cmd.Env = append(os.Environ(), "GO111MODULE=on")
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
t.Fatalf("godoc command failed: %v\nstderr=%q", err, stderr.String())
|
||||
}
|
||||
if strings.Contains(stderr.String(), "go mod download") {
|
||||
t.Errorf("stderr contains 'go mod download', is that intentional?\nstderr=%q", stderr.String())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright 2018 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 main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func findGOROOT() string {
|
||||
if env := os.Getenv("GOROOT"); env != "" {
|
||||
return filepath.Clean(env)
|
||||
}
|
||||
def := filepath.Clean(runtime.GOROOT())
|
||||
if runtime.Compiler == "gccgo" {
|
||||
// gccgo has no real GOROOT, and it certainly doesn't
|
||||
// depend on the executable's location.
|
||||
return def
|
||||
}
|
||||
out, err := exec.Command("go", "env", "GOROOT").Output()
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
return strings.TrimSpace(string(out))
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// Copyright 2010 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 main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"go/format"
|
||||
"log"
|
||||
"net/http"
|
||||
"text/template"
|
||||
|
||||
"golang.org/x/tools/godoc"
|
||||
"golang.org/x/tools/godoc/redirect"
|
||||
"golang.org/x/tools/godoc/vfs"
|
||||
|
||||
_ "golang.org/x/tools/playground" // register "/compile" playground redirect
|
||||
)
|
||||
|
||||
var (
|
||||
pres *godoc.Presentation
|
||||
fs = vfs.NameSpace{}
|
||||
)
|
||||
|
||||
func registerHandlers(pres *godoc.Presentation) {
|
||||
if pres == nil {
|
||||
panic("nil Presentation")
|
||||
}
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
|
||||
if req.URL.Path == "/" {
|
||||
http.Redirect(w, req, "/pkg/", http.StatusFound)
|
||||
return
|
||||
}
|
||||
pres.ServeHTTP(w, req)
|
||||
})
|
||||
mux.Handle("/pkg/C/", redirect.Handler("/cmd/cgo/"))
|
||||
mux.HandleFunc("/fmt", fmtHandler)
|
||||
redirect.Register(mux)
|
||||
|
||||
http.Handle("/", mux)
|
||||
}
|
||||
|
||||
func readTemplate(name string) *template.Template {
|
||||
if pres == nil {
|
||||
panic("no global Presentation set yet")
|
||||
}
|
||||
path := "lib/godoc/" + name
|
||||
|
||||
// use underlying file system fs to read the template file
|
||||
// (cannot use template ParseFile functions directly)
|
||||
data, err := vfs.ReadFile(fs, path)
|
||||
if err != nil {
|
||||
log.Fatal("readTemplate: ", err)
|
||||
}
|
||||
// be explicit with errors (for app engine use)
|
||||
t, err := template.New(name).Funcs(pres.FuncMap()).Parse(string(data))
|
||||
if err != nil {
|
||||
log.Fatal("readTemplate: ", err)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func readTemplates(p *godoc.Presentation) {
|
||||
p.CallGraphHTML = readTemplate("callgraph.html")
|
||||
p.DirlistHTML = readTemplate("dirlist.html")
|
||||
p.ErrorHTML = readTemplate("error.html")
|
||||
p.ExampleHTML = readTemplate("example.html")
|
||||
p.GodocHTML = readTemplate("godoc.html")
|
||||
p.ImplementsHTML = readTemplate("implements.html")
|
||||
p.MethodSetHTML = readTemplate("methodset.html")
|
||||
p.PackageHTML = readTemplate("package.html")
|
||||
p.PackageRootHTML = readTemplate("packageroot.html")
|
||||
p.SearchHTML = readTemplate("search.html")
|
||||
p.SearchDocHTML = readTemplate("searchdoc.html")
|
||||
p.SearchCodeHTML = readTemplate("searchcode.html")
|
||||
p.SearchTxtHTML = readTemplate("searchtxt.html")
|
||||
}
|
||||
|
||||
type fmtResponse struct {
|
||||
Body string
|
||||
Error string
|
||||
}
|
||||
|
||||
// fmtHandler takes a Go program in its "body" form value, formats it with
|
||||
// standard gofmt formatting, and writes a fmtResponse as a JSON object.
|
||||
func fmtHandler(w http.ResponseWriter, r *http.Request) {
|
||||
resp := new(fmtResponse)
|
||||
body, err := format.Source([]byte(r.FormValue("body")))
|
||||
if err != nil {
|
||||
resp.Error = err.Error()
|
||||
} else {
|
||||
resp.Body = string(body)
|
||||
}
|
||||
w.Header().Set("Content-type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
@@ -0,0 +1,504 @@
|
||||
// Copyright 2009 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.
|
||||
|
||||
// godoc: Go Documentation Server
|
||||
|
||||
// Web server tree:
|
||||
//
|
||||
// http://godoc/ redirect to /pkg/
|
||||
// http://godoc/src/ serve files from $GOROOT/src; .go gets pretty-printed
|
||||
// http://godoc/cmd/ serve documentation about commands
|
||||
// http://godoc/pkg/ serve documentation about packages
|
||||
// (idea is if you say import "compress/zlib", you go to
|
||||
// http://godoc/pkg/compress/zlib)
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
_ "expvar" // to serve /debug/vars
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/build"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
_ "net/http/pprof" // to serve /debug/pprof/*
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/tools/godoc"
|
||||
"golang.org/x/tools/godoc/static"
|
||||
"golang.org/x/tools/godoc/vfs"
|
||||
"golang.org/x/tools/godoc/vfs/gatefs"
|
||||
"golang.org/x/tools/godoc/vfs/mapfs"
|
||||
"golang.org/x/tools/godoc/vfs/zipfs"
|
||||
"golang.org/x/tools/internal/gocommand"
|
||||
)
|
||||
|
||||
const defaultAddr = "localhost:6060" // default webserver address
|
||||
|
||||
var (
|
||||
// file system to serve
|
||||
// (with e.g.: zip -r go.zip $GOROOT -i \*.go -i \*.html -i \*.css -i \*.js -i \*.txt -i \*.c -i \*.h -i \*.s -i \*.png -i \*.jpg -i \*.sh -i favicon.ico)
|
||||
zipfile = flag.String("zip", "", "zip file providing the file system to serve; disabled if empty")
|
||||
|
||||
// file-based index
|
||||
writeIndex = flag.Bool("write_index", false, "write index to a file; the file name must be specified with -index_files")
|
||||
|
||||
// network
|
||||
httpAddr = flag.String("http", defaultAddr, "HTTP service address")
|
||||
|
||||
// layout control
|
||||
urlFlag = flag.String("url", "", "print HTML for named URL")
|
||||
|
||||
verbose = flag.Bool("v", false, "verbose mode")
|
||||
|
||||
// file system roots
|
||||
// TODO(gri) consider the invariant that goroot always end in '/'
|
||||
goroot = flag.String("goroot", findGOROOT(), "Go root directory")
|
||||
|
||||
// layout control
|
||||
showTimestamps = flag.Bool("timestamps", false, "show timestamps with directory listings")
|
||||
templateDir = flag.String("templates", "", "load templates/JS/CSS from disk in this directory")
|
||||
showPlayground = flag.Bool("play", false, "enable playground")
|
||||
declLinks = flag.Bool("links", true, "link identifiers to their declarations")
|
||||
|
||||
// search index
|
||||
indexEnabled = flag.Bool("index", false, "enable search index")
|
||||
indexFiles = flag.String("index_files", "", "glob pattern specifying index files; if not empty, the index is read from these files in sorted order")
|
||||
indexInterval = flag.Duration("index_interval", 0, "interval of indexing; 0 for default (5m), negative to only index once at startup")
|
||||
maxResults = flag.Int("maxresults", 10000, "maximum number of full text search results shown")
|
||||
indexThrottle = flag.Float64("index_throttle", 0.75, "index throttle value; 0.0 = no time allocated, 1.0 = full throttle")
|
||||
|
||||
// source code notes
|
||||
notesRx = flag.String("notes", "BUG", "regular expression matching note markers to show")
|
||||
)
|
||||
|
||||
// An httpResponseRecorder is an http.ResponseWriter
|
||||
type httpResponseRecorder struct {
|
||||
body *bytes.Buffer
|
||||
header http.Header
|
||||
code int
|
||||
}
|
||||
|
||||
func (w *httpResponseRecorder) Header() http.Header { return w.header }
|
||||
func (w *httpResponseRecorder) Write(b []byte) (int, error) { return w.body.Write(b) }
|
||||
func (w *httpResponseRecorder) WriteHeader(code int) { w.code = code }
|
||||
|
||||
func usage() {
|
||||
fmt.Fprintf(os.Stderr, "usage: godoc -http="+defaultAddr+"\n")
|
||||
flag.PrintDefaults()
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
func loggingHandler(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
log.Printf("%s\t%s", req.RemoteAddr, req.URL)
|
||||
h.ServeHTTP(w, req)
|
||||
})
|
||||
}
|
||||
|
||||
func handleURLFlag() {
|
||||
// Try up to 10 fetches, following redirects.
|
||||
urlstr := *urlFlag
|
||||
for i := 0; i < 10; i++ {
|
||||
// Prepare request.
|
||||
u, err := url.Parse(urlstr)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
req := &http.Request{
|
||||
URL: u,
|
||||
}
|
||||
|
||||
// Invoke default HTTP handler to serve request
|
||||
// to our buffering httpWriter.
|
||||
w := &httpResponseRecorder{code: 200, header: make(http.Header), body: new(bytes.Buffer)}
|
||||
http.DefaultServeMux.ServeHTTP(w, req)
|
||||
|
||||
// Return data, error, or follow redirect.
|
||||
switch w.code {
|
||||
case 200: // ok
|
||||
os.Stdout.Write(w.body.Bytes())
|
||||
return
|
||||
case 301, 302, 303, 307: // redirect
|
||||
redirect := w.header.Get("Location")
|
||||
if redirect == "" {
|
||||
log.Fatalf("HTTP %d without Location header", w.code)
|
||||
}
|
||||
urlstr = redirect
|
||||
default:
|
||||
log.Fatalf("HTTP error %d", w.code)
|
||||
}
|
||||
}
|
||||
log.Fatalf("too many redirects")
|
||||
}
|
||||
|
||||
func initCorpus(corpus *godoc.Corpus) {
|
||||
err := corpus.Init()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
|
||||
// Check usage.
|
||||
if flag.NArg() > 0 {
|
||||
fmt.Fprintln(os.Stderr, `Unexpected arguments. Use "go doc" for command-line help output instead. For example, "go doc fmt.Printf".`)
|
||||
usage()
|
||||
}
|
||||
if *httpAddr == "" && *urlFlag == "" && !*writeIndex {
|
||||
fmt.Fprintln(os.Stderr, "At least one of -http, -url, or -write_index must be set to a non-zero value.")
|
||||
usage()
|
||||
}
|
||||
|
||||
// Set the resolved goroot.
|
||||
vfs.GOROOT = *goroot
|
||||
|
||||
fsGate := make(chan bool, 20)
|
||||
|
||||
// Determine file system to use.
|
||||
if *zipfile == "" {
|
||||
// use file system of underlying OS
|
||||
rootfs := gatefs.New(vfs.OS(*goroot), fsGate)
|
||||
fs.Bind("/", rootfs, "/", vfs.BindReplace)
|
||||
} else {
|
||||
// use file system specified via .zip file (path separator must be '/')
|
||||
rc, err := zip.OpenReader(*zipfile)
|
||||
if err != nil {
|
||||
log.Fatalf("%s: %s\n", *zipfile, err)
|
||||
}
|
||||
defer rc.Close() // be nice (e.g., -writeIndex mode)
|
||||
fs.Bind("/", zipfs.New(rc, *zipfile), *goroot, vfs.BindReplace)
|
||||
}
|
||||
if *templateDir != "" {
|
||||
fs.Bind("/lib/godoc", vfs.OS(*templateDir), "/", vfs.BindBefore)
|
||||
fs.Bind("/favicon.ico", vfs.OS(*templateDir), "/favicon.ico", vfs.BindReplace)
|
||||
} else {
|
||||
fs.Bind("/lib/godoc", mapfs.New(static.Files), "/", vfs.BindReplace)
|
||||
fs.Bind("/favicon.ico", mapfs.New(static.Files), "/favicon.ico", vfs.BindReplace)
|
||||
}
|
||||
|
||||
// Get the GOMOD value, use it to determine if godoc is being invoked in module mode.
|
||||
goModFile, err := goMod()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to determine go env GOMOD value: %v", err)
|
||||
goModFile = "" // Fall back to GOPATH mode.
|
||||
}
|
||||
|
||||
if goModFile != "" {
|
||||
fmt.Printf("using module mode; GOMOD=%s\n", goModFile)
|
||||
|
||||
// Detect whether to use vendor mode or not.
|
||||
vendorEnabled, mainModVendor, err := gocommand.VendorEnabled(context.Background(), gocommand.Invocation{}, &gocommand.Runner{})
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to determine if vendoring is enabled: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if vendorEnabled {
|
||||
// Bind the root directory of the main module.
|
||||
fs.Bind(path.Join("/src", mainModVendor.Path), gatefs.New(vfs.OS(mainModVendor.Dir), fsGate), "/", vfs.BindAfter)
|
||||
|
||||
// Bind the vendor directory.
|
||||
//
|
||||
// Note that in module mode, vendor directories in locations
|
||||
// other than the main module's root directory are ignored.
|
||||
// See https://golang.org/ref/mod#vendoring.
|
||||
vendorDir := filepath.Join(mainModVendor.Dir, "vendor")
|
||||
fs.Bind("/src", gatefs.New(vfs.OS(vendorDir), fsGate), "/", vfs.BindAfter)
|
||||
|
||||
} else {
|
||||
// Try to download dependencies that are not in the module cache in order to
|
||||
// show their documentation.
|
||||
// This may fail if module downloading is disallowed (GOPROXY=off) or due to
|
||||
// limited connectivity, in which case we print errors to stderr and show
|
||||
// documentation only for packages that are available.
|
||||
fillModuleCache(os.Stderr, goModFile)
|
||||
|
||||
// Determine modules in the build list.
|
||||
mods, err := buildList(goModFile)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to determine the build list of the main module: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Bind module trees into Go root.
|
||||
for _, m := range mods {
|
||||
if m.Dir == "" {
|
||||
// Module is not available in the module cache, skip it.
|
||||
continue
|
||||
}
|
||||
dst := path.Join("/src", m.Path)
|
||||
fs.Bind(dst, gatefs.New(vfs.OS(m.Dir), fsGate), "/", vfs.BindAfter)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fmt.Println("using GOPATH mode")
|
||||
|
||||
// Bind $GOPATH trees into Go root.
|
||||
for _, p := range filepath.SplitList(build.Default.GOPATH) {
|
||||
fs.Bind("/src", gatefs.New(vfs.OS(p), fsGate), "/src", vfs.BindAfter)
|
||||
}
|
||||
}
|
||||
|
||||
var corpus *godoc.Corpus
|
||||
if goModFile != "" {
|
||||
corpus = godoc.NewCorpus(moduleFS{fs})
|
||||
} else {
|
||||
corpus = godoc.NewCorpus(fs)
|
||||
}
|
||||
corpus.Verbose = *verbose
|
||||
corpus.MaxResults = *maxResults
|
||||
corpus.IndexEnabled = *indexEnabled
|
||||
if *maxResults == 0 {
|
||||
corpus.IndexFullText = false
|
||||
}
|
||||
corpus.IndexFiles = *indexFiles
|
||||
corpus.IndexDirectory = func(dir string) bool {
|
||||
return dir != "/pkg" && !strings.HasPrefix(dir, "/pkg/")
|
||||
}
|
||||
corpus.IndexThrottle = *indexThrottle
|
||||
corpus.IndexInterval = *indexInterval
|
||||
if *writeIndex || *urlFlag != "" {
|
||||
corpus.IndexThrottle = 1.0
|
||||
corpus.IndexEnabled = true
|
||||
initCorpus(corpus)
|
||||
} else {
|
||||
go initCorpus(corpus)
|
||||
}
|
||||
|
||||
// Initialize the version info before readTemplates, which saves
|
||||
// the map value in a method value.
|
||||
corpus.InitVersionInfo()
|
||||
|
||||
pres = godoc.NewPresentation(corpus)
|
||||
pres.ShowTimestamps = *showTimestamps
|
||||
pres.ShowPlayground = *showPlayground
|
||||
pres.DeclLinks = *declLinks
|
||||
if *notesRx != "" {
|
||||
pres.NotesRx = regexp.MustCompile(*notesRx)
|
||||
}
|
||||
|
||||
readTemplates(pres)
|
||||
registerHandlers(pres)
|
||||
|
||||
if *writeIndex {
|
||||
// Write search index and exit.
|
||||
if *indexFiles == "" {
|
||||
log.Fatal("no index file specified")
|
||||
}
|
||||
|
||||
log.Println("initialize file systems")
|
||||
*verbose = true // want to see what happens
|
||||
|
||||
corpus.UpdateIndex()
|
||||
|
||||
log.Println("writing index file", *indexFiles)
|
||||
f, err := os.Create(*indexFiles)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
index, _ := corpus.CurrentIndex()
|
||||
_, err = index.WriteTo(f)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Println("done")
|
||||
return
|
||||
}
|
||||
|
||||
// Print content that would be served at the URL *urlFlag.
|
||||
if *urlFlag != "" {
|
||||
handleURLFlag()
|
||||
return
|
||||
}
|
||||
|
||||
var handler http.Handler = http.DefaultServeMux
|
||||
if *verbose {
|
||||
log.Printf("Go Documentation Server")
|
||||
log.Printf("version = %s", runtime.Version())
|
||||
log.Printf("address = %s", *httpAddr)
|
||||
log.Printf("goroot = %s", *goroot)
|
||||
switch {
|
||||
case !*indexEnabled:
|
||||
log.Print("search index disabled")
|
||||
case *maxResults > 0:
|
||||
log.Printf("full text index enabled (maxresults = %d)", *maxResults)
|
||||
default:
|
||||
log.Print("identifier search index enabled")
|
||||
}
|
||||
fs.Fprint(os.Stderr)
|
||||
handler = loggingHandler(handler)
|
||||
}
|
||||
|
||||
// Initialize search index.
|
||||
if *indexEnabled {
|
||||
go corpus.RunIndexer()
|
||||
}
|
||||
|
||||
// Start http server.
|
||||
if *verbose {
|
||||
log.Println("starting HTTP server")
|
||||
}
|
||||
if err := http.ListenAndServe(*httpAddr, handler); err != nil {
|
||||
log.Fatalf("ListenAndServe %s: %v", *httpAddr, err)
|
||||
}
|
||||
}
|
||||
|
||||
// goMod returns the go env GOMOD value in the current directory
|
||||
// by invoking the go command.
|
||||
//
|
||||
// GOMOD is documented at https://golang.org/cmd/go/#hdr-Environment_variables:
|
||||
//
|
||||
// The absolute path to the go.mod of the main module,
|
||||
// or the empty string if not using modules.
|
||||
func goMod() (string, error) {
|
||||
out, err := exec.Command("go", "env", "-json", "GOMOD").Output()
|
||||
if ee := (*exec.ExitError)(nil); errors.As(err, &ee) {
|
||||
return "", fmt.Errorf("go command exited unsuccessfully: %v\n%s", ee.ProcessState.String(), ee.Stderr)
|
||||
} else if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var env struct {
|
||||
GoMod string
|
||||
}
|
||||
err = json.Unmarshal(out, &env)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return env.GoMod, nil
|
||||
}
|
||||
|
||||
// fillModuleCache does a best-effort attempt to fill the module cache
|
||||
// with all dependencies of the main module in the current directory
|
||||
// by invoking the go command. Module download logs are streamed to w.
|
||||
// If there are any problems encountered, they are also written to w.
|
||||
// It should only be used in module mode, when vendor mode isn't on.
|
||||
//
|
||||
// See https://golang.org/cmd/go/#hdr-Download_modules_to_local_cache.
|
||||
func fillModuleCache(w io.Writer, goMod string) {
|
||||
if goMod == os.DevNull {
|
||||
// No module requirements, nothing to do.
|
||||
return
|
||||
}
|
||||
|
||||
cmd := exec.Command("go", "mod", "download", "-json")
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
cmd.Stderr = w
|
||||
err := cmd.Run()
|
||||
if ee := (*exec.ExitError)(nil); errors.As(err, &ee) && ee.ExitCode() == 1 {
|
||||
// Exit code 1 from this command means there were some
|
||||
// non-empty Error values in the output. Print them to w.
|
||||
fmt.Fprintf(w, "documentation for some packages is not shown:\n")
|
||||
for dec := json.NewDecoder(&out); ; {
|
||||
var m struct {
|
||||
Path string // Module path.
|
||||
Version string // Module version.
|
||||
Error string // Error loading module.
|
||||
}
|
||||
err := dec.Decode(&m)
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
fmt.Fprintf(w, "error decoding JSON object from go mod download -json: %v\n", err)
|
||||
continue
|
||||
}
|
||||
if m.Error == "" {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(w, "\tmodule %s@%s is not in the module cache and there was a problem downloading it: %s\n", m.Path, m.Version, m.Error)
|
||||
}
|
||||
} else if err != nil {
|
||||
fmt.Fprintf(w, "there was a problem filling module cache: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
type mod struct {
|
||||
Path string // Module path.
|
||||
Dir string // Directory holding files for this module, if any.
|
||||
}
|
||||
|
||||
// buildList determines the build list in the current directory
|
||||
// by invoking the go command. It should only be used in module mode,
|
||||
// when vendor mode isn't on.
|
||||
//
|
||||
// See https://golang.org/cmd/go/#hdr-The_main_module_and_the_build_list.
|
||||
func buildList(goMod string) ([]mod, error) {
|
||||
if goMod == os.DevNull {
|
||||
// Empty build list.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
out, err := exec.Command("go", "list", "-m", "-json", "all").Output()
|
||||
if ee := (*exec.ExitError)(nil); errors.As(err, &ee) {
|
||||
return nil, fmt.Errorf("go command exited unsuccessfully: %v\n%s", ee.ProcessState.String(), ee.Stderr)
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var mods []mod
|
||||
for dec := json.NewDecoder(bytes.NewReader(out)); ; {
|
||||
var m mod
|
||||
err := dec.Decode(&m)
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mods = append(mods, m)
|
||||
}
|
||||
return mods, nil
|
||||
}
|
||||
|
||||
// moduleFS is a vfs.FileSystem wrapper used when godoc is running
|
||||
// in module mode. It's needed so that packages inside modules are
|
||||
// considered to be third party.
|
||||
//
|
||||
// It overrides the RootType method of the underlying filesystem
|
||||
// and implements it using a heuristic based on the import path.
|
||||
// If the first element of the import path does not contain a dot,
|
||||
// that package is considered to be inside GOROOT. If it contains
|
||||
// a dot, then that package is considered to be third party.
|
||||
//
|
||||
// TODO(dmitshur): The RootType abstraction works well when GOPATH
|
||||
// workspaces are bound at their roots, but scales poorly in the
|
||||
// general case. It should be replaced by a more direct solution
|
||||
// for determining whether a package is third party or not.
|
||||
type moduleFS struct{ vfs.FileSystem }
|
||||
|
||||
func (moduleFS) RootType(path string) vfs.RootType {
|
||||
if !strings.HasPrefix(path, "/src/") {
|
||||
return ""
|
||||
}
|
||||
domain := path[len("/src/"):]
|
||||
if i := strings.Index(domain, "/"); i >= 0 {
|
||||
domain = domain[:i]
|
||||
}
|
||||
if !strings.Contains(domain, ".") {
|
||||
// No dot in the first element of import path
|
||||
// suggests this is a package in GOROOT.
|
||||
return vfs.RootTypeGoRoot
|
||||
} else {
|
||||
// A dot in the first element of import path
|
||||
// suggests this is a third party package.
|
||||
return vfs.RootTypeGoPath
|
||||
}
|
||||
}
|
||||
func (fs moduleFS) String() string { return "module(" + fs.FileSystem.String() + ")" }
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright 2013 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.
|
||||
|
||||
/*
|
||||
Command goimports updates your Go import lines,
|
||||
adding missing ones and removing unreferenced ones.
|
||||
|
||||
$ go install golang.org/x/tools/cmd/goimports@latest
|
||||
|
||||
In addition to fixing imports, goimports also formats
|
||||
your code in the same style as gofmt so it can be used
|
||||
as a replacement for your editor's gofmt-on-save hook.
|
||||
|
||||
For emacs, make sure you have the latest go-mode.el:
|
||||
|
||||
https://github.com/dominikh/go-mode.el
|
||||
|
||||
Then in your .emacs file:
|
||||
|
||||
(setq gofmt-command "goimports")
|
||||
(add-hook 'before-save-hook 'gofmt-before-save)
|
||||
|
||||
For vim, set "gofmt_command" to "goimports":
|
||||
|
||||
https://golang.org/change/39c724dd7f252
|
||||
https://golang.org/wiki/IDEsAndTextEditorPlugins
|
||||
etc
|
||||
|
||||
For GoSublime, follow the steps described here:
|
||||
|
||||
http://michaelwhatcott.com/gosublime-goimports/
|
||||
|
||||
For other editors, you probably know what to do.
|
||||
|
||||
To exclude directories in your $GOPATH from being scanned for Go
|
||||
files, goimports respects a configuration file at
|
||||
$GOPATH/src/.goimportsignore which may contain blank lines, comment
|
||||
lines (beginning with '#'), or lines naming a directory relative to
|
||||
the configuration file to ignore when scanning. No globbing or regex
|
||||
patterns are allowed. Use the "-v" verbose flag to verify it's
|
||||
working and see what goimports is doing.
|
||||
|
||||
File bugs or feature requests at:
|
||||
|
||||
https://golang.org/issues/new?title=x/tools/cmd/goimports:+
|
||||
|
||||
Happy hacking!
|
||||
*/
|
||||
package main // import "golang.org/x/tools/cmd/goimports"
|
||||
@@ -0,0 +1,379 @@
|
||||
// Copyright 2013 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 main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/scanner"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/tools/internal/gocommand"
|
||||
"golang.org/x/tools/internal/imports"
|
||||
)
|
||||
|
||||
var (
|
||||
// main operation modes
|
||||
list = flag.Bool("l", false, "list files whose formatting differs from goimport's")
|
||||
write = flag.Bool("w", false, "write result to (source) file instead of stdout")
|
||||
doDiff = flag.Bool("d", false, "display diffs instead of rewriting files")
|
||||
srcdir = flag.String("srcdir", "", "choose imports as if source code is from `dir`. When operating on a single file, dir may instead be the complete file name.")
|
||||
|
||||
verbose bool // verbose logging
|
||||
|
||||
cpuProfile = flag.String("cpuprofile", "", "CPU profile output")
|
||||
memProfile = flag.String("memprofile", "", "memory profile output")
|
||||
memProfileRate = flag.Int("memrate", 0, "if > 0, sets runtime.MemProfileRate")
|
||||
|
||||
options = &imports.Options{
|
||||
TabWidth: 8,
|
||||
TabIndent: true,
|
||||
Comments: true,
|
||||
Fragment: true,
|
||||
Env: &imports.ProcessEnv{
|
||||
GocmdRunner: &gocommand.Runner{},
|
||||
},
|
||||
}
|
||||
exitCode = 0
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&options.AllErrors, "e", false, "report all errors (not just the first 10 on different lines)")
|
||||
flag.StringVar(&options.LocalPrefix, "local", "", "put imports beginning with this string after 3rd-party packages; comma-separated list")
|
||||
flag.BoolVar(&options.FormatOnly, "format-only", false, "if true, don't fix imports and only format. In this mode, goimports is effectively gofmt, with the addition that imports are grouped into sections.")
|
||||
}
|
||||
|
||||
func report(err error) {
|
||||
scanner.PrintError(os.Stderr, err)
|
||||
exitCode = 2
|
||||
}
|
||||
|
||||
func usage() {
|
||||
fmt.Fprintf(os.Stderr, "usage: goimports [flags] [path ...]\n")
|
||||
flag.PrintDefaults()
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
func isGoFile(f os.FileInfo) bool {
|
||||
// ignore non-Go files
|
||||
name := f.Name()
|
||||
return !f.IsDir() && !strings.HasPrefix(name, ".") && strings.HasSuffix(name, ".go")
|
||||
}
|
||||
|
||||
// argumentType is which mode goimports was invoked as.
|
||||
type argumentType int
|
||||
|
||||
const (
|
||||
// fromStdin means the user is piping their source into goimports.
|
||||
fromStdin argumentType = iota
|
||||
|
||||
// singleArg is the common case from editors, when goimports is run on
|
||||
// a single file.
|
||||
singleArg
|
||||
|
||||
// multipleArg is when the user ran "goimports file1.go file2.go"
|
||||
// or ran goimports on a directory tree.
|
||||
multipleArg
|
||||
)
|
||||
|
||||
func processFile(filename string, in io.Reader, out io.Writer, argType argumentType) error {
|
||||
opt := options
|
||||
if argType == fromStdin {
|
||||
nopt := *options
|
||||
nopt.Fragment = true
|
||||
opt = &nopt
|
||||
}
|
||||
|
||||
if in == nil {
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
in = f
|
||||
}
|
||||
|
||||
src, err := io.ReadAll(in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
target := filename
|
||||
if *srcdir != "" {
|
||||
// Determine whether the provided -srcdirc is a directory or file
|
||||
// and then use it to override the target.
|
||||
//
|
||||
// See https://github.com/dominikh/go-mode.el/issues/146
|
||||
if isFile(*srcdir) {
|
||||
if argType == multipleArg {
|
||||
return errors.New("-srcdir value can't be a file when passing multiple arguments or when walking directories")
|
||||
}
|
||||
target = *srcdir
|
||||
} else if argType == singleArg && strings.HasSuffix(*srcdir, ".go") && !isDir(*srcdir) {
|
||||
// For a file which doesn't exist on disk yet, but might shortly.
|
||||
// e.g. user in editor opens $DIR/newfile.go and newfile.go doesn't yet exist on disk.
|
||||
// The goimports on-save hook writes the buffer to a temp file
|
||||
// first and runs goimports before the actual save to newfile.go.
|
||||
// The editor's buffer is named "newfile.go" so that is passed to goimports as:
|
||||
// goimports -srcdir=/gopath/src/pkg/newfile.go /tmp/gofmtXXXXXXXX.go
|
||||
// and then the editor reloads the result from the tmp file and writes
|
||||
// it to newfile.go.
|
||||
target = *srcdir
|
||||
} else {
|
||||
// Pretend that file is from *srcdir in order to decide
|
||||
// visible imports correctly.
|
||||
target = filepath.Join(*srcdir, filepath.Base(filename))
|
||||
}
|
||||
}
|
||||
|
||||
res, err := imports.Process(target, src, opt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !bytes.Equal(src, res) {
|
||||
// formatting has changed
|
||||
if *list {
|
||||
fmt.Fprintln(out, filename)
|
||||
}
|
||||
if *write {
|
||||
if argType == fromStdin {
|
||||
// filename is "<standard input>"
|
||||
return errors.New("can't use -w on stdin")
|
||||
}
|
||||
// On Windows, we need to re-set the permissions from the file. See golang/go#38225.
|
||||
var perms os.FileMode
|
||||
if fi, err := os.Stat(filename); err == nil {
|
||||
perms = fi.Mode() & os.ModePerm
|
||||
}
|
||||
err = os.WriteFile(filename, res, perms)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if *doDiff {
|
||||
if argType == fromStdin {
|
||||
filename = "stdin.go" // because <standard input>.orig looks silly
|
||||
}
|
||||
data, err := diff(src, res, filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("computing diff: %s", err)
|
||||
}
|
||||
fmt.Printf("diff -u %s %s\n", filepath.ToSlash(filename+".orig"), filepath.ToSlash(filename))
|
||||
out.Write(data)
|
||||
}
|
||||
}
|
||||
|
||||
if !*list && !*write && !*doDiff {
|
||||
_, err = out.Write(res)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func visitFile(path string, f os.FileInfo, err error) error {
|
||||
if err == nil && isGoFile(f) {
|
||||
err = processFile(path, nil, os.Stdout, multipleArg)
|
||||
}
|
||||
if err != nil {
|
||||
report(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func walkDir(path string) {
|
||||
filepath.Walk(path, visitFile)
|
||||
}
|
||||
|
||||
func main() {
|
||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||
|
||||
// call gofmtMain in a separate function
|
||||
// so that it can use defer and have them
|
||||
// run before the exit.
|
||||
gofmtMain()
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
|
||||
// parseFlags parses command line flags and returns the paths to process.
|
||||
// It's a var so that custom implementations can replace it in other files.
|
||||
var parseFlags = func() []string {
|
||||
flag.BoolVar(&verbose, "v", false, "verbose logging")
|
||||
|
||||
flag.Parse()
|
||||
return flag.Args()
|
||||
}
|
||||
|
||||
func bufferedFileWriter(dest string) (w io.Writer, close func()) {
|
||||
f, err := os.Create(dest)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
bw := bufio.NewWriter(f)
|
||||
return bw, func() {
|
||||
if err := bw.Flush(); err != nil {
|
||||
log.Fatalf("error flushing %v: %v", dest, err)
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func gofmtMain() {
|
||||
flag.Usage = usage
|
||||
paths := parseFlags()
|
||||
|
||||
if *cpuProfile != "" {
|
||||
bw, flush := bufferedFileWriter(*cpuProfile)
|
||||
pprof.StartCPUProfile(bw)
|
||||
defer flush()
|
||||
defer pprof.StopCPUProfile()
|
||||
}
|
||||
// doTrace is a conditionally compiled wrapper around runtime/trace. It is
|
||||
// used to allow goimports to compile under gccgo, which does not support
|
||||
// runtime/trace. See https://golang.org/issue/15544.
|
||||
defer doTrace()()
|
||||
if *memProfileRate > 0 {
|
||||
runtime.MemProfileRate = *memProfileRate
|
||||
bw, flush := bufferedFileWriter(*memProfile)
|
||||
defer func() {
|
||||
runtime.GC() // materialize all statistics
|
||||
if err := pprof.WriteHeapProfile(bw); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
flush()
|
||||
}()
|
||||
}
|
||||
|
||||
if verbose {
|
||||
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
|
||||
options.Env.Logf = log.Printf
|
||||
}
|
||||
if options.TabWidth < 0 {
|
||||
fmt.Fprintf(os.Stderr, "negative tabwidth %d\n", options.TabWidth)
|
||||
exitCode = 2
|
||||
return
|
||||
}
|
||||
|
||||
if len(paths) == 0 {
|
||||
if err := processFile("<standard input>", os.Stdin, os.Stdout, fromStdin); err != nil {
|
||||
report(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
argType := singleArg
|
||||
if len(paths) > 1 {
|
||||
argType = multipleArg
|
||||
}
|
||||
|
||||
for _, path := range paths {
|
||||
switch dir, err := os.Stat(path); {
|
||||
case err != nil:
|
||||
report(err)
|
||||
case dir.IsDir():
|
||||
walkDir(path)
|
||||
default:
|
||||
if err := processFile(path, nil, os.Stdout, argType); err != nil {
|
||||
report(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func writeTempFile(dir, prefix string, data []byte) (string, error) {
|
||||
file, err := os.CreateTemp(dir, prefix)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
_, err = file.Write(data)
|
||||
if err1 := file.Close(); err == nil {
|
||||
err = err1
|
||||
}
|
||||
if err != nil {
|
||||
os.Remove(file.Name())
|
||||
return "", err
|
||||
}
|
||||
return file.Name(), nil
|
||||
}
|
||||
|
||||
func diff(b1, b2 []byte, filename string) (data []byte, err error) {
|
||||
f1, err := writeTempFile("", "gofmt", b1)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer os.Remove(f1)
|
||||
|
||||
f2, err := writeTempFile("", "gofmt", b2)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer os.Remove(f2)
|
||||
|
||||
cmd := "diff"
|
||||
if runtime.GOOS == "plan9" {
|
||||
cmd = "/bin/ape/diff"
|
||||
}
|
||||
|
||||
data, err = exec.Command(cmd, "-u", f1, f2).CombinedOutput()
|
||||
if len(data) > 0 {
|
||||
// diff exits with a non-zero status when the files don't match.
|
||||
// Ignore that failure as long as we get output.
|
||||
return replaceTempFilename(data, filename)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// replaceTempFilename replaces temporary filenames in diff with actual one.
|
||||
//
|
||||
// --- /tmp/gofmt316145376 2017-02-03 19:13:00.280468375 -0500
|
||||
// +++ /tmp/gofmt617882815 2017-02-03 19:13:00.280468375 -0500
|
||||
// ...
|
||||
// ->
|
||||
// --- path/to/file.go.orig 2017-02-03 19:13:00.280468375 -0500
|
||||
// +++ path/to/file.go 2017-02-03 19:13:00.280468375 -0500
|
||||
// ...
|
||||
func replaceTempFilename(diff []byte, filename string) ([]byte, error) {
|
||||
bs := bytes.SplitN(diff, []byte{'\n'}, 3)
|
||||
if len(bs) < 3 {
|
||||
return nil, fmt.Errorf("got unexpected diff for %s", filename)
|
||||
}
|
||||
// Preserve timestamps.
|
||||
var t0, t1 []byte
|
||||
if i := bytes.LastIndexByte(bs[0], '\t'); i != -1 {
|
||||
t0 = bs[0][i:]
|
||||
}
|
||||
if i := bytes.LastIndexByte(bs[1], '\t'); i != -1 {
|
||||
t1 = bs[1][i:]
|
||||
}
|
||||
// Always print filepath with slash separator.
|
||||
f := filepath.ToSlash(filename)
|
||||
bs[0] = []byte(fmt.Sprintf("--- %s%s", f+".orig", t0))
|
||||
bs[1] = []byte(fmt.Sprintf("+++ %s%s", f, t1))
|
||||
return bytes.Join(bs, []byte{'\n'}), nil
|
||||
}
|
||||
|
||||
// isFile reports whether name is a file.
|
||||
func isFile(name string) bool {
|
||||
fi, err := os.Stat(name)
|
||||
return err == nil && fi.Mode().IsRegular()
|
||||
}
|
||||
|
||||
// isDir reports whether name is a directory.
|
||||
func isDir(name string) bool {
|
||||
fi, err := os.Stat(name)
|
||||
return err == nil && fi.IsDir()
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright 2016 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.
|
||||
|
||||
//go:build gc
|
||||
// +build gc
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"runtime/trace"
|
||||
)
|
||||
|
||||
var traceProfile = flag.String("trace", "", "trace profile output")
|
||||
|
||||
func doTrace() func() {
|
||||
if *traceProfile != "" {
|
||||
bw, flush := bufferedFileWriter(*traceProfile)
|
||||
trace.Start(bw)
|
||||
return func() {
|
||||
trace.Stop()
|
||||
flush()
|
||||
}
|
||||
}
|
||||
return func() {}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright 2016 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.
|
||||
|
||||
//go:build !gc
|
||||
// +build !gc
|
||||
|
||||
package main
|
||||
|
||||
func doTrace() func() {
|
||||
return func() {}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
// Copyright 2015 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 gomvpkg command moves go packages, updating import declarations.
|
||||
// See the -help message or Usage constant for details.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/build"
|
||||
"os"
|
||||
|
||||
"golang.org/x/tools/go/buildutil"
|
||||
"golang.org/x/tools/refactor/rename"
|
||||
)
|
||||
|
||||
var (
|
||||
fromFlag = flag.String("from", "", "Import path of package to be moved")
|
||||
toFlag = flag.String("to", "", "Destination import path for package")
|
||||
vcsMvCmdFlag = flag.String("vcs_mv_cmd", "", `A template for the version control system's "move directory" command, e.g. "git mv {{.Src}} {{.Dst}}"`)
|
||||
helpFlag = flag.Bool("help", false, "show usage message")
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.Var((*buildutil.TagsFlag)(&build.Default.BuildTags), "tags", buildutil.TagsFlagDoc)
|
||||
}
|
||||
|
||||
const Usage = `gomvpkg: moves a package, updating import declarations
|
||||
|
||||
Usage:
|
||||
|
||||
gomvpkg -from <path> -to <path> [-vcs_mv_cmd <template>]
|
||||
|
||||
Flags:
|
||||
|
||||
-from specifies the import path of the package to be moved
|
||||
|
||||
-to specifies the destination import path
|
||||
|
||||
-vcs_mv_cmd specifies a shell command to inform the version control system of a
|
||||
directory move. The argument is a template using the syntax of the
|
||||
text/template package. It has two fields: Src and Dst, the absolute
|
||||
paths of the directories.
|
||||
|
||||
For example: "git mv {{.Src}} {{.Dst}}"
|
||||
|
||||
gomvpkg determines the set of packages that might be affected, including all
|
||||
packages importing the 'from' package and any of its subpackages. It will move
|
||||
the 'from' package and all its subpackages to the destination path and update all
|
||||
imports of those packages to point to its new import path.
|
||||
|
||||
gomvpkg rejects moves in which a package already exists at the destination import
|
||||
path, or in which a directory already exists at the location the package would be
|
||||
moved to.
|
||||
|
||||
gomvpkg will not always be able to rename imports when a package's name is changed.
|
||||
Import statements may want further cleanup.
|
||||
|
||||
gomvpkg's behavior is not defined if any of the packages to be moved are
|
||||
imported using dot imports.
|
||||
|
||||
Examples:
|
||||
|
||||
% gomvpkg -from myproject/foo -to myproject/bar
|
||||
|
||||
Move the package with import path "myproject/foo" to the new path
|
||||
"myproject/bar".
|
||||
|
||||
% gomvpkg -from myproject/foo -to myproject/bar -vcs_mv_cmd "git mv {{.Src}} {{.Dst}}"
|
||||
|
||||
Move the package with import path "myproject/foo" to the new path
|
||||
"myproject/bar" using "git mv" to execute the directory move.
|
||||
`
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
if len(flag.Args()) > 0 {
|
||||
fmt.Fprintln(os.Stderr, "gomvpkg: surplus arguments.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if *helpFlag || *fromFlag == "" || *toFlag == "" {
|
||||
fmt.Print(Usage)
|
||||
return
|
||||
}
|
||||
|
||||
if err := rename.Move(&build.Default, *fromFlag, *toFlag, *vcsMvCmdFlag); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "gomvpkg: %s.\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
// 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.
|
||||
|
||||
// Gonew starts a new Go module by copying a template module.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// gonew srcmod[@version] [dstmod [dir]]
|
||||
//
|
||||
// Gonew makes a copy of the srcmod module, changing its module path to dstmod.
|
||||
// It writes that new module to a new directory named by dir.
|
||||
// If dir already exists, it must be an empty directory.
|
||||
// If dir is omitted, gonew uses ./elem where elem is the final path element of dstmod.
|
||||
//
|
||||
// This command is highly experimental and subject to change.
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// To install gonew:
|
||||
//
|
||||
// go install golang.org/x/tools/cmd/gonew@latest
|
||||
//
|
||||
// To clone the basic command-line program template golang.org/x/example/hello
|
||||
// as your.domain/myprog, in the directory ./myprog:
|
||||
//
|
||||
// gonew golang.org/x/example/hello your.domain/myprog
|
||||
//
|
||||
// To clone the latest copy of the rsc.io/quote module, keeping that module path,
|
||||
// into ./quote:
|
||||
//
|
||||
// gonew rsc.io/quote
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/mod/modfile"
|
||||
"golang.org/x/mod/module"
|
||||
"golang.org/x/tools/internal/edit"
|
||||
)
|
||||
|
||||
func usage() {
|
||||
fmt.Fprintf(os.Stderr, "usage: gonew srcmod[@version] [dstmod [dir]]\n")
|
||||
fmt.Fprintf(os.Stderr, "See https://pkg.go.dev/golang.org/x/tools/cmd/gonew.\n")
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.SetPrefix("gonew: ")
|
||||
log.SetFlags(0)
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
args := flag.Args()
|
||||
|
||||
if len(args) < 1 || len(args) > 3 {
|
||||
usage()
|
||||
}
|
||||
|
||||
srcMod := args[0]
|
||||
srcModVers := srcMod
|
||||
if !strings.Contains(srcModVers, "@") {
|
||||
srcModVers += "@latest"
|
||||
}
|
||||
srcMod, _, _ = strings.Cut(srcMod, "@")
|
||||
if err := module.CheckPath(srcMod); err != nil {
|
||||
log.Fatalf("invalid source module name: %v", err)
|
||||
}
|
||||
|
||||
dstMod := srcMod
|
||||
if len(args) >= 2 {
|
||||
dstMod = args[1]
|
||||
if err := module.CheckPath(dstMod); err != nil {
|
||||
log.Fatalf("invalid destination module name: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
var dir string
|
||||
if len(args) == 3 {
|
||||
dir = args[2]
|
||||
} else {
|
||||
dir = "." + string(filepath.Separator) + path.Base(dstMod)
|
||||
}
|
||||
|
||||
// Dir must not exist or must be an empty directory.
|
||||
de, err := os.ReadDir(dir)
|
||||
if err == nil && len(de) > 0 {
|
||||
log.Fatalf("target directory %s exists and is non-empty", dir)
|
||||
}
|
||||
needMkdir := err != nil
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd := exec.Command("go", "mod", "download", "-json", srcModVers)
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
log.Fatalf("go mod download -json %s: %v\n%s%s", srcModVers, err, stderr.Bytes(), stdout.Bytes())
|
||||
}
|
||||
|
||||
var info struct {
|
||||
Dir string
|
||||
}
|
||||
if err := json.Unmarshal(stdout.Bytes(), &info); err != nil {
|
||||
log.Fatalf("go mod download -json %s: invalid JSON output: %v\n%s%s", srcMod, err, stderr.Bytes(), stdout.Bytes())
|
||||
}
|
||||
|
||||
if needMkdir {
|
||||
if err := os.MkdirAll(dir, 0777); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Copy from module cache into new directory, making edits as needed.
|
||||
filepath.WalkDir(info.Dir, func(src string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
rel, err := filepath.Rel(info.Dir, src)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
dst := filepath.Join(dir, rel)
|
||||
if d.IsDir() {
|
||||
if err := os.MkdirAll(dst, 0777); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(src)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
isRoot := !strings.Contains(rel, string(filepath.Separator))
|
||||
if strings.HasSuffix(rel, ".go") {
|
||||
data = fixGo(data, rel, srcMod, dstMod, isRoot)
|
||||
}
|
||||
if rel == "go.mod" {
|
||||
data = fixGoMod(data, srcMod, dstMod)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(dst, data, 0666); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
log.Printf("initialized %s in %s", dstMod, dir)
|
||||
}
|
||||
|
||||
// fixGo rewrites the Go source in data to replace srcMod with dstMod.
|
||||
// isRoot indicates whether the file is in the root directory of the module,
|
||||
// in which case we also update the package name.
|
||||
func fixGo(data []byte, file string, srcMod, dstMod string, isRoot bool) []byte {
|
||||
fset := token.NewFileSet()
|
||||
f, err := parser.ParseFile(fset, file, data, parser.ImportsOnly)
|
||||
if err != nil {
|
||||
log.Fatalf("parsing source module:\n%s", err)
|
||||
}
|
||||
|
||||
buf := edit.NewBuffer(data)
|
||||
at := func(p token.Pos) int {
|
||||
return fset.File(p).Offset(p)
|
||||
}
|
||||
|
||||
srcName := path.Base(srcMod)
|
||||
dstName := path.Base(dstMod)
|
||||
if isRoot {
|
||||
if name := f.Name.Name; name == srcName || name == srcName+"_test" {
|
||||
dname := dstName + strings.TrimPrefix(name, srcName)
|
||||
if !token.IsIdentifier(dname) {
|
||||
log.Fatalf("%s: cannot rename package %s to package %s: invalid package name", file, name, dname)
|
||||
}
|
||||
buf.Replace(at(f.Name.Pos()), at(f.Name.End()), dname)
|
||||
}
|
||||
}
|
||||
|
||||
for _, spec := range f.Imports {
|
||||
path, err := strconv.Unquote(spec.Path.Value)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if path == srcMod {
|
||||
if srcName != dstName && spec.Name == nil {
|
||||
// Add package rename because source code uses original name.
|
||||
// The renaming looks strange, but template authors are unlikely to
|
||||
// create a template where the root package is imported by packages
|
||||
// in subdirectories, and the renaming at least keeps the code working.
|
||||
// A more sophisticated approach would be to rename the uses of
|
||||
// the package identifier in the file too, but then you have to worry about
|
||||
// name collisions, and given how unlikely this is, it doesn't seem worth
|
||||
// trying to clean up the file that way.
|
||||
buf.Insert(at(spec.Path.Pos()), srcName+" ")
|
||||
}
|
||||
// Change import path to dstMod
|
||||
buf.Replace(at(spec.Path.Pos()), at(spec.Path.End()), strconv.Quote(dstMod))
|
||||
}
|
||||
if strings.HasPrefix(path, srcMod+"/") {
|
||||
// Change import path to begin with dstMod
|
||||
buf.Replace(at(spec.Path.Pos()), at(spec.Path.End()), strconv.Quote(strings.Replace(path, srcMod, dstMod, 1)))
|
||||
}
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// fixGoMod rewrites the go.mod content in data to replace srcMod with dstMod
|
||||
// in the module path.
|
||||
func fixGoMod(data []byte, srcMod, dstMod string) []byte {
|
||||
f, err := modfile.ParseLax("go.mod", data, nil)
|
||||
if err != nil {
|
||||
log.Fatalf("parsing source module:\n%s", err)
|
||||
}
|
||||
f.AddModuleStmt(dstMod)
|
||||
new, err := f.Format()
|
||||
if err != nil {
|
||||
return data
|
||||
}
|
||||
return new
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
// 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 main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/internal/diffp"
|
||||
"golang.org/x/tools/internal/testenv"
|
||||
"golang.org/x/tools/txtar"
|
||||
)
|
||||
|
||||
func init() {
|
||||
if os.Getenv("TestGonewMain") == "1" {
|
||||
main()
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// Each file in testdata is a txtar file with the command to run,
|
||||
// the contents of modules to initialize in a fake proxy,
|
||||
// the expected stdout and stderr, and the expected file contents.
|
||||
files, err := filepath.Glob("testdata/*.txt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(files) == 0 {
|
||||
t.Fatal("no test cases")
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
t.Run(filepath.Base(file), func(t *testing.T) {
|
||||
data, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ar := txtar.Parse(data)
|
||||
|
||||
// If the command begins with ! it means it should fail.
|
||||
// After the optional ! the first argument must be 'gonew'
|
||||
// followed by the arguments to gonew.
|
||||
args := strings.Fields(string(ar.Comment))
|
||||
wantFail := false
|
||||
if len(args) > 0 && args[0] == "!" {
|
||||
wantFail = true
|
||||
args = args[1:]
|
||||
}
|
||||
if len(args) == 0 || args[0] != "gonew" {
|
||||
t.Fatalf("invalid command comment")
|
||||
}
|
||||
|
||||
// Collect modules into proxy tree and store in temp directory.
|
||||
dir := t.TempDir()
|
||||
proxyDir := filepath.Join(dir, "proxy")
|
||||
writeProxyFiles(t, proxyDir, ar)
|
||||
extra := ""
|
||||
if runtime.GOOS == "windows" {
|
||||
// Windows absolute paths don't start with / so we need one more.
|
||||
extra = "/"
|
||||
}
|
||||
proxyURL := "file://" + extra + filepath.ToSlash(proxyDir)
|
||||
|
||||
// Run gonew in a fresh 'out' directory.
|
||||
out := filepath.Join(dir, "out")
|
||||
if err := os.Mkdir(out, 0777); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmd := exec.Command(exe, args[1:]...)
|
||||
cmd.Dir = out
|
||||
cmd.Env = append(os.Environ(), "TestGonewMain=1", "GOPROXY="+proxyURL, "GOSUMDB=off")
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
if err := cmd.Run(); err == nil && wantFail {
|
||||
t.Errorf("unexpected success exit")
|
||||
} else if err != nil && !wantFail {
|
||||
t.Errorf("unexpected failure exit")
|
||||
}
|
||||
|
||||
// Collect the expected output from the txtar.
|
||||
want := make(map[string]txtar.File)
|
||||
for _, f := range ar.Files {
|
||||
if f.Name == "stdout" || f.Name == "stderr" || strings.HasPrefix(f.Name, "out/") {
|
||||
want[f.Name] = f
|
||||
}
|
||||
}
|
||||
|
||||
// Check stdout and stderr.
|
||||
// Change \ to / so Windows output looks like Unix output.
|
||||
stdoutBuf := bytes.ReplaceAll(stdout.Bytes(), []byte(`\`), []byte("/"))
|
||||
stderrBuf := bytes.ReplaceAll(stderr.Bytes(), []byte(`\`), []byte("/"))
|
||||
// Note that stdout and stderr can be omitted from the archive if empty.
|
||||
if !bytes.Equal(stdoutBuf, want["stdout"].Data) {
|
||||
t.Errorf("wrong stdout: %s", diffp.Diff("want", want["stdout"].Data, "have", stdoutBuf))
|
||||
}
|
||||
if !bytes.Equal(stderrBuf, want["stderr"].Data) {
|
||||
t.Errorf("wrong stderr: %s", diffp.Diff("want", want["stderr"].Data, "have", stderrBuf))
|
||||
}
|
||||
delete(want, "stdout")
|
||||
delete(want, "stderr")
|
||||
|
||||
// Check remaining expected outputs.
|
||||
err = filepath.WalkDir(out, func(name string, info fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
data, err := os.ReadFile(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
short := "out" + filepath.ToSlash(strings.TrimPrefix(name, out))
|
||||
f, ok := want[short]
|
||||
if !ok {
|
||||
t.Errorf("unexpected file %s:\n%s", short, data)
|
||||
return nil
|
||||
}
|
||||
delete(want, short)
|
||||
if !bytes.Equal(data, f.Data) {
|
||||
t.Errorf("wrong %s: %s", short, diffp.Diff("want", f.Data, "have", data))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for name := range want {
|
||||
t.Errorf("missing file %s", name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// A Zip is a zip file being written.
|
||||
type Zip struct {
|
||||
buf bytes.Buffer
|
||||
w *zip.Writer
|
||||
}
|
||||
|
||||
// writeProxyFiles collects all the module content from ar and writes
|
||||
// files in the format of the proxy URL space, so that the 'proxy' directory
|
||||
// can be used in a GOPROXY=file:/// URL.
|
||||
func writeProxyFiles(t *testing.T, proxy string, ar *txtar.Archive) {
|
||||
zips := make(map[string]*Zip)
|
||||
others := make(map[string]string)
|
||||
for _, f := range ar.Files {
|
||||
i := strings.Index(f.Name, "@")
|
||||
if i < 0 {
|
||||
continue
|
||||
}
|
||||
j := strings.Index(f.Name[i:], "/")
|
||||
if j < 0 {
|
||||
t.Fatalf("unexpected archive file %s", f.Name)
|
||||
}
|
||||
j += i
|
||||
mod, vers, file := f.Name[:i], f.Name[i+1:j], f.Name[j+1:]
|
||||
zipName := mod + "/@v/" + vers + ".zip"
|
||||
z := zips[zipName]
|
||||
if z == nil {
|
||||
others[mod+"/@v/list"] += vers + "\n"
|
||||
others[mod+"/@v/"+vers+".info"] = fmt.Sprintf("{%q: %q}\n", "Version", vers)
|
||||
z = new(Zip)
|
||||
z.w = zip.NewWriter(&z.buf)
|
||||
zips[zipName] = z
|
||||
}
|
||||
if file == "go.mod" {
|
||||
others[mod+"/@v/"+vers+".mod"] = string(f.Data)
|
||||
}
|
||||
w, err := z.w.Create(f.Name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := w.Write(f.Data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
for name, z := range zips {
|
||||
if err := z.w.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(filepath.Join(proxy, name)), 0777); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(proxy, name), z.buf.Bytes(), 0666); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
for name, data := range others {
|
||||
// zip loop already created directory
|
||||
if err := os.WriteFile(filepath.Join(proxy, name), []byte(data), 0666); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
gonew example.com/quote my.com/test
|
||||
|
||||
-- example.com/quote@v1.5.2/go.mod --
|
||||
module example.com/quote
|
||||
-- example.com/quote@v1.5.2/quote.go --
|
||||
package quote
|
||||
|
||||
import (
|
||||
"example.com/quote/bar"
|
||||
)
|
||||
|
||||
func Quote() {}
|
||||
-- example.com/quote@v1.5.2/quote/another.go --
|
||||
package quote // another package quote!
|
||||
-- stderr --
|
||||
gonew: initialized my.com/test in ./test
|
||||
-- out/test/go.mod --
|
||||
module my.com/test
|
||||
-- out/test/quote.go --
|
||||
package test
|
||||
|
||||
import (
|
||||
"my.com/test/bar"
|
||||
)
|
||||
|
||||
func Quote() {}
|
||||
-- out/test/quote/another.go --
|
||||
package quote // another package quote!
|
||||
@@ -0,0 +1,383 @@
|
||||
// Copyright 2017 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 main_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/internal/testenv"
|
||||
)
|
||||
|
||||
type test struct {
|
||||
offset, from, to string // specify the arguments
|
||||
fileSpecified bool // true if the offset or from args specify a specific file
|
||||
pkgs map[string][]string
|
||||
wantErr bool
|
||||
wantOut string // a substring expected to be in the output
|
||||
packages map[string][]string // a map of the package name to the files contained within, which will be numbered by i.go where i is the index
|
||||
}
|
||||
|
||||
// Test that renaming that would modify cgo files will produce an error and not modify the file.
|
||||
func TestGeneratedFiles(t *testing.T) {
|
||||
testenv.NeedsTool(t, "go")
|
||||
testenv.NeedsTool(t, "cgo")
|
||||
|
||||
tmp, bin, cleanup := buildGorename(t)
|
||||
defer cleanup()
|
||||
|
||||
srcDir := filepath.Join(tmp, "src")
|
||||
err := os.Mkdir(srcDir, os.ModePerm)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var env = []string{fmt.Sprintf("GOPATH=%s", tmp)}
|
||||
for _, envVar := range os.Environ() {
|
||||
if !strings.HasPrefix(envVar, "GOPATH=") {
|
||||
env = append(env, envVar)
|
||||
}
|
||||
}
|
||||
// gorename currently requires GOPATH mode.
|
||||
env = append(env, "GO111MODULE=off")
|
||||
|
||||
// Testing renaming in packages that include cgo files:
|
||||
for iter, renameTest := range []test{
|
||||
{
|
||||
// Test: variable not used in any cgo file -> no error
|
||||
from: `"mytest"::f`, to: "g",
|
||||
packages: map[string][]string{
|
||||
"mytest": []string{`package mytest; func f() {}`,
|
||||
`package mytest
|
||||
// #include <stdio.h>
|
||||
import "C"
|
||||
|
||||
func z() {C.puts(nil)}`},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOut: "Renamed 1 occurrence in 1 file in 1 package.",
|
||||
}, {
|
||||
// Test: to name used in cgo file -> rename error
|
||||
from: `"mytest"::f`, to: "g",
|
||||
packages: map[string][]string{
|
||||
"mytest": []string{`package mytest; func f() {}`,
|
||||
`package mytest
|
||||
// #include <stdio.h>
|
||||
import "C"
|
||||
|
||||
func g() {C.puts(nil)}`},
|
||||
},
|
||||
wantErr: true,
|
||||
wantOut: "conflicts with func in same block",
|
||||
},
|
||||
{
|
||||
// Test: from name in package in cgo file -> error
|
||||
from: `"mytest"::f`, to: "g",
|
||||
packages: map[string][]string{
|
||||
"mytest": []string{`package mytest
|
||||
|
||||
// #include <stdio.h>
|
||||
import "C"
|
||||
|
||||
func f() { C.puts(nil); }
|
||||
`},
|
||||
},
|
||||
wantErr: true,
|
||||
wantOut: "gorename: refusing to modify generated file containing DO NOT EDIT marker:",
|
||||
}, {
|
||||
// Test: from name in cgo file -> error
|
||||
from: filepath.Join("mytest", "0.go") + `::f`, to: "g",
|
||||
fileSpecified: true,
|
||||
packages: map[string][]string{
|
||||
"mytest": []string{`package mytest
|
||||
|
||||
// #include <stdio.h>
|
||||
import "C"
|
||||
|
||||
func f() { C.puts(nil); }
|
||||
`},
|
||||
},
|
||||
wantErr: true,
|
||||
wantOut: "gorename: refusing to modify generated file containing DO NOT EDIT marker:",
|
||||
}, {
|
||||
// Test: offset in cgo file -> identifier in cgo error
|
||||
offset: filepath.Join("main", "0.go") + `:#78`, to: "bar",
|
||||
fileSpecified: true,
|
||||
wantErr: true,
|
||||
packages: map[string][]string{
|
||||
"main": {`package main
|
||||
|
||||
// #include <unistd.h>
|
||||
import "C"
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
foo := 1
|
||||
C.close(2)
|
||||
fmt.Println(foo)
|
||||
}
|
||||
`},
|
||||
},
|
||||
wantOut: "cannot rename identifiers in generated file containing DO NOT EDIT marker:",
|
||||
}, {
|
||||
// Test: from identifier appears in cgo file in another package -> error
|
||||
from: `"test"::Foo`, to: "Bar",
|
||||
packages: map[string][]string{
|
||||
"test": []string{
|
||||
`package test
|
||||
|
||||
func Foo(x int) (int){
|
||||
return x * 2
|
||||
}
|
||||
`,
|
||||
},
|
||||
"main": []string{
|
||||
`package main
|
||||
|
||||
import "test"
|
||||
import "fmt"
|
||||
// #include <unistd.h>
|
||||
import "C"
|
||||
|
||||
func fun() {
|
||||
x := test.Foo(3)
|
||||
C.close(3)
|
||||
fmt.Println(x)
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
wantOut: "gorename: refusing to modify generated file containing DO NOT EDIT marker:",
|
||||
}, {
|
||||
// Test: from identifier doesn't appear in cgo file that includes modified package -> rename successful
|
||||
from: `"test".Foo::x`, to: "y",
|
||||
packages: map[string][]string{
|
||||
"test": []string{
|
||||
`package test
|
||||
|
||||
func Foo(x int) (int){
|
||||
return x * 2
|
||||
}
|
||||
`,
|
||||
},
|
||||
"main": []string{
|
||||
`package main
|
||||
import "test"
|
||||
import "fmt"
|
||||
// #include <unistd.h>
|
||||
import "C"
|
||||
|
||||
func fun() {
|
||||
x := test.Foo(3)
|
||||
C.close(3)
|
||||
fmt.Println(x)
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOut: "Renamed 2 occurrences in 1 file in 1 package.",
|
||||
}, {
|
||||
// Test: from name appears in cgo file in same package -> error
|
||||
from: `"mytest"::f`, to: "g",
|
||||
packages: map[string][]string{
|
||||
"mytest": []string{`package mytest; func f() {}`,
|
||||
`package mytest
|
||||
// #include <stdio.h>
|
||||
import "C"
|
||||
|
||||
func z() {C.puts(nil); f()}`,
|
||||
`package mytest
|
||||
// #include <unistd.h>
|
||||
import "C"
|
||||
|
||||
func foo() {C.close(3); f()}`,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
wantOut: "gorename: refusing to modify generated files containing DO NOT EDIT marker:",
|
||||
}, {
|
||||
// Test: from name in file, identifier not used in cgo file -> rename successful
|
||||
from: filepath.Join("mytest", "0.go") + `::f`, to: "g",
|
||||
fileSpecified: true,
|
||||
packages: map[string][]string{
|
||||
"mytest": []string{`package mytest; func f() {}`,
|
||||
`package mytest
|
||||
// #include <stdio.h>
|
||||
import "C"
|
||||
|
||||
func z() {C.puts(nil)}`},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOut: "Renamed 1 occurrence in 1 file in 1 package.",
|
||||
}, {
|
||||
// Test: from identifier imported to another package but does not modify cgo file -> rename successful
|
||||
from: `"test".Foo`, to: "Bar",
|
||||
packages: map[string][]string{
|
||||
"test": []string{
|
||||
`package test
|
||||
|
||||
func Foo(x int) (int){
|
||||
return x * 2
|
||||
}
|
||||
`,
|
||||
},
|
||||
"main": []string{
|
||||
`package main
|
||||
// #include <unistd.h>
|
||||
import "C"
|
||||
|
||||
func fun() {
|
||||
C.close(3)
|
||||
}
|
||||
`,
|
||||
`package main
|
||||
import "test"
|
||||
import "fmt"
|
||||
func g() { fmt.Println(test.Foo(3)) }
|
||||
`,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOut: "Renamed 2 occurrences in 2 files in 2 packages.",
|
||||
},
|
||||
} {
|
||||
// Write the test files
|
||||
testCleanup := setUpPackages(t, srcDir, renameTest.packages)
|
||||
|
||||
// Set up arguments
|
||||
var args []string
|
||||
|
||||
var arg, val string
|
||||
if renameTest.offset != "" {
|
||||
arg, val = "-offset", renameTest.offset
|
||||
} else {
|
||||
arg, val = "-from", renameTest.from
|
||||
}
|
||||
|
||||
prefix := fmt.Sprintf("%d: %s %q -to %q", iter, arg, val, renameTest.to)
|
||||
|
||||
if renameTest.fileSpecified {
|
||||
// add the src dir to the value of the argument
|
||||
val = filepath.Join(srcDir, val)
|
||||
}
|
||||
|
||||
args = append(args, arg, val, "-to", renameTest.to)
|
||||
|
||||
// Run command
|
||||
cmd := exec.Command(bin, args...)
|
||||
cmd.Args[0] = "gorename"
|
||||
cmd.Env = env
|
||||
|
||||
// Check the output
|
||||
out, err := cmd.CombinedOutput()
|
||||
// errors should result in no changes to files
|
||||
if err != nil {
|
||||
if !renameTest.wantErr {
|
||||
t.Errorf("%s: received unexpected error %s", prefix, err)
|
||||
}
|
||||
// Compare output
|
||||
if ok := strings.Contains(string(out), renameTest.wantOut); !ok {
|
||||
t.Errorf("%s: unexpected command output: %s (want: %s)", prefix, out, renameTest.wantOut)
|
||||
}
|
||||
// Check that no files were modified
|
||||
if modified := modifiedFiles(t, srcDir, renameTest.packages); len(modified) != 0 {
|
||||
t.Errorf("%s: files unexpectedly modified: %s", prefix, modified)
|
||||
}
|
||||
|
||||
} else {
|
||||
if !renameTest.wantErr {
|
||||
if ok := strings.Contains(string(out), renameTest.wantOut); !ok {
|
||||
t.Errorf("%s: unexpected command output: %s (want: %s)", prefix, out, renameTest.wantOut)
|
||||
}
|
||||
} else {
|
||||
t.Errorf("%s: command succeeded unexpectedly, output: %s", prefix, out)
|
||||
}
|
||||
}
|
||||
testCleanup()
|
||||
}
|
||||
}
|
||||
|
||||
// buildGorename builds the gorename executable.
|
||||
// It returns its path, and a cleanup function.
|
||||
func buildGorename(t *testing.T) (tmp, bin string, cleanup func()) {
|
||||
if runtime.GOOS == "android" {
|
||||
t.Skipf("the dependencies are not available on android")
|
||||
}
|
||||
|
||||
tmp, err := os.MkdirTemp("", "gorename-regtest-")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if cleanup == nil { // probably, go build failed.
|
||||
os.RemoveAll(tmp)
|
||||
}
|
||||
}()
|
||||
|
||||
bin = filepath.Join(tmp, "gorename")
|
||||
if runtime.GOOS == "windows" {
|
||||
bin += ".exe"
|
||||
}
|
||||
cmd := exec.Command("go", "build", "-o", bin)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("Building gorename: %v\n%s", err, out)
|
||||
}
|
||||
return tmp, bin, func() { os.RemoveAll(tmp) }
|
||||
}
|
||||
|
||||
// setUpPackages sets up the files in a temporary directory provided by arguments.
|
||||
func setUpPackages(t *testing.T, dir string, packages map[string][]string) (cleanup func()) {
|
||||
var pkgDirs []string
|
||||
|
||||
for pkgName, files := range packages {
|
||||
// Create a directory for the package.
|
||||
pkgDir := filepath.Join(dir, pkgName)
|
||||
pkgDirs = append(pkgDirs, pkgDir)
|
||||
|
||||
if err := os.Mkdir(pkgDir, os.ModePerm); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Write the packages files
|
||||
for i, val := range files {
|
||||
file := filepath.Join(pkgDir, strconv.Itoa(i)+".go")
|
||||
if err := os.WriteFile(file, []byte(val), os.ModePerm); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return func() {
|
||||
for _, dir := range pkgDirs {
|
||||
os.RemoveAll(dir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// modifiedFiles returns a list of files that were renamed (without the prefix dir).
|
||||
func modifiedFiles(t *testing.T, dir string, packages map[string][]string) (results []string) {
|
||||
|
||||
for pkgName, files := range packages {
|
||||
pkgDir := filepath.Join(dir, pkgName)
|
||||
|
||||
for i, val := range files {
|
||||
file := filepath.Join(pkgDir, strconv.Itoa(i)+".go")
|
||||
// read file contents and compare to val
|
||||
if contents, err := os.ReadFile(file); err != nil {
|
||||
t.Fatalf("File missing: %s", err)
|
||||
} else if string(contents) != val {
|
||||
results = append(results, strings.TrimPrefix(dir, file))
|
||||
}
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright 2014 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 gorename command performs precise type-safe renaming of
|
||||
// identifiers in Go source code.
|
||||
//
|
||||
// Run with -help for usage information, or view the Usage constant in
|
||||
// package golang.org/x/tools/refactor/rename, which contains most of
|
||||
// the implementation.
|
||||
package main // import "golang.org/x/tools/cmd/gorename"
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/build"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"golang.org/x/tools/go/buildutil"
|
||||
"golang.org/x/tools/refactor/rename"
|
||||
)
|
||||
|
||||
var (
|
||||
offsetFlag = flag.String("offset", "", "file and byte offset of identifier to be renamed, e.g. 'file.go:#123'. For use by editors.")
|
||||
fromFlag = flag.String("from", "", "identifier to be renamed; see -help for formats")
|
||||
toFlag = flag.String("to", "", "new name for identifier")
|
||||
helpFlag = flag.Bool("help", false, "show usage message")
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.Var((*buildutil.TagsFlag)(&build.Default.BuildTags), "tags", buildutil.TagsFlagDoc)
|
||||
flag.BoolVar(&rename.Force, "force", false, "proceed, even if conflicts were reported")
|
||||
flag.BoolVar(&rename.Verbose, "v", false, "print verbose information")
|
||||
flag.BoolVar(&rename.Diff, "d", false, "display diffs instead of rewriting files")
|
||||
flag.StringVar(&rename.DiffCmd, "diffcmd", "diff", "diff command invoked when using -d")
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.SetPrefix("gorename: ")
|
||||
log.SetFlags(0)
|
||||
flag.Parse()
|
||||
if len(flag.Args()) > 0 {
|
||||
log.Fatal("surplus arguments")
|
||||
}
|
||||
|
||||
if *helpFlag || (*offsetFlag == "" && *fromFlag == "" && *toFlag == "") {
|
||||
fmt.Print(rename.Usage)
|
||||
return
|
||||
}
|
||||
|
||||
if err := rename.Main(&build.Default, *offsetFlag, *fromFlag, *toFlag); err != nil {
|
||||
if err != rename.ConflictError {
|
||||
log.Fatal(err)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
// Copyright 2011 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.
|
||||
|
||||
// gotype.go is a copy of the original source maintained
|
||||
// in $GOROOT/src/go/types/gotype.go, but with the call
|
||||
// to types.SizesFor factored out so we can provide a local
|
||||
// implementation when compiling against Go 1.8 and earlier.
|
||||
//
|
||||
// This code is here for the sole purpose of satisfying historic
|
||||
// references to this location, and for making gotype accessible
|
||||
// via 'go get'.
|
||||
//
|
||||
// Do NOT make changes to this version as they will not be maintained
|
||||
// (and possibly overwritten). Any changes should be made to the original
|
||||
// and then ported to here.
|
||||
|
||||
/*
|
||||
The gotype command, like the front-end of a Go compiler, parses and
|
||||
type-checks a single Go package. Errors are reported if the analysis
|
||||
fails; otherwise gotype is quiet (unless -v is set).
|
||||
|
||||
Without a list of paths, gotype reads from standard input, which
|
||||
must provide a single Go source file defining a complete package.
|
||||
|
||||
With a single directory argument, gotype checks the Go files in
|
||||
that directory, comprising a single package. Use -t to include the
|
||||
(in-package) _test.go files. Use -x to type check only external
|
||||
test files.
|
||||
|
||||
Otherwise, each path must be the filename of a Go file belonging
|
||||
to the same package.
|
||||
|
||||
Imports are processed by importing directly from the source of
|
||||
imported packages (default), or by importing from compiled and
|
||||
installed packages (by setting -c to the respective compiler).
|
||||
|
||||
The -c flag must be set to a compiler ("gc", "gccgo") when type-
|
||||
checking packages containing imports with relative import paths
|
||||
(import "./mypkg") because the source importer cannot know which
|
||||
files to include for such packages.
|
||||
|
||||
Usage:
|
||||
|
||||
gotype [flags] [path...]
|
||||
|
||||
The flags are:
|
||||
|
||||
-t
|
||||
include local test files in a directory (ignored if -x is provided)
|
||||
-x
|
||||
consider only external test files in a directory
|
||||
-e
|
||||
report all errors (not just the first 10)
|
||||
-v
|
||||
verbose mode
|
||||
-c
|
||||
compiler used for installed packages (gc, gccgo, or source); default: source
|
||||
|
||||
Flags controlling additional output:
|
||||
|
||||
-ast
|
||||
print AST (forces -seq)
|
||||
-trace
|
||||
print parse trace (forces -seq)
|
||||
-comments
|
||||
parse comments (ignored unless -ast or -trace is provided)
|
||||
|
||||
Examples:
|
||||
|
||||
To check the files a.go, b.go, and c.go:
|
||||
|
||||
gotype a.go b.go c.go
|
||||
|
||||
To check an entire package including (in-package) tests in the directory dir and print the processed files:
|
||||
|
||||
gotype -t -v dir
|
||||
|
||||
To check the external test package (if any) in the current directory, based on installed packages compiled with
|
||||
cmd/compile:
|
||||
|
||||
gotype -c=gc -x .
|
||||
|
||||
To verify the output of a pipe:
|
||||
|
||||
echo "package foo" | gotype
|
||||
*/
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/build"
|
||||
"go/importer"
|
||||
"go/parser"
|
||||
"go/scanner"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// main operation modes
|
||||
testFiles = flag.Bool("t", false, "include in-package test files in a directory")
|
||||
xtestFiles = flag.Bool("x", false, "consider only external test files in a directory")
|
||||
allErrors = flag.Bool("e", false, "report all errors, not just the first 10")
|
||||
verbose = flag.Bool("v", false, "verbose mode")
|
||||
compiler = flag.String("c", defaultCompiler, "compiler used for installed packages (gc, gccgo, or source)")
|
||||
|
||||
// additional output control
|
||||
printAST = flag.Bool("ast", false, "print AST (forces -seq)")
|
||||
printTrace = flag.Bool("trace", false, "print parse trace (forces -seq)")
|
||||
parseComments = flag.Bool("comments", false, "parse comments (ignored unless -ast or -trace is provided)")
|
||||
)
|
||||
|
||||
var (
|
||||
fset = token.NewFileSet()
|
||||
errorCount = 0
|
||||
sequential = false
|
||||
parserMode parser.Mode
|
||||
)
|
||||
|
||||
func initParserMode() {
|
||||
if *allErrors {
|
||||
parserMode |= parser.AllErrors
|
||||
}
|
||||
if *printAST {
|
||||
sequential = true
|
||||
}
|
||||
if *printTrace {
|
||||
parserMode |= parser.Trace
|
||||
sequential = true
|
||||
}
|
||||
if *parseComments && (*printAST || *printTrace) {
|
||||
parserMode |= parser.ParseComments
|
||||
}
|
||||
}
|
||||
|
||||
const usageString = `usage: gotype [flags] [path ...]
|
||||
|
||||
The gotype command, like the front-end of a Go compiler, parses and
|
||||
type-checks a single Go package. Errors are reported if the analysis
|
||||
fails; otherwise gotype is quiet (unless -v is set).
|
||||
|
||||
Without a list of paths, gotype reads from standard input, which
|
||||
must provide a single Go source file defining a complete package.
|
||||
|
||||
With a single directory argument, gotype checks the Go files in
|
||||
that directory, comprising a single package. Use -t to include the
|
||||
(in-package) _test.go files. Use -x to type check only external
|
||||
test files.
|
||||
|
||||
Otherwise, each path must be the filename of a Go file belonging
|
||||
to the same package.
|
||||
|
||||
Imports are processed by importing directly from the source of
|
||||
imported packages (default), or by importing from compiled and
|
||||
installed packages (by setting -c to the respective compiler).
|
||||
|
||||
The -c flag must be set to a compiler ("gc", "gccgo") when type-
|
||||
checking packages containing imports with relative import paths
|
||||
(import "./mypkg") because the source importer cannot know which
|
||||
files to include for such packages.
|
||||
`
|
||||
|
||||
func usage() {
|
||||
fmt.Fprint(os.Stderr, usageString)
|
||||
fmt.Fprintln(os.Stderr)
|
||||
flag.PrintDefaults()
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
func report(err error) {
|
||||
scanner.PrintError(os.Stderr, err)
|
||||
if list, ok := err.(scanner.ErrorList); ok {
|
||||
errorCount += len(list)
|
||||
return
|
||||
}
|
||||
errorCount++
|
||||
}
|
||||
|
||||
// parse may be called concurrently
|
||||
func parse(filename string, src interface{}) (*ast.File, error) {
|
||||
if *verbose {
|
||||
fmt.Println(filename)
|
||||
}
|
||||
file, err := parser.ParseFile(fset, filename, src, parserMode) // ok to access fset concurrently
|
||||
if *printAST {
|
||||
ast.Print(fset, file)
|
||||
}
|
||||
return file, err
|
||||
}
|
||||
|
||||
func parseStdin() (*ast.File, error) {
|
||||
src, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parse("<standard input>", src)
|
||||
}
|
||||
|
||||
func parseFiles(dir string, filenames []string) ([]*ast.File, error) {
|
||||
files := make([]*ast.File, len(filenames))
|
||||
errors := make([]error, len(filenames))
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i, filename := range filenames {
|
||||
wg.Add(1)
|
||||
go func(i int, filepath string) {
|
||||
defer wg.Done()
|
||||
files[i], errors[i] = parse(filepath, nil)
|
||||
}(i, filepath.Join(dir, filename))
|
||||
if sequential {
|
||||
wg.Wait()
|
||||
}
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// if there are errors, return the first one for deterministic results
|
||||
for _, err := range errors {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func parseDir(dir string) ([]*ast.File, error) {
|
||||
ctxt := build.Default
|
||||
pkginfo, err := ctxt.ImportDir(dir, 0)
|
||||
if _, nogo := err.(*build.NoGoError); err != nil && !nogo {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if *xtestFiles {
|
||||
return parseFiles(dir, pkginfo.XTestGoFiles)
|
||||
}
|
||||
|
||||
filenames := append(pkginfo.GoFiles, pkginfo.CgoFiles...)
|
||||
if *testFiles {
|
||||
filenames = append(filenames, pkginfo.TestGoFiles...)
|
||||
}
|
||||
return parseFiles(dir, filenames)
|
||||
}
|
||||
|
||||
func getPkgFiles(args []string) ([]*ast.File, error) {
|
||||
if len(args) == 0 {
|
||||
// stdin
|
||||
file, err := parseStdin()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []*ast.File{file}, nil
|
||||
}
|
||||
|
||||
if len(args) == 1 {
|
||||
// possibly a directory
|
||||
path := args[0]
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if info.IsDir() {
|
||||
return parseDir(path)
|
||||
}
|
||||
}
|
||||
|
||||
// list of files
|
||||
return parseFiles("", args)
|
||||
}
|
||||
|
||||
func checkPkgFiles(files []*ast.File) {
|
||||
type bailout struct{}
|
||||
|
||||
// if checkPkgFiles is called multiple times, set up conf only once
|
||||
conf := types.Config{
|
||||
FakeImportC: true,
|
||||
Error: func(err error) {
|
||||
if !*allErrors && errorCount >= 10 {
|
||||
panic(bailout{})
|
||||
}
|
||||
report(err)
|
||||
},
|
||||
Importer: importer.ForCompiler(fset, *compiler, nil),
|
||||
Sizes: SizesFor(build.Default.Compiler, build.Default.GOARCH),
|
||||
}
|
||||
|
||||
defer func() {
|
||||
switch p := recover().(type) {
|
||||
case nil, bailout:
|
||||
// normal return or early exit
|
||||
default:
|
||||
// re-panic
|
||||
panic(p)
|
||||
}
|
||||
}()
|
||||
|
||||
const path = "pkg" // any non-empty string will do for now
|
||||
conf.Check(path, fset, files, nil)
|
||||
}
|
||||
|
||||
func printStats(d time.Duration) {
|
||||
fileCount := 0
|
||||
lineCount := 0
|
||||
fset.Iterate(func(f *token.File) bool {
|
||||
fileCount++
|
||||
lineCount += f.LineCount()
|
||||
return true
|
||||
})
|
||||
|
||||
fmt.Printf(
|
||||
"%s (%d files, %d lines, %d lines/s)\n",
|
||||
d, fileCount, lineCount, int64(float64(lineCount)/d.Seconds()),
|
||||
)
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
initParserMode()
|
||||
|
||||
start := time.Now()
|
||||
|
||||
files, err := getPkgFiles(flag.Args())
|
||||
if err != nil {
|
||||
report(err)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
checkPkgFiles(files)
|
||||
if errorCount > 0 {
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
if *verbose {
|
||||
printStats(time.Since(start))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright 2017 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.
|
||||
|
||||
//go:build !go1.9
|
||||
// +build !go1.9
|
||||
|
||||
// This file contains a copy of the implementation of types.SizesFor
|
||||
// since this function is not available in go/types before Go 1.9.
|
||||
|
||||
package main
|
||||
|
||||
import "go/types"
|
||||
|
||||
const defaultCompiler = "gc"
|
||||
|
||||
var gcArchSizes = map[string]*types.StdSizes{
|
||||
"386": {4, 4},
|
||||
"arm": {4, 4},
|
||||
"arm64": {8, 8},
|
||||
"amd64": {8, 8},
|
||||
"amd64p32": {4, 8},
|
||||
"mips": {4, 4},
|
||||
"mipsle": {4, 4},
|
||||
"mips64": {8, 8},
|
||||
"mips64le": {8, 8},
|
||||
"ppc64": {8, 8},
|
||||
"ppc64le": {8, 8},
|
||||
"s390x": {8, 8},
|
||||
}
|
||||
|
||||
func SizesFor(compiler, arch string) types.Sizes {
|
||||
if compiler != "gc" {
|
||||
return nil
|
||||
}
|
||||
s, ok := gcArchSizes[arch]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return s
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Copyright 2017 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.
|
||||
|
||||
//go:build go1.9
|
||||
// +build go1.9
|
||||
|
||||
package main
|
||||
|
||||
import "go/types"
|
||||
|
||||
const defaultCompiler = "source"
|
||||
|
||||
func SizesFor(compiler, arch string) types.Sizes {
|
||||
return types.SizesFor(compiler, arch)
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright 2009 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.
|
||||
|
||||
/*
|
||||
Goyacc is a version of yacc for Go.
|
||||
It is written in Go and generates parsers written in Go.
|
||||
|
||||
Usage:
|
||||
|
||||
goyacc args...
|
||||
|
||||
It is largely transliterated from the Inferno version written in Limbo
|
||||
which in turn was largely transliterated from the Plan 9 version
|
||||
written in C and documented at
|
||||
|
||||
https://9p.io/magic/man2html/1/yacc
|
||||
|
||||
Adepts of the original yacc will have no trouble adapting to this
|
||||
form of the tool.
|
||||
|
||||
The directory $GOPATH/src/golang.org/x/tools/cmd/goyacc/testdata/expr
|
||||
is a yacc program for a very simple expression parser. See expr.y and
|
||||
main.go in that directory for examples of how to write and build
|
||||
goyacc programs.
|
||||
|
||||
The generated parser is reentrant. The parsing function yyParse expects
|
||||
to be given an argument that conforms to the following interface:
|
||||
|
||||
type yyLexer interface {
|
||||
Lex(lval *yySymType) int
|
||||
Error(e string)
|
||||
}
|
||||
|
||||
Lex should return the token identifier, and place other token
|
||||
information in lval (which replaces the usual yylval).
|
||||
Error is equivalent to yyerror in the original yacc.
|
||||
|
||||
Code inside the grammar actions may refer to the variable yylex,
|
||||
which holds the yyLexer passed to yyParse.
|
||||
|
||||
Clients that need to understand more about the parser state can
|
||||
create the parser separately from invoking it. The function yyNewParser
|
||||
returns a yyParser conforming to the following interface:
|
||||
|
||||
type yyParser interface {
|
||||
Parse(yyLex) int
|
||||
Lookahead() int
|
||||
}
|
||||
|
||||
Parse runs the parser; the top-level call yyParse(yylex) is equivalent
|
||||
to yyNewParser().Parse(yylex).
|
||||
|
||||
Lookahead can be called during grammar actions to read (but not consume)
|
||||
the value of the current lookahead token, as returned by yylex.Lex.
|
||||
If there is no current lookahead token (because the parser has not called Lex
|
||||
or has consumed the token returned by the most recent call to Lex),
|
||||
Lookahead returns -1. Calling Lookahead is equivalent to reading
|
||||
yychar from within in a grammar action.
|
||||
|
||||
Multiple grammars compiled into a single program should be placed in
|
||||
distinct packages. If that is impossible, the "-p prefix" flag to
|
||||
goyacc sets the prefix, by default yy, that begins the names of
|
||||
symbols, including types, the parser, and the lexer, generated and
|
||||
referenced by yacc's generated code. Setting it to distinct values
|
||||
allows multiple grammars to be placed in a single package.
|
||||
*/
|
||||
package main
|
||||
@@ -0,0 +1,20 @@
|
||||
This directory contains a simple program demonstrating how to use
|
||||
the Go version of yacc.
|
||||
|
||||
To build it:
|
||||
|
||||
$ go generate
|
||||
$ go build
|
||||
|
||||
or
|
||||
|
||||
$ go generate
|
||||
$ go run expr.go
|
||||
|
||||
The file main.go contains the "go generate" command to run yacc to
|
||||
create expr.go from expr.y. It also has the package doc comment,
|
||||
as godoc will not scan the .y file.
|
||||
|
||||
The actual implementation is in expr.y.
|
||||
|
||||
The program is not installed in the binary distributions of Go.
|
||||
@@ -0,0 +1,202 @@
|
||||
// Copyright 2013 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 is an example of a goyacc program.
|
||||
// To build it:
|
||||
// goyacc -p "expr" expr.y (produces y.go)
|
||||
// go build -o expr y.go
|
||||
// expr
|
||||
// > <type an expression>
|
||||
|
||||
%{
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math/big"
|
||||
"os"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
%}
|
||||
|
||||
%union {
|
||||
num *big.Rat
|
||||
}
|
||||
|
||||
%type <num> expr expr1 expr2 expr3
|
||||
|
||||
%token '+' '-' '*' '/' '(' ')'
|
||||
|
||||
%token <num> NUM
|
||||
|
||||
%%
|
||||
|
||||
top:
|
||||
expr
|
||||
{
|
||||
if $1.IsInt() {
|
||||
fmt.Println($1.Num().String())
|
||||
} else {
|
||||
fmt.Println($1.String())
|
||||
}
|
||||
}
|
||||
|
||||
expr:
|
||||
expr1
|
||||
| '+' expr
|
||||
{
|
||||
$$ = $2
|
||||
}
|
||||
| '-' expr
|
||||
{
|
||||
$$ = $2.Neg($2)
|
||||
}
|
||||
|
||||
expr1:
|
||||
expr2
|
||||
| expr1 '+' expr2
|
||||
{
|
||||
$$ = $1.Add($1, $3)
|
||||
}
|
||||
| expr1 '-' expr2
|
||||
{
|
||||
$$ = $1.Sub($1, $3)
|
||||
}
|
||||
|
||||
expr2:
|
||||
expr3
|
||||
| expr2 '*' expr3
|
||||
{
|
||||
$$ = $1.Mul($1, $3)
|
||||
}
|
||||
| expr2 '/' expr3
|
||||
{
|
||||
$$ = $1.Quo($1, $3)
|
||||
}
|
||||
|
||||
expr3:
|
||||
NUM
|
||||
| '(' expr ')'
|
||||
{
|
||||
$$ = $2
|
||||
}
|
||||
|
||||
|
||||
%%
|
||||
|
||||
// The parser expects the lexer to return 0 on EOF. Give it a name
|
||||
// for clarity.
|
||||
const eof = 0
|
||||
|
||||
// The parser uses the type <prefix>Lex as a lexer. It must provide
|
||||
// the methods Lex(*<prefix>SymType) int and Error(string).
|
||||
type exprLex struct {
|
||||
line []byte
|
||||
peek rune
|
||||
}
|
||||
|
||||
// The parser calls this method to get each new token. This
|
||||
// implementation returns operators and NUM.
|
||||
func (x *exprLex) Lex(yylval *exprSymType) int {
|
||||
for {
|
||||
c := x.next()
|
||||
switch c {
|
||||
case eof:
|
||||
return eof
|
||||
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
|
||||
return x.num(c, yylval)
|
||||
case '+', '-', '*', '/', '(', ')':
|
||||
return int(c)
|
||||
|
||||
// Recognize Unicode multiplication and division
|
||||
// symbols, returning what the parser expects.
|
||||
case '×':
|
||||
return '*'
|
||||
case '÷':
|
||||
return '/'
|
||||
|
||||
case ' ', '\t', '\n', '\r':
|
||||
default:
|
||||
log.Printf("unrecognized character %q", c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Lex a number.
|
||||
func (x *exprLex) num(c rune, yylval *exprSymType) int {
|
||||
add := func(b *bytes.Buffer, c rune) {
|
||||
if _, err := b.WriteRune(c); err != nil {
|
||||
log.Fatalf("WriteRune: %s", err)
|
||||
}
|
||||
}
|
||||
var b bytes.Buffer
|
||||
add(&b, c)
|
||||
L: for {
|
||||
c = x.next()
|
||||
switch c {
|
||||
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', 'e', 'E':
|
||||
add(&b, c)
|
||||
default:
|
||||
break L
|
||||
}
|
||||
}
|
||||
if c != eof {
|
||||
x.peek = c
|
||||
}
|
||||
yylval.num = &big.Rat{}
|
||||
_, ok := yylval.num.SetString(b.String())
|
||||
if !ok {
|
||||
log.Printf("bad number %q", b.String())
|
||||
return eof
|
||||
}
|
||||
return NUM
|
||||
}
|
||||
|
||||
// Return the next rune for the lexer.
|
||||
func (x *exprLex) next() rune {
|
||||
if x.peek != eof {
|
||||
r := x.peek
|
||||
x.peek = eof
|
||||
return r
|
||||
}
|
||||
if len(x.line) == 0 {
|
||||
return eof
|
||||
}
|
||||
c, size := utf8.DecodeRune(x.line)
|
||||
x.line = x.line[size:]
|
||||
if c == utf8.RuneError && size == 1 {
|
||||
log.Print("invalid utf8")
|
||||
return x.next()
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// The parser calls this method on a parse error.
|
||||
func (x *exprLex) Error(s string) {
|
||||
log.Printf("parse error: %s", s)
|
||||
}
|
||||
|
||||
func main() {
|
||||
in := bufio.NewReader(os.Stdin)
|
||||
for {
|
||||
if _, err := os.Stdout.WriteString("> "); err != nil {
|
||||
log.Fatalf("WriteString: %s", err)
|
||||
}
|
||||
line, err := in.ReadBytes('\n')
|
||||
if err == io.EOF {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("ReadBytes: %s", err)
|
||||
}
|
||||
|
||||
exprParse(&exprLex{line: line})
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user