➕ Update vendor
This commit is contained in:
parent
982ef9c07f
commit
e39b302bdc
|
@ -0,0 +1,10 @@
|
|||
# Treat all files in this repo as binary, with no git magic updating
|
||||
# line endings. Windows users contributing to Go will need to use a
|
||||
# modern version of git and editors capable of LF line endings.
|
||||
#
|
||||
# We'll prevent accidental CRLF line endings from entering the repo
|
||||
# via the git-review gofmt checks.
|
||||
#
|
||||
# See golang.org/issue/9281
|
||||
|
||||
* -text
|
|
@ -0,0 +1,2 @@
|
|||
# Add no patterns to .gitignore except for files generated by the build.
|
||||
last-change
|
|
@ -0,0 +1,3 @@
|
|||
# This source code refers to The Go Authors for copyright purposes.
|
||||
# The master list of authors is in the main Go distribution,
|
||||
# visible at http://tip.golang.org/AUTHORS.
|
|
@ -0,0 +1,31 @@
|
|||
# Contributing to Go
|
||||
|
||||
Go is an open source project.
|
||||
|
||||
It is the work of hundreds of contributors. We appreciate your help!
|
||||
|
||||
|
||||
## Filing issues
|
||||
|
||||
When [filing an issue](https://golang.org/issue/new), make sure to answer these five questions:
|
||||
|
||||
1. What version of Go are you using (`go version`)?
|
||||
2. What operating system and processor architecture are you using?
|
||||
3. What did you do?
|
||||
4. What did you expect to see?
|
||||
5. What did you see instead?
|
||||
|
||||
General questions should go to the [golang-nuts mailing list](https://groups.google.com/group/golang-nuts) instead of the issue tracker.
|
||||
The gophers there will answer or ask you to file an issue if you've tripped over a bug.
|
||||
|
||||
## Contributing code
|
||||
|
||||
Please read the [Contribution Guidelines](https://golang.org/doc/contribute.html)
|
||||
before sending patches.
|
||||
|
||||
**We do not accept GitHub pull requests**
|
||||
(we use [Gerrit](https://code.google.com/p/gerrit/) instead for code review).
|
||||
|
||||
Unless otherwise noted, the Go source files are distributed under
|
||||
the BSD-style license found in the LICENSE file.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# This source code was written by the Go contributors.
|
||||
# The master list of contributors is in the main Go distribution,
|
||||
# visible at http://tip.golang.org/CONTRIBUTORS.
|
|
@ -0,0 +1,27 @@
|
|||
# Go Tools
|
||||
|
||||
This subrepository holds the source for various packages and tools that support
|
||||
the Go programming language.
|
||||
|
||||
Some of the tools, `godoc` and `vet` for example, are included in binary Go
|
||||
distributions.
|
||||
|
||||
Others, including the Go `guru` and the test coverage tool, can be fetched with
|
||||
`go get`.
|
||||
|
||||
Packages include a type-checker for Go and an implementation of the
|
||||
Static Single Assignment form (SSA) representation for Go programs.
|
||||
|
||||
## Download/Install
|
||||
|
||||
The easiest way to install is to run `go get -u golang.org/x/tools/...`. You can
|
||||
also manually git clone the repository to `$GOPATH/src/golang.org/x/tools`.
|
||||
|
||||
## Report Issues / Send Patches
|
||||
|
||||
This repository uses Gerrit for code changes. To learn how to submit changes to
|
||||
this repository, see https://golang.org/doc/contribute.html.
|
||||
|
||||
The main issue tracker for the tools repository is located at
|
||||
https://github.com/golang/go/issues. Prefix your issue with "x/tools/(your
|
||||
subdir):" in the subject line, so it is easy to find.
|
|
@ -0,0 +1,131 @@
|
|||
// 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 parse provides support for parsing benchmark results as
|
||||
// generated by 'go test -bench'.
|
||||
package parse // import "golang.org/x/tools/benchmark/parse"
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Flags used by Benchmark.Measured to indicate
|
||||
// which measurements a Benchmark contains.
|
||||
const (
|
||||
NsPerOp = 1 << iota
|
||||
MBPerS
|
||||
AllocedBytesPerOp
|
||||
AllocsPerOp
|
||||
)
|
||||
|
||||
// Benchmark is one run of a single benchmark.
|
||||
type Benchmark struct {
|
||||
Name string // benchmark name
|
||||
N int // number of iterations
|
||||
NsPerOp float64 // nanoseconds per iteration
|
||||
AllocedBytesPerOp uint64 // bytes allocated per iteration
|
||||
AllocsPerOp uint64 // allocs per iteration
|
||||
MBPerS float64 // MB processed per second
|
||||
Measured int // which measurements were recorded
|
||||
Ord int // ordinal position within a benchmark run
|
||||
}
|
||||
|
||||
// ParseLine extracts a Benchmark from a single line of testing.B
|
||||
// output.
|
||||
func ParseLine(line string) (*Benchmark, error) {
|
||||
fields := strings.Fields(line)
|
||||
|
||||
// Two required, positional fields: Name and iterations.
|
||||
if len(fields) < 2 {
|
||||
return nil, fmt.Errorf("two fields required, have %d", len(fields))
|
||||
}
|
||||
if !strings.HasPrefix(fields[0], "Benchmark") {
|
||||
return nil, fmt.Errorf(`first field does not start with "Benchmark"`)
|
||||
}
|
||||
n, err := strconv.Atoi(fields[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b := &Benchmark{Name: fields[0], N: n}
|
||||
|
||||
// Parse any remaining pairs of fields; we've parsed one pair already.
|
||||
for i := 1; i < len(fields)/2; i++ {
|
||||
b.parseMeasurement(fields[i*2], fields[i*2+1])
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (b *Benchmark) parseMeasurement(quant string, unit string) {
|
||||
switch unit {
|
||||
case "ns/op":
|
||||
if f, err := strconv.ParseFloat(quant, 64); err == nil {
|
||||
b.NsPerOp = f
|
||||
b.Measured |= NsPerOp
|
||||
}
|
||||
case "MB/s":
|
||||
if f, err := strconv.ParseFloat(quant, 64); err == nil {
|
||||
b.MBPerS = f
|
||||
b.Measured |= MBPerS
|
||||
}
|
||||
case "B/op":
|
||||
if i, err := strconv.ParseUint(quant, 10, 64); err == nil {
|
||||
b.AllocedBytesPerOp = i
|
||||
b.Measured |= AllocedBytesPerOp
|
||||
}
|
||||
case "allocs/op":
|
||||
if i, err := strconv.ParseUint(quant, 10, 64); err == nil {
|
||||
b.AllocsPerOp = i
|
||||
b.Measured |= AllocsPerOp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Benchmark) String() string {
|
||||
buf := new(bytes.Buffer)
|
||||
fmt.Fprintf(buf, "%s %d", b.Name, b.N)
|
||||
if (b.Measured & NsPerOp) != 0 {
|
||||
fmt.Fprintf(buf, " %.2f ns/op", b.NsPerOp)
|
||||
}
|
||||
if (b.Measured & MBPerS) != 0 {
|
||||
fmt.Fprintf(buf, " %.2f MB/s", b.MBPerS)
|
||||
}
|
||||
if (b.Measured & AllocedBytesPerOp) != 0 {
|
||||
fmt.Fprintf(buf, " %d B/op", b.AllocedBytesPerOp)
|
||||
}
|
||||
if (b.Measured & AllocsPerOp) != 0 {
|
||||
fmt.Fprintf(buf, " %d allocs/op", b.AllocsPerOp)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// Set is a collection of benchmarks from one
|
||||
// testing.B run, keyed by name to facilitate comparison.
|
||||
type Set map[string][]*Benchmark
|
||||
|
||||
// ParseSet extracts a Set from testing.B output.
|
||||
// ParseSet preserves the order of benchmarks that have identical
|
||||
// names.
|
||||
func ParseSet(r io.Reader) (Set, error) {
|
||||
bb := make(Set)
|
||||
scan := bufio.NewScanner(r)
|
||||
ord := 0
|
||||
for scan.Scan() {
|
||||
if b, err := ParseLine(scan.Text()); err == nil {
|
||||
b.Ord = ord
|
||||
ord++
|
||||
bb[b.Name] = append(bb[b.Name], b)
|
||||
}
|
||||
}
|
||||
|
||||
if err := scan.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return bb, nil
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
// 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 parse
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseLine(t *testing.T) {
|
||||
cases := []struct {
|
||||
line string
|
||||
want *Benchmark
|
||||
err bool // expect an error
|
||||
}{
|
||||
{
|
||||
line: "BenchmarkEncrypt 100000000 19.6 ns/op",
|
||||
want: &Benchmark{
|
||||
Name: "BenchmarkEncrypt",
|
||||
N: 100000000, NsPerOp: 19.6,
|
||||
Measured: NsPerOp,
|
||||
},
|
||||
},
|
||||
{
|
||||
line: "BenchmarkEncrypt 100000000 19.6 ns/op 817.77 MB/s",
|
||||
want: &Benchmark{
|
||||
Name: "BenchmarkEncrypt",
|
||||
N: 100000000, NsPerOp: 19.6, MBPerS: 817.77,
|
||||
Measured: NsPerOp | MBPerS,
|
||||
},
|
||||
},
|
||||
{
|
||||
line: "BenchmarkEncrypt 100000000 19.6 ns/op 817.77",
|
||||
want: &Benchmark{
|
||||
Name: "BenchmarkEncrypt",
|
||||
N: 100000000, NsPerOp: 19.6,
|
||||
Measured: NsPerOp,
|
||||
},
|
||||
},
|
||||
{
|
||||
line: "BenchmarkEncrypt 100000000 19.6 ns/op 817.77 MB/s 5 allocs/op",
|
||||
want: &Benchmark{
|
||||
Name: "BenchmarkEncrypt",
|
||||
N: 100000000, NsPerOp: 19.6, MBPerS: 817.77, AllocsPerOp: 5,
|
||||
Measured: NsPerOp | MBPerS | AllocsPerOp,
|
||||
},
|
||||
},
|
||||
{
|
||||
line: "BenchmarkEncrypt 100000000 19.6 ns/op 817.77 MB/s 3 B/op 5 allocs/op",
|
||||
want: &Benchmark{
|
||||
Name: "BenchmarkEncrypt",
|
||||
N: 100000000, NsPerOp: 19.6, MBPerS: 817.77, AllocedBytesPerOp: 3, AllocsPerOp: 5,
|
||||
Measured: NsPerOp | MBPerS | AllocedBytesPerOp | AllocsPerOp,
|
||||
},
|
||||
},
|
||||
// error handling cases
|
||||
{
|
||||
line: "BenchPress 100 19.6 ns/op", // non-benchmark
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
line: "BenchmarkEncrypt lots 19.6 ns/op", // non-int iterations
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
line: "BenchmarkBridge 100000000 19.6 smoots", // unknown unit
|
||||
want: &Benchmark{
|
||||
Name: "BenchmarkBridge",
|
||||
N: 100000000,
|
||||
},
|
||||
},
|
||||
{
|
||||
line: "PASS",
|
||||
err: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
have, err := ParseLine(tt.line)
|
||||
if tt.err && err == nil {
|
||||
t.Errorf("parsing line %q should have failed", tt.line)
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(have, tt.want) {
|
||||
t.Errorf("parsed line %q incorrectly, want %v have %v", tt.line, tt.want, have)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSet(t *testing.T) {
|
||||
// Test two things:
|
||||
// 1. The noise that can accompany testing.B output gets ignored.
|
||||
// 2. Benchmarks with the same name have their order preserved.
|
||||
in := `
|
||||
? crypto [no test files]
|
||||
PASS
|
||||
pem_decrypt_test.go:17: test 4. %!s(x509.PEMCipher=5)
|
||||
... [output truncated]
|
||||
|
||||
BenchmarkEncrypt 100000000 19.6 ns/op
|
||||
BenchmarkEncrypt 5000000 517 ns/op
|
||||
=== RUN TestChunk
|
||||
--- PASS: TestChunk (0.00 seconds)
|
||||
--- SKIP: TestLinuxSendfile (0.00 seconds)
|
||||
fs_test.go:716: skipping; linux-only test
|
||||
BenchmarkReadRequestApachebench 1000000 2960 ns/op 27.70 MB/s 839 B/op 9 allocs/op
|
||||
BenchmarkClientServerParallel64 50000 59192 ns/op 7028 B/op 60 allocs/op
|
||||
ok net/http 95.783s
|
||||
`
|
||||
|
||||
want := Set{
|
||||
"BenchmarkReadRequestApachebench": []*Benchmark{
|
||||
{
|
||||
Name: "BenchmarkReadRequestApachebench",
|
||||
N: 1000000, NsPerOp: 2960, MBPerS: 27.70, AllocedBytesPerOp: 839, AllocsPerOp: 9,
|
||||
Measured: NsPerOp | MBPerS | AllocedBytesPerOp | AllocsPerOp,
|
||||
Ord: 2,
|
||||
},
|
||||
},
|
||||
"BenchmarkClientServerParallel64": []*Benchmark{
|
||||
{
|
||||
Name: "BenchmarkClientServerParallel64",
|
||||
N: 50000, NsPerOp: 59192, AllocedBytesPerOp: 7028, AllocsPerOp: 60,
|
||||
Measured: NsPerOp | AllocedBytesPerOp | AllocsPerOp,
|
||||
Ord: 3,
|
||||
},
|
||||
},
|
||||
"BenchmarkEncrypt": []*Benchmark{
|
||||
{
|
||||
Name: "BenchmarkEncrypt",
|
||||
N: 100000000, NsPerOp: 19.6,
|
||||
Measured: NsPerOp,
|
||||
Ord: 0,
|
||||
},
|
||||
{
|
||||
Name: "BenchmarkEncrypt",
|
||||
N: 5000000, NsPerOp: 517,
|
||||
Measured: NsPerOp,
|
||||
Ord: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
have, err := ParseSet(strings.NewReader(in))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err during ParseSet: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(want, have) {
|
||||
t.Errorf("parsed bench set incorrectly, want %v have %v", want, have)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
// 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.
|
||||
|
||||
// Adapted from encoding/xml/read_test.go.
|
||||
|
||||
// Package atom defines XML data structures for an Atom feed.
|
||||
package atom // import "golang.org/x/tools/blog/atom"
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Feed struct {
|
||||
XMLName xml.Name `xml:"http://www.w3.org/2005/Atom feed"`
|
||||
Title string `xml:"title"`
|
||||
ID string `xml:"id"`
|
||||
Link []Link `xml:"link"`
|
||||
Updated TimeStr `xml:"updated"`
|
||||
Author *Person `xml:"author"`
|
||||
Entry []*Entry `xml:"entry"`
|
||||
}
|
||||
|
||||
type Entry struct {
|
||||
Title string `xml:"title"`
|
||||
ID string `xml:"id"`
|
||||
Link []Link `xml:"link"`
|
||||
Published TimeStr `xml:"published"`
|
||||
Updated TimeStr `xml:"updated"`
|
||||
Author *Person `xml:"author"`
|
||||
Summary *Text `xml:"summary"`
|
||||
Content *Text `xml:"content"`
|
||||
}
|
||||
|
||||
type Link struct {
|
||||
Rel string `xml:"rel,attr,omitempty"`
|
||||
Href string `xml:"href,attr"`
|
||||
Type string `xml:"type,attr,omitempty"`
|
||||
HrefLang string `xml:"hreflang,attr,omitempty"`
|
||||
Title string `xml:"title,attr,omitempty"`
|
||||
Length uint `xml:"length,attr,omitempty"`
|
||||
}
|
||||
|
||||
type Person struct {
|
||||
Name string `xml:"name"`
|
||||
URI string `xml:"uri,omitempty"`
|
||||
Email string `xml:"email,omitempty"`
|
||||
InnerXML string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
type Text struct {
|
||||
Type string `xml:"type,attr"`
|
||||
Body string `xml:",chardata"`
|
||||
}
|
||||
|
||||
type TimeStr string
|
||||
|
||||
func Time(t time.Time) TimeStr {
|
||||
return TimeStr(t.Format("2006-01-02T15:04:05-07:00"))
|
||||
}
|
|
@ -0,0 +1,437 @@
|
|||
// 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 blog implements a web server for articles written in present format.
|
||||
package blog // import "golang.org/x/tools/blog"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/tools/blog/atom"
|
||||
"golang.org/x/tools/present"
|
||||
)
|
||||
|
||||
var validJSONPFunc = regexp.MustCompile(`(?i)^[a-z_][a-z0-9_.]*$`)
|
||||
|
||||
// Config specifies Server configuration values.
|
||||
type Config struct {
|
||||
ContentPath string // Relative or absolute location of article files and related content.
|
||||
TemplatePath string // Relative or absolute location of template files.
|
||||
|
||||
BaseURL string // Absolute base URL (for permalinks; no trailing slash).
|
||||
BasePath string // Base URL path relative to server root (no trailing slash).
|
||||
GodocURL string // The base URL of godoc (for menu bar; no trailing slash).
|
||||
Hostname string // Server host name, used for rendering ATOM feeds.
|
||||
|
||||
HomeArticles int // Articles to display on the home page.
|
||||
FeedArticles int // Articles to include in Atom and JSON feeds.
|
||||
FeedTitle string // The title of the Atom XML feed
|
||||
|
||||
PlayEnabled bool
|
||||
}
|
||||
|
||||
// Doc represents an article adorned with presentation data.
|
||||
type Doc struct {
|
||||
*present.Doc
|
||||
Permalink string // Canonical URL for this document.
|
||||
Path string // Path relative to server root (including base).
|
||||
HTML template.HTML // rendered article
|
||||
|
||||
Related []*Doc
|
||||
Newer, Older *Doc
|
||||
}
|
||||
|
||||
// Server implements an http.Handler that serves blog articles.
|
||||
type Server struct {
|
||||
cfg Config
|
||||
docs []*Doc
|
||||
tags []string
|
||||
docPaths map[string]*Doc // key is path without BasePath.
|
||||
docTags map[string][]*Doc
|
||||
template struct {
|
||||
home, index, article, doc *template.Template
|
||||
}
|
||||
atomFeed []byte // pre-rendered Atom feed
|
||||
jsonFeed []byte // pre-rendered JSON feed
|
||||
content http.Handler
|
||||
}
|
||||
|
||||
// NewServer constructs a new Server using the specified config.
|
||||
func NewServer(cfg Config) (*Server, error) {
|
||||
present.PlayEnabled = cfg.PlayEnabled
|
||||
|
||||
if notExist(cfg.TemplatePath) {
|
||||
return nil, fmt.Errorf("template directory not found: %s", cfg.TemplatePath)
|
||||
}
|
||||
root := filepath.Join(cfg.TemplatePath, "root.tmpl")
|
||||
parse := func(name string) (*template.Template, error) {
|
||||
path := filepath.Join(cfg.TemplatePath, name)
|
||||
if notExist(path) {
|
||||
return nil, fmt.Errorf("template %s was not found in %s", name, cfg.TemplatePath)
|
||||
}
|
||||
t := template.New("").Funcs(funcMap)
|
||||
return t.ParseFiles(root, path)
|
||||
}
|
||||
|
||||
s := &Server{cfg: cfg}
|
||||
|
||||
// Parse templates.
|
||||
var err error
|
||||
s.template.home, err = parse("home.tmpl")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.template.index, err = parse("index.tmpl")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.template.article, err = parse("article.tmpl")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p := present.Template().Funcs(funcMap)
|
||||
s.template.doc, err = p.ParseFiles(filepath.Join(cfg.TemplatePath, "doc.tmpl"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Load content.
|
||||
err = s.loadDocs(filepath.Clean(cfg.ContentPath))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = s.renderAtomFeed()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = s.renderJSONFeed()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Set up content file server.
|
||||
s.content = http.StripPrefix(s.cfg.BasePath, http.FileServer(http.Dir(cfg.ContentPath)))
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
var funcMap = template.FuncMap{
|
||||
"sectioned": sectioned,
|
||||
"authors": authors,
|
||||
}
|
||||
|
||||
// sectioned returns true if the provided Doc contains more than one section.
|
||||
// This is used to control whether to display the table of contents and headings.
|
||||
func sectioned(d *present.Doc) bool {
|
||||
return len(d.Sections) > 1
|
||||
}
|
||||
|
||||
// authors returns a comma-separated list of author names.
|
||||
func authors(authors []present.Author) string {
|
||||
var b bytes.Buffer
|
||||
last := len(authors) - 1
|
||||
for i, a := range authors {
|
||||
if i > 0 {
|
||||
if i == last {
|
||||
b.WriteString(" and ")
|
||||
} else {
|
||||
b.WriteString(", ")
|
||||
}
|
||||
}
|
||||
b.WriteString(authorName(a))
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// authorName returns the first line of the Author text: the author's name.
|
||||
func authorName(a present.Author) string {
|
||||
el := a.TextElem()
|
||||
if len(el) == 0 {
|
||||
return ""
|
||||
}
|
||||
text, ok := el[0].(present.Text)
|
||||
if !ok || len(text.Lines) == 0 {
|
||||
return ""
|
||||
}
|
||||
return text.Lines[0]
|
||||
}
|
||||
|
||||
// loadDocs reads all content from the provided file system root, renders all
|
||||
// the articles it finds, adds them to the Server's docs field, computes the
|
||||
// denormalized docPaths, docTags, and tags fields, and populates the various
|
||||
// helper fields (Next, Previous, Related) for each Doc.
|
||||
func (s *Server) loadDocs(root string) error {
|
||||
// Read content into docs field.
|
||||
const ext = ".article"
|
||||
fn := func(p string, info os.FileInfo, err error) error {
|
||||
if filepath.Ext(p) != ext {
|
||||
return nil
|
||||
}
|
||||
f, err := os.Open(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
d, err := present.Parse(f, p, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
html := new(bytes.Buffer)
|
||||
err = d.Render(html, s.template.doc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p = p[len(root) : len(p)-len(ext)] // trim root and extension
|
||||
p = filepath.ToSlash(p)
|
||||
s.docs = append(s.docs, &Doc{
|
||||
Doc: d,
|
||||
Path: s.cfg.BasePath + p,
|
||||
Permalink: s.cfg.BaseURL + p,
|
||||
HTML: template.HTML(html.String()),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
err := filepath.Walk(root, fn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sort.Sort(docsByTime(s.docs))
|
||||
|
||||
// Pull out doc paths and tags and put in reverse-associating maps.
|
||||
s.docPaths = make(map[string]*Doc)
|
||||
s.docTags = make(map[string][]*Doc)
|
||||
for _, d := range s.docs {
|
||||
s.docPaths[strings.TrimPrefix(d.Path, s.cfg.BasePath)] = d
|
||||
for _, t := range d.Tags {
|
||||
s.docTags[t] = append(s.docTags[t], d)
|
||||
}
|
||||
}
|
||||
|
||||
// Pull out unique sorted list of tags.
|
||||
for t := range s.docTags {
|
||||
s.tags = append(s.tags, t)
|
||||
}
|
||||
sort.Strings(s.tags)
|
||||
|
||||
// Set up presentation-related fields, Newer, Older, and Related.
|
||||
for _, doc := range s.docs {
|
||||
// Newer, Older: docs adjacent to doc
|
||||
for i := range s.docs {
|
||||
if s.docs[i] != doc {
|
||||
continue
|
||||
}
|
||||
if i > 0 {
|
||||
doc.Newer = s.docs[i-1]
|
||||
}
|
||||
if i+1 < len(s.docs) {
|
||||
doc.Older = s.docs[i+1]
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// Related: all docs that share tags with doc.
|
||||
related := make(map[*Doc]bool)
|
||||
for _, t := range doc.Tags {
|
||||
for _, d := range s.docTags[t] {
|
||||
if d != doc {
|
||||
related[d] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
for d := range related {
|
||||
doc.Related = append(doc.Related, d)
|
||||
}
|
||||
sort.Sort(docsByTime(doc.Related))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// renderAtomFeed generates an XML Atom feed and stores it in the Server's
|
||||
// atomFeed field.
|
||||
func (s *Server) renderAtomFeed() error {
|
||||
var updated time.Time
|
||||
if len(s.docs) > 0 {
|
||||
updated = s.docs[0].Time
|
||||
}
|
||||
feed := atom.Feed{
|
||||
Title: s.cfg.FeedTitle,
|
||||
ID: "tag:" + s.cfg.Hostname + ",2013:" + s.cfg.Hostname,
|
||||
Updated: atom.Time(updated),
|
||||
Link: []atom.Link{{
|
||||
Rel: "self",
|
||||
Href: s.cfg.BaseURL + "/feed.atom",
|
||||
}},
|
||||
}
|
||||
for i, doc := range s.docs {
|
||||
if i >= s.cfg.FeedArticles {
|
||||
break
|
||||
}
|
||||
e := &atom.Entry{
|
||||
Title: doc.Title,
|
||||
ID: feed.ID + doc.Path,
|
||||
Link: []atom.Link{{
|
||||
Rel: "alternate",
|
||||
Href: doc.Permalink,
|
||||
}},
|
||||
Published: atom.Time(doc.Time),
|
||||
Updated: atom.Time(doc.Time),
|
||||
Summary: &atom.Text{
|
||||
Type: "html",
|
||||
Body: summary(doc),
|
||||
},
|
||||
Content: &atom.Text{
|
||||
Type: "html",
|
||||
Body: string(doc.HTML),
|
||||
},
|
||||
Author: &atom.Person{
|
||||
Name: authors(doc.Authors),
|
||||
},
|
||||
}
|
||||
feed.Entry = append(feed.Entry, e)
|
||||
}
|
||||
data, err := xml.Marshal(&feed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.atomFeed = data
|
||||
return nil
|
||||
}
|
||||
|
||||
type jsonItem struct {
|
||||
Title string
|
||||
Link string
|
||||
Time time.Time
|
||||
Summary string
|
||||
Content string
|
||||
Author string
|
||||
}
|
||||
|
||||
// renderJSONFeed generates a JSON feed and stores it in the Server's jsonFeed
|
||||
// field.
|
||||
func (s *Server) renderJSONFeed() error {
|
||||
var feed []jsonItem
|
||||
for i, doc := range s.docs {
|
||||
if i >= s.cfg.FeedArticles {
|
||||
break
|
||||
}
|
||||
item := jsonItem{
|
||||
Title: doc.Title,
|
||||
Link: doc.Permalink,
|
||||
Time: doc.Time,
|
||||
Summary: summary(doc),
|
||||
Content: string(doc.HTML),
|
||||
Author: authors(doc.Authors),
|
||||
}
|
||||
feed = append(feed, item)
|
||||
}
|
||||
data, err := json.Marshal(feed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.jsonFeed = data
|
||||
return nil
|
||||
}
|
||||
|
||||
// summary returns the first paragraph of text from the provided Doc.
|
||||
func summary(d *Doc) string {
|
||||
if len(d.Sections) == 0 {
|
||||
return ""
|
||||
}
|
||||
for _, elem := range d.Sections[0].Elem {
|
||||
text, ok := elem.(present.Text)
|
||||
if !ok || text.Pre {
|
||||
// skip everything but non-text elements
|
||||
continue
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
for _, s := range text.Lines {
|
||||
buf.WriteString(string(present.Style(s)))
|
||||
buf.WriteByte('\n')
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// rootData encapsulates data destined for the root template.
|
||||
type rootData struct {
|
||||
Doc *Doc
|
||||
BasePath string
|
||||
GodocURL string
|
||||
Data interface{}
|
||||
}
|
||||
|
||||
// ServeHTTP serves the front, index, and article pages
|
||||
// as well as the ATOM and JSON feeds.
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
d = rootData{BasePath: s.cfg.BasePath, GodocURL: s.cfg.GodocURL}
|
||||
t *template.Template
|
||||
)
|
||||
switch p := strings.TrimPrefix(r.URL.Path, s.cfg.BasePath); p {
|
||||
case "/":
|
||||
d.Data = s.docs
|
||||
if len(s.docs) > s.cfg.HomeArticles {
|
||||
d.Data = s.docs[:s.cfg.HomeArticles]
|
||||
}
|
||||
t = s.template.home
|
||||
case "/index":
|
||||
d.Data = s.docs
|
||||
t = s.template.index
|
||||
case "/feed.atom", "/feeds/posts/default":
|
||||
w.Header().Set("Content-type", "application/atom+xml; charset=utf-8")
|
||||
w.Write(s.atomFeed)
|
||||
return
|
||||
case "/.json":
|
||||
if p := r.FormValue("jsonp"); validJSONPFunc.MatchString(p) {
|
||||
w.Header().Set("Content-type", "application/javascript; charset=utf-8")
|
||||
fmt.Fprintf(w, "%v(%s)", p, s.jsonFeed)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-type", "application/json; charset=utf-8")
|
||||
w.Write(s.jsonFeed)
|
||||
return
|
||||
default:
|
||||
doc, ok := s.docPaths[p]
|
||||
if !ok {
|
||||
// Not a doc; try to just serve static content.
|
||||
s.content.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
d.Doc = doc
|
||||
t = s.template.article
|
||||
}
|
||||
err := t.ExecuteTemplate(w, "root", d)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
// docsByTime implements sort.Interface, sorting Docs by their Time field.
|
||||
type docsByTime []*Doc
|
||||
|
||||
func (s docsByTime) Len() int { return len(s) }
|
||||
func (s docsByTime) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||
func (s docsByTime) Less(i, j int) bool { return s[i].Time.After(s[j].Time) }
|
||||
|
||||
// notExist reports whether the path exists or not.
|
||||
func notExist(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return os.IsNotExist(err)
|
||||
}
|
|
@ -0,0 +1,184 @@
|
|||
// 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() {
|
||||
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,59 @@
|
|||
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)
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
||||
/*
|
||||
|
||||
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 @@
|
|||
testdata/out.got
|
|
@ -0,0 +1,468 @@
|
|||
// 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] <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 -package option is given, bundle uses that name.
|
||||
// Otherwise, if the -dst option is given, bundle uses the last
|
||||
// element of the destination import path.
|
||||
// Otherwise, by default bundle uses the package name found in the
|
||||
// package sources in the current directory.
|
||||
//
|
||||
// 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 rewriting the import "golang.org/x/net/http2/hpack"
|
||||
// to "internal/golang.org/x/net/http2/hpack":
|
||||
//
|
||||
// cd $GOROOT/src/net/http
|
||||
// bundle -o h2_bundle.go \
|
||||
// -prefix http2 \
|
||||
// -import golang.org/x/net/http2/hpack=internal/golang.org/x/net/http2/hpack \
|
||||
// golang.org/x/net/http2
|
||||
//
|
||||
// Two ways to update the http2 bundle:
|
||||
//
|
||||
// go generate net/http
|
||||
//
|
||||
// cd $GOROOT/src/net/http
|
||||
// go generate
|
||||
//
|
||||
// Update both bundles, restricting ``go generate'' to running bundle commands:
|
||||
//
|
||||
// go generate -run bundle cmd/dist net/http
|
||||
//
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/build"
|
||||
"go/format"
|
||||
"go/parser"
|
||||
"go/printer"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/tools/go/loader"
|
||||
)
|
||||
|
||||
var (
|
||||
outputFile = flag.String("o", "", "write output to `file` (default standard output)")
|
||||
dstPath = flag.String("dst", "", "set destination import `path` (default taken from current directory)")
|
||||
pkgName = flag.String("pkg", "", "set destination package `name` (default taken from current directory)")
|
||||
prefix = flag.String("prefix", "", "set bundled identifier prefix to `p` (default source package name + \"_\")")
|
||||
underscore = flag.Bool("underscore", false, "rewrite golang.org to golang_org in imports; temporary workaround for golang.org/issue/16333")
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
if *dstPath != "" {
|
||||
if *pkgName == "" {
|
||||
*pkgName = path.Base(*dstPath)
|
||||
}
|
||||
} else {
|
||||
wd, _ := os.Getwd()
|
||||
pkg, err := build.ImportDir(wd, 0)
|
||||
if err != nil {
|
||||
log.Fatalf("cannot find package in current directory: %v", err)
|
||||
}
|
||||
*dstPath = pkg.ImportPath
|
||||
if *pkgName == "" {
|
||||
*pkgName = pkg.Name
|
||||
}
|
||||
}
|
||||
|
||||
code, err := bundle(args[0], *dstPath, *pkgName, *prefix)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if *outputFile != "" {
|
||||
err := ioutil.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 ctxt = &build.Default
|
||||
|
||||
func bundle(src, dst, dstpkg, prefix string) ([]byte, error) {
|
||||
// Load the initial package.
|
||||
conf := loader.Config{ParserMode: parser.ParseComments, Build: ctxt}
|
||||
conf.TypeCheckFuncBodies = func(p string) bool { return p == src }
|
||||
conf.Import(src)
|
||||
|
||||
lprog, err := conf.Load()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Because there was a single Import call and Load succeeded,
|
||||
// InitialPackages is guaranteed to hold the sole requested package.
|
||||
info := lprog.InitialPackages()[0]
|
||||
if prefix == "" {
|
||||
pkgName := info.Files[0].Name.Name
|
||||
prefix = pkgName + "_"
|
||||
}
|
||||
|
||||
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 info.Uses {
|
||||
if obj == from {
|
||||
if field := info.Defs[id]; field != nil {
|
||||
rename(field)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rename each package-level object.
|
||||
scope := info.Pkg.Scope()
|
||||
for _, name := range scope.Names() {
|
||||
rename(scope.Lookup(name))
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
|
||||
fmt.Fprintf(&out, "// Code generated by golang.org/x/tools/cmd/bundle. DO NOT EDIT.\n")
|
||||
if *outputFile != "" {
|
||||
fmt.Fprintf(&out, "//go:generate bundle %s\n", strings.Join(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 info.Files {
|
||||
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 info.Files {
|
||||
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 conf.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 {
|
||||
if *underscore {
|
||||
spec = strings.Replace(spec, "golang.org/", "golang_org/", 1)
|
||||
}
|
||||
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 info.Files {
|
||||
// Update renamed identifiers.
|
||||
for id, obj := range info.Defs {
|
||||
if objsToUpdate[obj] {
|
||||
id.Name = prefix + obj.Name()
|
||||
}
|
||||
}
|
||||
for id, obj := range info.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 := info.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, lprog.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, lprog.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
|
||||
}
|
||||
|
||||
type flagFunc func(string)
|
||||
|
||||
func (f flagFunc) Set(s string) error {
|
||||
f(s)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f flagFunc) String() string { return "" }
|
|
@ -0,0 +1,74 @@
|
|||
// 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.
|
||||
|
||||
// +build go1.9
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/go/buildutil"
|
||||
)
|
||||
|
||||
func TestBundle(t *testing.T) {
|
||||
load := func(name string) string {
|
||||
data, err := ioutil.ReadFile(name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
ctxt = buildutil.FakeContext(map[string]map[string]string{
|
||||
"initial": {
|
||||
"a.go": load("testdata/src/initial/a.go"),
|
||||
"b.go": load("testdata/src/initial/b.go"),
|
||||
"c.go": load("testdata/src/initial/c.go"),
|
||||
},
|
||||
"domain.name/importdecl": {
|
||||
"p.go": load("testdata/src/domain.name/importdecl/p.go"),
|
||||
},
|
||||
"fmt": {
|
||||
"print.go": `package fmt; func Println(...interface{})`,
|
||||
},
|
||||
})
|
||||
|
||||
os.Args = os.Args[:1] // avoid e.g. -test=short in the output
|
||||
out, err := bundle("initial", "github.com/dest", "dest", "prefix")
|
||||
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 := ioutil.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,62 @@
|
|||
// 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
vendor/golang.org/x/tools/cmd/bundle/testdata/src/domain.name/importdecl/p.go
generated
vendored
Normal file
3
vendor/golang.org/x/tools/cmd/bundle/testdata/src/domain.name/importdecl/p.go
generated
vendored
Normal file
|
@ -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,361 @@
|
|||
// 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 (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/build"
|
||||
"go/token"
|
||||
"io"
|
||||
"log"
|
||||
"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/loader"
|
||||
"golang.org/x/tools/go/pointer"
|
||||
"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, pta)`)
|
||||
|
||||
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")
|
||||
|
||||
ptalogFlag = flag.String("ptalog", "",
|
||||
"Location of the points-to analysis log file, or empty to disable logging.")
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.Var((*buildutil.TagsFlag)(&build.Default.BuildTags), "tags", buildutil.TagsFlagDoc)
|
||||
}
|
||||
|
||||
const Usage = `callgraph: display the the call graph of a Go program.
|
||||
|
||||
Usage:
|
||||
|
||||
callgraph [-algo=static|cha|rta|pta] [-test] [-format=...] <args>...
|
||||
|
||||
Flags:
|
||||
|
||||
-algo Specifies the call-graph construction algorithm, one of:
|
||||
|
||||
static static calls only (unsound)
|
||||
cha Class Hierarchy Analysis
|
||||
rta Rapid Type Analysis
|
||||
pta inclusion-based Points-To Analysis
|
||||
|
||||
The algorithms are ordered by increasing precision in their
|
||||
treatment of dynamic calls (and thus also computational cost).
|
||||
RTA and PTA require 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, e.g. Caller.Pkg.Pkg.Path yields the
|
||||
import path of the enclosing package. Consult the go/ssa
|
||||
API documentation for details.
|
||||
|
||||
` + loader.FromArgsUsage + `
|
||||
|
||||
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 pointer analysis algorithm:
|
||||
|
||||
callgraph -format='{{.Caller}} -{{.Dynamic}}-> {{.Callee}}' -test -algo=pta 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(&build.Default, *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(ctxt *build.Context, algo, format string, tests bool, args []string) error {
|
||||
conf := loader.Config{Build: ctxt}
|
||||
|
||||
if len(args) == 0 {
|
||||
fmt.Fprintln(os.Stderr, Usage)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Use the initial packages from the command line.
|
||||
_, err := conf.FromArgs(args, tests)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Load, parse and type-check the whole program.
|
||||
iprog, err := conf.Load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create and build SSA-form program representation.
|
||||
prog := ssautil.CreateProgram(iprog, 0)
|
||||
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":
|
||||
// Set up points-to analysis log file.
|
||||
var ptalog io.Writer
|
||||
if *ptalogFlag != "" {
|
||||
if f, err := os.Create(*ptalogFlag); err != nil {
|
||||
log.Fatalf("Failed to create PTA log file: %s", err)
|
||||
} else {
|
||||
buf := bufio.NewWriter(f)
|
||||
ptalog = buf
|
||||
defer func() {
|
||||
if err := buf.Flush(); err != nil {
|
||||
log.Printf("flush: %s", err)
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
log.Printf("close: %s", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
mains, err := mainPackages(prog, tests)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config := &pointer.Config{
|
||||
Mains: mains,
|
||||
BuildCallGraph: true,
|
||||
Log: ptalog,
|
||||
}
|
||||
ptares, err := pointer.Analyze(config)
|
||||
if err != nil {
|
||||
return err // internal error in pointer analysis
|
||||
}
|
||||
cg = ptares.CallGraph
|
||||
|
||||
case "rta":
|
||||
mains, err := mainPackages(prog, tests)
|
||||
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.
|
||||
|
||||
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}}`
|
||||
}
|
||||
|
||||
tmpl, err := template.New("-format").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(prog *ssa.Program, tests bool) ([]*ssa.Package, error) {
|
||||
pkgs := prog.AllPackages() // TODO(adonovan): use only initial packages
|
||||
|
||||
// If tests, create a "testmain" package for each test.
|
||||
var mains []*ssa.Package
|
||||
if tests {
|
||||
for _, pkg := range pkgs {
|
||||
if main := prog.CreateTestMainPackage(pkg); main != nil {
|
||||
mains = append(mains, main)
|
||||
}
|
||||
}
|
||||
if mains == nil {
|
||||
return nil, fmt.Errorf("no tests")
|
||||
}
|
||||
return mains, nil
|
||||
}
|
||||
|
||||
// Otherwise, use the main packages.
|
||||
mains = append(mains, ssautil.MainPackages(pkgs)...)
|
||||
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,81 @@
|
|||
// 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.
|
||||
|
||||
// +build !android
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/build"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCallgraph(t *testing.T) {
|
||||
ctxt := build.Default // copy
|
||||
ctxt.GOPATH = "testdata"
|
||||
|
||||
const format = "{{.Caller}} --> {{.Callee}}"
|
||||
|
||||
for _, test := range []struct {
|
||||
algo, format string
|
||||
tests bool
|
||||
want []string
|
||||
}{
|
||||
{"rta", format, 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`,
|
||||
}},
|
||||
{"pta", format, false, []string{
|
||||
// pta distinguishes main->C, main2->D. Also has a root node.
|
||||
`<root> --> pkg.init`,
|
||||
`<root> --> pkg.main`,
|
||||
`pkg.main --> (pkg.C).f`,
|
||||
`pkg.main --> pkg.main2`,
|
||||
`pkg.main2 --> (pkg.D).f`,
|
||||
}},
|
||||
// tests: main is not called.
|
||||
{"rta", format, true, []string{
|
||||
`pkg$testmain.init --> pkg.init`,
|
||||
`pkg.Example --> (pkg.C).f`,
|
||||
}},
|
||||
{"pta", format, true, []string{
|
||||
`<root> --> pkg$testmain.init`,
|
||||
`<root> --> pkg.Example`,
|
||||
`pkg$testmain.init --> pkg.init`,
|
||||
`pkg.Example --> (pkg.C).f`,
|
||||
}},
|
||||
} {
|
||||
stdout = new(bytes.Buffer)
|
||||
if err := doCallgraph(&ctxt, test.algo, test.format, test.tests, []string{"pkg"}); err != nil {
|
||||
t.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
got := sortedLines(fmt.Sprint(stdout))
|
||||
if !reflect.DeepEqual(got, test.want) {
|
||||
t.Errorf("callgraph(%q, %q, %t):\ngot:\n%s\nwant:\n%s",
|
||||
test.algo, test.format, test.tests,
|
||||
strings.Join(got, "\n"),
|
||||
strings.Join(test.want, "\n"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sortedLines(s string) []string {
|
||||
s = strings.TrimSpace(s)
|
||||
lines := strings.Split(s, "\n")
|
||||
sort.Strings(lines)
|
||||
return lines
|
||||
}
|
|
@ -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
|
||||
}
|
7
vendor/golang.org/x/tools/cmd/callgraph/testdata/src/pkg/pkg_test.go
generated
vendored
Normal file
7
vendor/golang.org/x/tools/cmd/callgraph/testdata/src/pkg/pkg_test.go
generated
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
package main
|
||||
|
||||
// Don't import "testing", it adds a lot of callgraph edges.
|
||||
|
||||
func Example() {
|
||||
C(0).f()
|
||||
}
|
|
@ -0,0 +1,360 @@
|
|||
// 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.
|
||||
//
|
||||
// -count n
|
||||
// Run each benchmark n times (default 1).
|
||||
//
|
||||
// -cpuprofile file
|
||||
// Write a CPU profile of the compiler to file.
|
||||
//
|
||||
// -memprofile file
|
||||
// Write a memory profile of the compiler to file.
|
||||
//
|
||||
// -memprofilerate rate
|
||||
// Set runtime.MemProfileRate during compilation.
|
||||
//
|
||||
// -obj
|
||||
// Report object file statistics.
|
||||
//
|
||||
// -pkg
|
||||
// Benchmark compiling a single package.
|
||||
//
|
||||
// -run regexp
|
||||
// Only run benchmarks with names matching regexp.
|
||||
//
|
||||
// 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"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/build"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
goroot string
|
||||
compiler string
|
||||
runRE *regexp.Regexp
|
||||
is6g 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")
|
||||
flagCompilerFlags = flag.String("compileflags", "", "additional `flags` to pass to compile")
|
||||
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")
|
||||
)
|
||||
|
||||
var tests = []struct {
|
||||
name string
|
||||
dir string
|
||||
long bool
|
||||
}{
|
||||
{"BenchmarkTemplate", "html/template", false},
|
||||
{"BenchmarkUnicode", "unicode", false},
|
||||
{"BenchmarkGoTypes", "go/types", false},
|
||||
{"BenchmarkCompiler", "cmd/compile/internal/gc", false},
|
||||
{"BenchmarkSSA", "cmd/compile/internal/ssa", false},
|
||||
{"BenchmarkFlate", "compress/flate", false},
|
||||
{"BenchmarkGoParser", "go/parser", false},
|
||||
{"BenchmarkReflect", "reflect", false},
|
||||
{"BenchmarkTar", "archive/tar", false},
|
||||
{"BenchmarkXML", "encoding/xml", false},
|
||||
{"BenchmarkStdCmd", "", true},
|
||||
{"BenchmarkHelloSize", "", false},
|
||||
{"BenchmarkCmdGoSize", "", 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))
|
||||
|
||||
compiler = *flagCompiler
|
||||
if compiler == "" {
|
||||
out, err := exec.Command(*flagGoCmd, "tool", "-n", "compile").CombinedOutput()
|
||||
if err != nil {
|
||||
out, err = exec.Command(*flagGoCmd, "tool", "-n", "6g").CombinedOutput()
|
||||
is6g = true
|
||||
if err != nil {
|
||||
out, err = exec.Command(*flagGoCmd, "tool", "-n", "compile").CombinedOutput()
|
||||
log.Fatalf("go tool -n compiler: %v\n%s", err, out)
|
||||
}
|
||||
}
|
||||
compiler = strings.TrimSpace(string(out))
|
||||
}
|
||||
|
||||
if *flagRun != "" {
|
||||
r, err := regexp.Compile(*flagRun)
|
||||
if err != nil {
|
||||
log.Fatalf("invalid -run argument: %v", err)
|
||||
}
|
||||
runRE = r
|
||||
}
|
||||
|
||||
for i := 0; i < *flagCount; i++ {
|
||||
if *flagPackage != "" {
|
||||
runBuild("BenchmarkPkg", *flagPackage, i)
|
||||
continue
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if tt.long && *flagShort {
|
||||
continue
|
||||
}
|
||||
if runRE == nil || runRE.MatchString(tt.name) {
|
||||
runBuild(tt.name, tt.dir, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func runCmd(name string, cmd *exec.Cmd) {
|
||||
start := time.Now()
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
log.Printf("%v: %v\n%s", name, err, out)
|
||||
return
|
||||
}
|
||||
fmt.Printf("%s 1 %d ns/op\n", name, time.Since(start).Nanoseconds())
|
||||
}
|
||||
|
||||
func runStdCmd() {
|
||||
args := []string{"build", "-a"}
|
||||
if *flagCompilerFlags != "" {
|
||||
args = append(args, "-gcflags", *flagCompilerFlags)
|
||||
}
|
||||
args = append(args, "std", "cmd")
|
||||
cmd := exec.Command(*flagGoCmd, args...)
|
||||
cmd.Dir = filepath.Join(goroot, "src")
|
||||
runCmd("BenchmarkStdCmd", cmd)
|
||||
}
|
||||
|
||||
// path is either a path to a file ("$GOROOT/test/helloworld.go") or a package path ("cmd/go").
|
||||
func runSize(name, path string) {
|
||||
cmd := exec.Command(*flagGoCmd, "build", "-o", "_compilebenchout_", path)
|
||||
cmd.Stdout = os.Stderr
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
defer os.Remove("_compilebenchout_")
|
||||
info, err := os.Stat("_compilebenchout_")
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
out, err := exec.Command("size", "_compilebenchout_").CombinedOutput()
|
||||
if err != nil {
|
||||
log.Printf("size: %v\n%s", err, out)
|
||||
return
|
||||
}
|
||||
lines := strings.Split(string(out), "\n")
|
||||
if len(lines) < 2 {
|
||||
log.Printf("not enough output from size: %s", out)
|
||||
return
|
||||
}
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
func runBuild(name, dir string, count int) {
|
||||
switch name {
|
||||
case "BenchmarkStdCmd":
|
||||
runStdCmd()
|
||||
return
|
||||
case "BenchmarkCmdGoSize":
|
||||
runSize("BenchmarkCmdGoSize", "cmd/go")
|
||||
return
|
||||
case "BenchmarkHelloSize":
|
||||
runSize("BenchmarkHelloSize", filepath.Join(goroot, "test/helloworld.go"))
|
||||
return
|
||||
}
|
||||
|
||||
pkg, err := build.Import(dir, ".", 0)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
args := []string{"-o", "_compilebench_.o"}
|
||||
if is6g {
|
||||
*flagMemprofilerate = -1
|
||||
*flagAlloc = false
|
||||
*flagCpuprofile = ""
|
||||
*flagMemprofile = ""
|
||||
}
|
||||
if *flagMemprofilerate >= 0 {
|
||||
args = append(args, "-memprofilerate", fmt.Sprint(*flagMemprofilerate))
|
||||
}
|
||||
args = append(args, strings.Fields(*flagCompilerFlags)...)
|
||||
if *flagAlloc || *flagCpuprofile != "" || *flagMemprofile != "" {
|
||||
if *flagAlloc || *flagMemprofile != "" {
|
||||
args = append(args, "-memprofile", "_compilebench_.memprof")
|
||||
}
|
||||
if *flagCpuprofile != "" {
|
||||
args = append(args, "-cpuprofile", "_compilebench_.cpuprof")
|
||||
}
|
||||
}
|
||||
args = append(args, pkg.GoFiles...)
|
||||
cmd := exec.Command(compiler, args...)
|
||||
cmd.Dir = pkg.Dir
|
||||
cmd.Stdout = os.Stderr
|
||||
cmd.Stderr = os.Stderr
|
||||
start := time.Now()
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
log.Printf("%v: %v", name, err)
|
||||
return
|
||||
}
|
||||
end := time.Now()
|
||||
|
||||
var allocs, allocbytes int64
|
||||
if *flagAlloc || *flagMemprofile != "" {
|
||||
out, err := ioutil.ReadFile(pkg.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
|
||||
}
|
||||
switch f[1] {
|
||||
case "TotalAlloc":
|
||||
allocbytes = val
|
||||
case "Mallocs":
|
||||
allocs = val
|
||||
}
|
||||
}
|
||||
|
||||
if *flagMemprofile != "" {
|
||||
if err := ioutil.WriteFile(*flagMemprofile, out, 0666); err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
}
|
||||
os.Remove(pkg.Dir + "/_compilebench_.memprof")
|
||||
}
|
||||
|
||||
if *flagCpuprofile != "" {
|
||||
out, err := ioutil.ReadFile(pkg.Dir + "/_compilebench_.cpuprof")
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
outpath := *flagCpuprofile
|
||||
if *flagCount != 1 {
|
||||
outpath = fmt.Sprintf("%s_%d", outpath, count)
|
||||
}
|
||||
if err := ioutil.WriteFile(outpath, out, 0666); err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
os.Remove(pkg.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 *flagAlloc {
|
||||
fmt.Printf(" %d B/op %d allocs/op", allocbytes, allocs)
|
||||
}
|
||||
|
||||
opath := pkg.Dir + "/_compilebench_.o"
|
||||
if *flagObj {
|
||||
// TODO(josharian): object files are big; just read enough to find what we seek.
|
||||
data, err := ioutil.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)
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
NOTE: For Go releases 1.5 and later, this tool lives in the
|
||||
standard repository. The code here is not maintained.
|
|
@ -0,0 +1,722 @@
|
|||
// 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"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/printer"
|
||||
"go/token"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const usageMessage = "" +
|
||||
`Usage of 'go tool cover':
|
||||
Given a coverage profile produced by 'go test':
|
||||
go test -coverprofile=c.out
|
||||
|
||||
Open a web browser displaying annotated source code:
|
||||
go tool cover -html=c.out
|
||||
|
||||
Write out an HTML file instead of launching a web browser:
|
||||
go tool cover -html=c.out -o coverage.html
|
||||
|
||||
Display coverage percentages to stdout for each function:
|
||||
go tool cover -func=c.out
|
||||
|
||||
Finally, to generate modified source code with coverage annotations
|
||||
(what go test -cover does):
|
||||
go tool cover -mode=set -var=CoverageVariableName program.go
|
||||
`
|
||||
|
||||
func usage() {
|
||||
fmt.Fprintln(os.Stderr, usageMessage)
|
||||
fmt.Fprintln(os.Stderr, "Flags:")
|
||||
flag.PrintDefaults()
|
||||
fmt.Fprintln(os.Stderr, "\n Only one of -html, -func, or -mode may be set.")
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
var (
|
||||
mode = flag.String("mode", "", "coverage mode: set, count, atomic")
|
||||
varVar = flag.String("var", "GoCover", "name of coverage variable to generate")
|
||||
output = flag.String("o", "", "file for output; default: stdout")
|
||||
htmlOut = flag.String("html", "", "generate HTML representation of coverage profile")
|
||||
funcOut = flag.String("func", "", "output coverage profile information for each function")
|
||||
)
|
||||
|
||||
var profile string // The profile to read; the value of -html or -func
|
||||
|
||||
var counterStmt func(*File, ast.Expr) ast.Stmt
|
||||
|
||||
const (
|
||||
atomicPackagePath = "sync/atomic"
|
||||
atomicPackageName = "_cover_atomic_"
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
|
||||
// Usage information when no arguments.
|
||||
if flag.NFlag() == 0 && flag.NArg() == 0 {
|
||||
flag.Usage()
|
||||
}
|
||||
|
||||
err := parseFlags()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
fmt.Fprintln(os.Stderr, `For usage information, run "go tool cover -help"`)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
// Generate coverage-annotated source.
|
||||
if *mode != "" {
|
||||
annotate(flag.Arg(0))
|
||||
return
|
||||
}
|
||||
|
||||
// Output HTML or function coverage information.
|
||||
if *htmlOut != "" {
|
||||
err = htmlOutput(profile, *output)
|
||||
} else {
|
||||
err = funcOutput(profile, *output)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "cover: %v\n", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
}
|
||||
|
||||
// parseFlags sets the profile and counterStmt globals and performs validations.
|
||||
func parseFlags() error {
|
||||
profile = *htmlOut
|
||||
if *funcOut != "" {
|
||||
if profile != "" {
|
||||
return fmt.Errorf("too many options")
|
||||
}
|
||||
profile = *funcOut
|
||||
}
|
||||
|
||||
// Must either display a profile or rewrite Go source.
|
||||
if (profile == "") == (*mode == "") {
|
||||
return fmt.Errorf("too many options")
|
||||
}
|
||||
|
||||
if *mode != "" {
|
||||
switch *mode {
|
||||
case "set":
|
||||
counterStmt = setCounterStmt
|
||||
case "count":
|
||||
counterStmt = incCounterStmt
|
||||
case "atomic":
|
||||
counterStmt = atomicCounterStmt
|
||||
default:
|
||||
return fmt.Errorf("unknown -mode %v", *mode)
|
||||
}
|
||||
|
||||
if flag.NArg() == 0 {
|
||||
return fmt.Errorf("missing source file")
|
||||
} else if flag.NArg() == 1 {
|
||||
return nil
|
||||
}
|
||||
} else if flag.NArg() == 0 {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("too many arguments")
|
||||
}
|
||||
|
||||
// Block represents the information about a basic block to be recorded in the analysis.
|
||||
// Note: Our definition of basic block is based on control structures; we don't break
|
||||
// apart && and ||. We could but it doesn't seem important enough to bother.
|
||||
type Block struct {
|
||||
startByte token.Pos
|
||||
endByte token.Pos
|
||||
numStmt int
|
||||
}
|
||||
|
||||
// File is a wrapper for the state of a file used in the parser.
|
||||
// The basic parse tree walker is a method of this type.
|
||||
type File struct {
|
||||
fset *token.FileSet
|
||||
name string // Name of file.
|
||||
astFile *ast.File
|
||||
blocks []Block
|
||||
atomicPkg string // Package name for "sync/atomic" in this file.
|
||||
}
|
||||
|
||||
// Visit implements the ast.Visitor interface.
|
||||
func (f *File) Visit(node ast.Node) ast.Visitor {
|
||||
switch n := node.(type) {
|
||||
case *ast.BlockStmt:
|
||||
// If it's a switch or select, the body is a list of case clauses; don't tag the block itself.
|
||||
if len(n.List) > 0 {
|
||||
switch n.List[0].(type) {
|
||||
case *ast.CaseClause: // switch
|
||||
for _, n := range n.List {
|
||||
clause := n.(*ast.CaseClause)
|
||||
clause.Body = f.addCounters(clause.Pos(), clause.End(), clause.Body, false)
|
||||
}
|
||||
return f
|
||||
case *ast.CommClause: // select
|
||||
for _, n := range n.List {
|
||||
clause := n.(*ast.CommClause)
|
||||
clause.Body = f.addCounters(clause.Pos(), clause.End(), clause.Body, false)
|
||||
}
|
||||
return f
|
||||
}
|
||||
}
|
||||
n.List = f.addCounters(n.Lbrace, n.Rbrace+1, n.List, true) // +1 to step past closing brace.
|
||||
case *ast.IfStmt:
|
||||
ast.Walk(f, n.Body)
|
||||
if n.Else == nil {
|
||||
return nil
|
||||
}
|
||||
// The elses are special, because if we have
|
||||
// if x {
|
||||
// } else if y {
|
||||
// }
|
||||
// we want to cover the "if y". To do this, we need a place to drop the counter,
|
||||
// so we add a hidden block:
|
||||
// if x {
|
||||
// } else {
|
||||
// if y {
|
||||
// }
|
||||
// }
|
||||
switch stmt := n.Else.(type) {
|
||||
case *ast.IfStmt:
|
||||
block := &ast.BlockStmt{
|
||||
Lbrace: n.Body.End(), // Start at end of the "if" block so the covered part looks like it starts at the "else".
|
||||
List: []ast.Stmt{stmt},
|
||||
Rbrace: stmt.End(),
|
||||
}
|
||||
n.Else = block
|
||||
case *ast.BlockStmt:
|
||||
stmt.Lbrace = n.Body.End() // Start at end of the "if" block so the covered part looks like it starts at the "else".
|
||||
default:
|
||||
panic("unexpected node type in if")
|
||||
}
|
||||
ast.Walk(f, n.Else)
|
||||
return nil
|
||||
case *ast.SelectStmt:
|
||||
// Don't annotate an empty select - creates a syntax error.
|
||||
if n.Body == nil || len(n.Body.List) == 0 {
|
||||
return nil
|
||||
}
|
||||
case *ast.SwitchStmt:
|
||||
// Don't annotate an empty switch - creates a syntax error.
|
||||
if n.Body == nil || len(n.Body.List) == 0 {
|
||||
return nil
|
||||
}
|
||||
case *ast.TypeSwitchStmt:
|
||||
// Don't annotate an empty type switch - creates a syntax error.
|
||||
if n.Body == nil || len(n.Body.List) == 0 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// unquote returns the unquoted string.
|
||||
func unquote(s string) string {
|
||||
t, err := strconv.Unquote(s)
|
||||
if err != nil {
|
||||
log.Fatalf("cover: improperly quoted string %q\n", s)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// addImport adds an import for the specified path, if one does not already exist, and returns
|
||||
// the local package name.
|
||||
func (f *File) addImport(path string) string {
|
||||
// Does the package already import it?
|
||||
for _, s := range f.astFile.Imports {
|
||||
if unquote(s.Path.Value) == path {
|
||||
if s.Name != nil {
|
||||
return s.Name.Name
|
||||
}
|
||||
return filepath.Base(path)
|
||||
}
|
||||
}
|
||||
newImport := &ast.ImportSpec{
|
||||
Name: ast.NewIdent(atomicPackageName),
|
||||
Path: &ast.BasicLit{
|
||||
Kind: token.STRING,
|
||||
Value: fmt.Sprintf("%q", path),
|
||||
},
|
||||
}
|
||||
impDecl := &ast.GenDecl{
|
||||
Tok: token.IMPORT,
|
||||
Specs: []ast.Spec{
|
||||
newImport,
|
||||
},
|
||||
}
|
||||
// Make the new import the first Decl in the file.
|
||||
astFile := f.astFile
|
||||
astFile.Decls = append(astFile.Decls, nil)
|
||||
copy(astFile.Decls[1:], astFile.Decls[0:])
|
||||
astFile.Decls[0] = impDecl
|
||||
astFile.Imports = append(astFile.Imports, newImport)
|
||||
|
||||
// Now refer to the package, just in case it ends up unused.
|
||||
// That is, append to the end of the file the declaration
|
||||
// var _ = _cover_atomic_.AddUint32
|
||||
reference := &ast.GenDecl{
|
||||
Tok: token.VAR,
|
||||
Specs: []ast.Spec{
|
||||
&ast.ValueSpec{
|
||||
Names: []*ast.Ident{
|
||||
ast.NewIdent("_"),
|
||||
},
|
||||
Values: []ast.Expr{
|
||||
&ast.SelectorExpr{
|
||||
X: ast.NewIdent(atomicPackageName),
|
||||
Sel: ast.NewIdent("AddUint32"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
astFile.Decls = append(astFile.Decls, reference)
|
||||
return atomicPackageName
|
||||
}
|
||||
|
||||
var slashslash = []byte("//")
|
||||
|
||||
// initialComments returns the prefix of content containing only
|
||||
// whitespace and line comments. Any +build directives must appear
|
||||
// within this region. This approach is more reliable than using
|
||||
// go/printer to print a modified AST containing comments.
|
||||
//
|
||||
func initialComments(content []byte) []byte {
|
||||
// Derived from go/build.Context.shouldBuild.
|
||||
end := 0
|
||||
p := content
|
||||
for len(p) > 0 {
|
||||
line := p
|
||||
if i := bytes.IndexByte(line, '\n'); i >= 0 {
|
||||
line, p = line[:i], p[i+1:]
|
||||
} else {
|
||||
p = p[len(p):]
|
||||
}
|
||||
line = bytes.TrimSpace(line)
|
||||
if len(line) == 0 { // Blank line.
|
||||
end = len(content) - len(p)
|
||||
continue
|
||||
}
|
||||
if !bytes.HasPrefix(line, slashslash) { // Not comment line.
|
||||
break
|
||||
}
|
||||
}
|
||||
return content[:end]
|
||||
}
|
||||
|
||||
func annotate(name string) {
|
||||
fset := token.NewFileSet()
|
||||
content, err := ioutil.ReadFile(name)
|
||||
if err != nil {
|
||||
log.Fatalf("cover: %s: %s", name, err)
|
||||
}
|
||||
parsedFile, err := parser.ParseFile(fset, name, content, parser.ParseComments)
|
||||
if err != nil {
|
||||
log.Fatalf("cover: %s: %s", name, err)
|
||||
}
|
||||
parsedFile.Comments = trimComments(parsedFile, fset)
|
||||
|
||||
file := &File{
|
||||
fset: fset,
|
||||
name: name,
|
||||
astFile: parsedFile,
|
||||
}
|
||||
if *mode == "atomic" {
|
||||
file.atomicPkg = file.addImport(atomicPackagePath)
|
||||
}
|
||||
ast.Walk(file, file.astFile)
|
||||
fd := os.Stdout
|
||||
if *output != "" {
|
||||
var err error
|
||||
fd, err = os.Create(*output)
|
||||
if err != nil {
|
||||
log.Fatalf("cover: %s", err)
|
||||
}
|
||||
}
|
||||
fd.Write(initialComments(content)) // Retain '// +build' directives.
|
||||
file.print(fd)
|
||||
// After printing the source tree, add some declarations for the counters etc.
|
||||
// We could do this by adding to the tree, but it's easier just to print the text.
|
||||
file.addVariables(fd)
|
||||
}
|
||||
|
||||
// trimComments drops all but the //go: comments, some of which are semantically important.
|
||||
// We drop all others because they can appear in places that cause our counters
|
||||
// to appear in syntactically incorrect places. //go: appears at the beginning of
|
||||
// the line and is syntactically safe.
|
||||
func trimComments(file *ast.File, fset *token.FileSet) []*ast.CommentGroup {
|
||||
var comments []*ast.CommentGroup
|
||||
for _, group := range file.Comments {
|
||||
var list []*ast.Comment
|
||||
for _, comment := range group.List {
|
||||
if strings.HasPrefix(comment.Text, "//go:") && fset.Position(comment.Slash).Column == 1 {
|
||||
list = append(list, comment)
|
||||
}
|
||||
}
|
||||
if list != nil {
|
||||
comments = append(comments, &ast.CommentGroup{List: list})
|
||||
}
|
||||
}
|
||||
return comments
|
||||
}
|
||||
|
||||
func (f *File) print(w io.Writer) {
|
||||
printer.Fprint(w, f.fset, f.astFile)
|
||||
}
|
||||
|
||||
// intLiteral returns an ast.BasicLit representing the integer value.
|
||||
func (f *File) intLiteral(i int) *ast.BasicLit {
|
||||
node := &ast.BasicLit{
|
||||
Kind: token.INT,
|
||||
Value: fmt.Sprint(i),
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
// index returns an ast.BasicLit representing the number of counters present.
|
||||
func (f *File) index() *ast.BasicLit {
|
||||
return f.intLiteral(len(f.blocks))
|
||||
}
|
||||
|
||||
// setCounterStmt returns the expression: __count[23] = 1.
|
||||
func setCounterStmt(f *File, counter ast.Expr) ast.Stmt {
|
||||
return &ast.AssignStmt{
|
||||
Lhs: []ast.Expr{counter},
|
||||
Tok: token.ASSIGN,
|
||||
Rhs: []ast.Expr{f.intLiteral(1)},
|
||||
}
|
||||
}
|
||||
|
||||
// incCounterStmt returns the expression: __count[23]++.
|
||||
func incCounterStmt(f *File, counter ast.Expr) ast.Stmt {
|
||||
return &ast.IncDecStmt{
|
||||
X: counter,
|
||||
Tok: token.INC,
|
||||
}
|
||||
}
|
||||
|
||||
// atomicCounterStmt returns the expression: atomic.AddUint32(&__count[23], 1)
|
||||
func atomicCounterStmt(f *File, counter ast.Expr) ast.Stmt {
|
||||
return &ast.ExprStmt{
|
||||
X: &ast.CallExpr{
|
||||
Fun: &ast.SelectorExpr{
|
||||
X: ast.NewIdent(f.atomicPkg),
|
||||
Sel: ast.NewIdent("AddUint32"),
|
||||
},
|
||||
Args: []ast.Expr{&ast.UnaryExpr{
|
||||
Op: token.AND,
|
||||
X: counter,
|
||||
},
|
||||
f.intLiteral(1),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// newCounter creates a new counter expression of the appropriate form.
|
||||
func (f *File) newCounter(start, end token.Pos, numStmt int) ast.Stmt {
|
||||
counter := &ast.IndexExpr{
|
||||
X: &ast.SelectorExpr{
|
||||
X: ast.NewIdent(*varVar),
|
||||
Sel: ast.NewIdent("Count"),
|
||||
},
|
||||
Index: f.index(),
|
||||
}
|
||||
stmt := counterStmt(f, counter)
|
||||
f.blocks = append(f.blocks, Block{start, end, numStmt})
|
||||
return stmt
|
||||
}
|
||||
|
||||
// addCounters takes a list of statements and adds counters to the beginning of
|
||||
// each basic block at the top level of that list. For instance, given
|
||||
//
|
||||
// S1
|
||||
// if cond {
|
||||
// S2
|
||||
// }
|
||||
// S3
|
||||
//
|
||||
// counters will be added before S1 and before S3. The block containing S2
|
||||
// will be visited in a separate call.
|
||||
// TODO: Nested simple blocks get unnecessary (but correct) counters
|
||||
func (f *File) addCounters(pos, blockEnd token.Pos, list []ast.Stmt, extendToClosingBrace bool) []ast.Stmt {
|
||||
// Special case: make sure we add a counter to an empty block. Can't do this below
|
||||
// or we will add a counter to an empty statement list after, say, a return statement.
|
||||
if len(list) == 0 {
|
||||
return []ast.Stmt{f.newCounter(pos, blockEnd, 0)}
|
||||
}
|
||||
// We have a block (statement list), but it may have several basic blocks due to the
|
||||
// appearance of statements that affect the flow of control.
|
||||
var newList []ast.Stmt
|
||||
for {
|
||||
// Find first statement that affects flow of control (break, continue, if, etc.).
|
||||
// It will be the last statement of this basic block.
|
||||
var last int
|
||||
end := blockEnd
|
||||
for last = 0; last < len(list); last++ {
|
||||
end = f.statementBoundary(list[last])
|
||||
if f.endsBasicSourceBlock(list[last]) {
|
||||
extendToClosingBrace = false // Block is broken up now.
|
||||
last++
|
||||
break
|
||||
}
|
||||
}
|
||||
if extendToClosingBrace {
|
||||
end = blockEnd
|
||||
}
|
||||
if pos != end { // Can have no source to cover if e.g. blocks abut.
|
||||
newList = append(newList, f.newCounter(pos, end, last))
|
||||
}
|
||||
newList = append(newList, list[0:last]...)
|
||||
list = list[last:]
|
||||
if len(list) == 0 {
|
||||
break
|
||||
}
|
||||
pos = list[0].Pos()
|
||||
}
|
||||
return newList
|
||||
}
|
||||
|
||||
// hasFuncLiteral reports the existence and position of the first func literal
|
||||
// in the node, if any. If a func literal appears, it usually marks the termination
|
||||
// of a basic block because the function body is itself a block.
|
||||
// Therefore we draw a line at the start of the body of the first function literal we find.
|
||||
// TODO: what if there's more than one? Probably doesn't matter much.
|
||||
func hasFuncLiteral(n ast.Node) (bool, token.Pos) {
|
||||
if n == nil {
|
||||
return false, 0
|
||||
}
|
||||
var literal funcLitFinder
|
||||
ast.Walk(&literal, n)
|
||||
return literal.found(), token.Pos(literal)
|
||||
}
|
||||
|
||||
// statementBoundary finds the location in s that terminates the current basic
|
||||
// block in the source.
|
||||
func (f *File) statementBoundary(s ast.Stmt) token.Pos {
|
||||
// Control flow statements are easy.
|
||||
switch s := s.(type) {
|
||||
case *ast.BlockStmt:
|
||||
// Treat blocks like basic blocks to avoid overlapping counters.
|
||||
return s.Lbrace
|
||||
case *ast.IfStmt:
|
||||
found, pos := hasFuncLiteral(s.Init)
|
||||
if found {
|
||||
return pos
|
||||
}
|
||||
found, pos = hasFuncLiteral(s.Cond)
|
||||
if found {
|
||||
return pos
|
||||
}
|
||||
return s.Body.Lbrace
|
||||
case *ast.ForStmt:
|
||||
found, pos := hasFuncLiteral(s.Init)
|
||||
if found {
|
||||
return pos
|
||||
}
|
||||
found, pos = hasFuncLiteral(s.Cond)
|
||||
if found {
|
||||
return pos
|
||||
}
|
||||
found, pos = hasFuncLiteral(s.Post)
|
||||
if found {
|
||||
return pos
|
||||
}
|
||||
return s.Body.Lbrace
|
||||
case *ast.LabeledStmt:
|
||||
return f.statementBoundary(s.Stmt)
|
||||
case *ast.RangeStmt:
|
||||
found, pos := hasFuncLiteral(s.X)
|
||||
if found {
|
||||
return pos
|
||||
}
|
||||
return s.Body.Lbrace
|
||||
case *ast.SwitchStmt:
|
||||
found, pos := hasFuncLiteral(s.Init)
|
||||
if found {
|
||||
return pos
|
||||
}
|
||||
found, pos = hasFuncLiteral(s.Tag)
|
||||
if found {
|
||||
return pos
|
||||
}
|
||||
return s.Body.Lbrace
|
||||
case *ast.SelectStmt:
|
||||
return s.Body.Lbrace
|
||||
case *ast.TypeSwitchStmt:
|
||||
found, pos := hasFuncLiteral(s.Init)
|
||||
if found {
|
||||
return pos
|
||||
}
|
||||
return s.Body.Lbrace
|
||||
}
|
||||
// If not a control flow statement, it is a declaration, expression, call, etc. and it may have a function literal.
|
||||
// If it does, that's tricky because we want to exclude the body of the function from this block.
|
||||
// Draw a line at the start of the body of the first function literal we find.
|
||||
// TODO: what if there's more than one? Probably doesn't matter much.
|
||||
found, pos := hasFuncLiteral(s)
|
||||
if found {
|
||||
return pos
|
||||
}
|
||||
return s.End()
|
||||
}
|
||||
|
||||
// endsBasicSourceBlock reports whether s changes the flow of control: break, if, etc.,
|
||||
// or if it's just problematic, for instance contains a function literal, which will complicate
|
||||
// accounting due to the block-within-an expression.
|
||||
func (f *File) endsBasicSourceBlock(s ast.Stmt) bool {
|
||||
switch s := s.(type) {
|
||||
case *ast.BlockStmt:
|
||||
// Treat blocks like basic blocks to avoid overlapping counters.
|
||||
return true
|
||||
case *ast.BranchStmt:
|
||||
return true
|
||||
case *ast.ForStmt:
|
||||
return true
|
||||
case *ast.IfStmt:
|
||||
return true
|
||||
case *ast.LabeledStmt:
|
||||
return f.endsBasicSourceBlock(s.Stmt)
|
||||
case *ast.RangeStmt:
|
||||
return true
|
||||
case *ast.SwitchStmt:
|
||||
return true
|
||||
case *ast.SelectStmt:
|
||||
return true
|
||||
case *ast.TypeSwitchStmt:
|
||||
return true
|
||||
case *ast.ExprStmt:
|
||||
// Calls to panic change the flow.
|
||||
// We really should verify that "panic" is the predefined function,
|
||||
// but without type checking we can't and the likelihood of it being
|
||||
// an actual problem is vanishingly small.
|
||||
if call, ok := s.X.(*ast.CallExpr); ok {
|
||||
if ident, ok := call.Fun.(*ast.Ident); ok && ident.Name == "panic" && len(call.Args) == 1 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
found, _ := hasFuncLiteral(s)
|
||||
return found
|
||||
}
|
||||
|
||||
// funcLitFinder implements the ast.Visitor pattern to find the location of any
|
||||
// function literal in a subtree.
|
||||
type funcLitFinder token.Pos
|
||||
|
||||
func (f *funcLitFinder) Visit(node ast.Node) (w ast.Visitor) {
|
||||
if f.found() {
|
||||
return nil // Prune search.
|
||||
}
|
||||
switch n := node.(type) {
|
||||
case *ast.FuncLit:
|
||||
*f = funcLitFinder(n.Body.Lbrace)
|
||||
return nil // Prune search.
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *funcLitFinder) found() bool {
|
||||
return token.Pos(*f) != token.NoPos
|
||||
}
|
||||
|
||||
// Sort interface for []block1; used for self-check in addVariables.
|
||||
|
||||
type block1 struct {
|
||||
Block
|
||||
index int
|
||||
}
|
||||
|
||||
type blockSlice []block1
|
||||
|
||||
func (b blockSlice) Len() int { return len(b) }
|
||||
func (b blockSlice) Less(i, j int) bool { return b[i].startByte < b[j].startByte }
|
||||
func (b blockSlice) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
|
||||
|
||||
// offset translates a token position into a 0-indexed byte offset.
|
||||
func (f *File) offset(pos token.Pos) int {
|
||||
return f.fset.Position(pos).Offset
|
||||
}
|
||||
|
||||
// addVariables adds to the end of the file the declarations to set up the counter and position variables.
|
||||
func (f *File) addVariables(w io.Writer) {
|
||||
// Self-check: Verify that the instrumented basic blocks are disjoint.
|
||||
t := make([]block1, len(f.blocks))
|
||||
for i := range f.blocks {
|
||||
t[i].Block = f.blocks[i]
|
||||
t[i].index = i
|
||||
}
|
||||
sort.Sort(blockSlice(t))
|
||||
for i := 1; i < len(t); i++ {
|
||||
if t[i-1].endByte > t[i].startByte {
|
||||
fmt.Fprintf(os.Stderr, "cover: internal error: block %d overlaps block %d\n", t[i-1].index, t[i].index)
|
||||
// Note: error message is in byte positions, not token positions.
|
||||
fmt.Fprintf(os.Stderr, "\t%s:#%d,#%d %s:#%d,#%d\n",
|
||||
f.name, f.offset(t[i-1].startByte), f.offset(t[i-1].endByte),
|
||||
f.name, f.offset(t[i].startByte), f.offset(t[i].endByte))
|
||||
}
|
||||
}
|
||||
|
||||
// Declare the coverage struct as a package-level variable.
|
||||
fmt.Fprintf(w, "\nvar %s = struct {\n", *varVar)
|
||||
fmt.Fprintf(w, "\tCount [%d]uint32\n", len(f.blocks))
|
||||
fmt.Fprintf(w, "\tPos [3 * %d]uint32\n", len(f.blocks))
|
||||
fmt.Fprintf(w, "\tNumStmt [%d]uint16\n", len(f.blocks))
|
||||
fmt.Fprintf(w, "} {\n")
|
||||
|
||||
// Initialize the position array field.
|
||||
fmt.Fprintf(w, "\tPos: [3 * %d]uint32{\n", len(f.blocks))
|
||||
|
||||
// A nice long list of positions. Each position is encoded as follows to reduce size:
|
||||
// - 32-bit starting line number
|
||||
// - 32-bit ending line number
|
||||
// - (16 bit ending column number << 16) | (16-bit starting column number).
|
||||
for i, block := range f.blocks {
|
||||
start := f.fset.Position(block.startByte)
|
||||
end := f.fset.Position(block.endByte)
|
||||
fmt.Fprintf(w, "\t\t%d, %d, %#x, // [%d]\n", start.Line, end.Line, (end.Column&0xFFFF)<<16|(start.Column&0xFFFF), i)
|
||||
}
|
||||
|
||||
// Close the position array.
|
||||
fmt.Fprintf(w, "\t},\n")
|
||||
|
||||
// Initialize the position array field.
|
||||
fmt.Fprintf(w, "\tNumStmt: [%d]uint16{\n", len(f.blocks))
|
||||
|
||||
// A nice long list of statements-per-block, so we can give a conventional
|
||||
// valuation of "percent covered". To save space, it's a 16-bit number, so we
|
||||
// clamp it if it overflows - won't matter in practice.
|
||||
for i, block := range f.blocks {
|
||||
n := block.numStmt
|
||||
if n > 1<<16-1 {
|
||||
n = 1<<16 - 1
|
||||
}
|
||||
fmt.Fprintf(w, "\t\t%d, // %d\n", n, i)
|
||||
}
|
||||
|
||||
// Close the statements-per-block array.
|
||||
fmt.Fprintf(w, "\t},\n")
|
||||
|
||||
// Close the struct initialization.
|
||||
fmt.Fprintf(w, "}\n")
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
// 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.
|
||||
|
||||
// No testdata on Android.
|
||||
|
||||
// +build !android
|
||||
|
||||
package main_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const (
|
||||
// Data directory, also the package directory for the test.
|
||||
testdata = "testdata"
|
||||
|
||||
// Binaries we compile.
|
||||
testcover = "./testcover.exe"
|
||||
)
|
||||
|
||||
var (
|
||||
// Files we use.
|
||||
testMain = filepath.Join(testdata, "main.go")
|
||||
testTest = filepath.Join(testdata, "test.go")
|
||||
coverInput = filepath.Join(testdata, "test_line.go")
|
||||
coverOutput = filepath.Join(testdata, "test_cover.go")
|
||||
)
|
||||
|
||||
var debug = false // Keeps the rewritten files around if set.
|
||||
|
||||
// Run this shell script, but do it in Go so it can be run by "go test".
|
||||
//
|
||||
// replace the word LINE with the line number < testdata/test.go > testdata/test_line.go
|
||||
// go build -o ./testcover
|
||||
// ./testcover -mode=count -var=CoverTest -o ./testdata/test_cover.go testdata/test_line.go
|
||||
// go run ./testdata/main.go ./testdata/test.go
|
||||
//
|
||||
func TestCover(t *testing.T) {
|
||||
// Read in the test file (testTest) and write it, with LINEs specified, to coverInput.
|
||||
file, err := ioutil.ReadFile(testTest)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
lines := bytes.Split(file, []byte("\n"))
|
||||
for i, line := range lines {
|
||||
lines[i] = bytes.Replace(line, []byte("LINE"), []byte(fmt.Sprint(i+1)), -1)
|
||||
}
|
||||
err = ioutil.WriteFile(coverInput, bytes.Join(lines, []byte("\n")), 0666)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// defer removal of test_line.go
|
||||
if !debug {
|
||||
defer os.Remove(coverInput)
|
||||
}
|
||||
|
||||
// go build -o testcover
|
||||
cmd := exec.Command("go", "build", "-o", testcover)
|
||||
run(cmd, t)
|
||||
|
||||
// defer removal of testcover
|
||||
defer os.Remove(testcover)
|
||||
|
||||
// ./testcover -mode=count -var=coverTest -o ./testdata/test_cover.go testdata/test_line.go
|
||||
cmd = exec.Command(testcover, "-mode=count", "-var=coverTest", "-o", coverOutput, coverInput)
|
||||
run(cmd, t)
|
||||
|
||||
// defer removal of ./testdata/test_cover.go
|
||||
if !debug {
|
||||
defer os.Remove(coverOutput)
|
||||
}
|
||||
|
||||
// go run ./testdata/main.go ./testdata/test.go
|
||||
cmd = exec.Command("go", "run", testMain, coverOutput)
|
||||
run(cmd, t)
|
||||
}
|
||||
|
||||
func run(c *exec.Cmd, t *testing.T) {
|
||||
c.Stdout = os.Stdout
|
||||
c.Stderr = os.Stderr
|
||||
err := c.Run()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
// 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.
|
||||
|
||||
/*
|
||||
Cover is a program for analyzing the coverage profiles generated by
|
||||
'go test -coverprofile=cover.out'.
|
||||
|
||||
Cover is also used by 'go test -cover' to rewrite the source code with
|
||||
annotations to track which parts of each function are executed.
|
||||
It operates on one Go source file at a time, computing approximate
|
||||
basic block information by studying the source. It is thus more portable
|
||||
than binary-rewriting coverage tools, but also a little less capable.
|
||||
For instance, it does not probe inside && and || expressions, and can
|
||||
be mildly confused by single statements with multiple function literals.
|
||||
|
||||
For usage information, please see:
|
||||
go help testflag
|
||||
go tool cover -help
|
||||
|
||||
No longer maintained:
|
||||
|
||||
For Go releases 1.5 and later, this tool lives in the
|
||||
standard repository. The code here is not maintained.
|
||||
|
||||
*/
|
||||
package main // import "golang.org/x/tools/cmd/cover"
|
|
@ -0,0 +1,166 @@
|
|||
// 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 file implements the visitor that computes the (line, column)-(line-column) range for each function.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/build"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"text/tabwriter"
|
||||
|
||||
"golang.org/x/tools/cover"
|
||||
)
|
||||
|
||||
// funcOutput takes two file names as arguments, a coverage profile to read as input and an output
|
||||
// file to write ("" means to write to standard output). The function reads the profile and produces
|
||||
// as output the coverage data broken down by function, like this:
|
||||
//
|
||||
// fmt/format.go:30: init 100.0%
|
||||
// fmt/format.go:57: clearflags 100.0%
|
||||
// ...
|
||||
// fmt/scan.go:1046: doScan 100.0%
|
||||
// fmt/scan.go:1075: advance 96.2%
|
||||
// fmt/scan.go:1119: doScanf 96.8%
|
||||
// total: (statements) 91.9%
|
||||
|
||||
func funcOutput(profile, outputFile string) error {
|
||||
profiles, err := cover.ParseProfiles(profile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var out *bufio.Writer
|
||||
if outputFile == "" {
|
||||
out = bufio.NewWriter(os.Stdout)
|
||||
} else {
|
||||
fd, err := os.Create(outputFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fd.Close()
|
||||
out = bufio.NewWriter(fd)
|
||||
}
|
||||
defer out.Flush()
|
||||
|
||||
tabber := tabwriter.NewWriter(out, 1, 8, 1, '\t', 0)
|
||||
defer tabber.Flush()
|
||||
|
||||
var total, covered int64
|
||||
for _, profile := range profiles {
|
||||
fn := profile.FileName
|
||||
file, err := findFile(fn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
funcs, err := findFuncs(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Now match up functions and profile blocks.
|
||||
for _, f := range funcs {
|
||||
c, t := f.coverage(profile)
|
||||
fmt.Fprintf(tabber, "%s:%d:\t%s\t%.1f%%\n", fn, f.startLine, f.name, 100.0*float64(c)/float64(t))
|
||||
total += t
|
||||
covered += c
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(tabber, "total:\t(statements)\t%.1f%%\n", 100.0*float64(covered)/float64(total))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// findFuncs parses the file and returns a slice of FuncExtent descriptors.
|
||||
func findFuncs(name string) ([]*FuncExtent, error) {
|
||||
fset := token.NewFileSet()
|
||||
parsedFile, err := parser.ParseFile(fset, name, nil, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
visitor := &FuncVisitor{
|
||||
fset: fset,
|
||||
name: name,
|
||||
astFile: parsedFile,
|
||||
}
|
||||
ast.Walk(visitor, visitor.astFile)
|
||||
return visitor.funcs, nil
|
||||
}
|
||||
|
||||
// FuncExtent describes a function's extent in the source by file and position.
|
||||
type FuncExtent struct {
|
||||
name string
|
||||
startLine int
|
||||
startCol int
|
||||
endLine int
|
||||
endCol int
|
||||
}
|
||||
|
||||
// FuncVisitor implements the visitor that builds the function position list for a file.
|
||||
type FuncVisitor struct {
|
||||
fset *token.FileSet
|
||||
name string // Name of file.
|
||||
astFile *ast.File
|
||||
funcs []*FuncExtent
|
||||
}
|
||||
|
||||
// Visit implements the ast.Visitor interface.
|
||||
func (v *FuncVisitor) Visit(node ast.Node) ast.Visitor {
|
||||
switch n := node.(type) {
|
||||
case *ast.FuncDecl:
|
||||
start := v.fset.Position(n.Pos())
|
||||
end := v.fset.Position(n.End())
|
||||
fe := &FuncExtent{
|
||||
name: n.Name.Name,
|
||||
startLine: start.Line,
|
||||
startCol: start.Column,
|
||||
endLine: end.Line,
|
||||
endCol: end.Column,
|
||||
}
|
||||
v.funcs = append(v.funcs, fe)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// coverage returns the fraction of the statements in the function that were covered, as a numerator and denominator.
|
||||
func (f *FuncExtent) coverage(profile *cover.Profile) (num, den int64) {
|
||||
// We could avoid making this n^2 overall by doing a single scan and annotating the functions,
|
||||
// but the sizes of the data structures is never very large and the scan is almost instantaneous.
|
||||
var covered, total int64
|
||||
// The blocks are sorted, so we can stop counting as soon as we reach the end of the relevant block.
|
||||
for _, b := range profile.Blocks {
|
||||
if b.StartLine > f.endLine || (b.StartLine == f.endLine && b.StartCol >= f.endCol) {
|
||||
// Past the end of the function.
|
||||
break
|
||||
}
|
||||
if b.EndLine < f.startLine || (b.EndLine == f.startLine && b.EndCol <= f.startCol) {
|
||||
// Before the beginning of the function
|
||||
continue
|
||||
}
|
||||
total += int64(b.NumStmt)
|
||||
if b.Count > 0 {
|
||||
covered += int64(b.NumStmt)
|
||||
}
|
||||
}
|
||||
if total == 0 {
|
||||
total = 1 // Avoid zero denominator.
|
||||
}
|
||||
return covered, total
|
||||
}
|
||||
|
||||
// findFile finds the location of the named file in GOROOT, GOPATH etc.
|
||||
func findFile(file string) (string, error) {
|
||||
dir, file := filepath.Split(file)
|
||||
pkg, err := build.Import(dir, ".", build.FindOnly)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("can't find %q: %v", file, err)
|
||||
}
|
||||
return filepath.Join(pkg.Dir, file), nil
|
||||
}
|
|
@ -0,0 +1,284 @@
|
|||
// 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"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"golang.org/x/tools/cover"
|
||||
)
|
||||
|
||||
// htmlOutput reads the profile data from profile and generates an HTML
|
||||
// coverage report, writing it to outfile. If outfile is empty,
|
||||
// it writes the report to a temporary file and opens it in a web browser.
|
||||
func htmlOutput(profile, outfile string) error {
|
||||
profiles, err := cover.ParseProfiles(profile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var d templateData
|
||||
|
||||
for _, profile := range profiles {
|
||||
fn := profile.FileName
|
||||
if profile.Mode == "set" {
|
||||
d.Set = true
|
||||
}
|
||||
file, err := findFile(fn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
src, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't read %q: %v", fn, err)
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
err = htmlGen(&buf, src, profile.Boundaries(src))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.Files = append(d.Files, &templateFile{
|
||||
Name: fn,
|
||||
Body: template.HTML(buf.String()),
|
||||
Coverage: percentCovered(profile),
|
||||
})
|
||||
}
|
||||
|
||||
var out *os.File
|
||||
if outfile == "" {
|
||||
var dir string
|
||||
dir, err = ioutil.TempDir("", "cover")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out, err = os.Create(filepath.Join(dir, "coverage.html"))
|
||||
} else {
|
||||
out, err = os.Create(outfile)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = htmlTemplate.Execute(out, d)
|
||||
if err == nil {
|
||||
err = out.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if outfile == "" {
|
||||
if !startBrowser("file://" + out.Name()) {
|
||||
fmt.Fprintf(os.Stderr, "HTML output written to %s\n", out.Name())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// percentCovered returns, as a percentage, the fraction of the statements in
|
||||
// the profile covered by the test run.
|
||||
// In effect, it reports the coverage of a given source file.
|
||||
func percentCovered(p *cover.Profile) float64 {
|
||||
var total, covered int64
|
||||
for _, b := range p.Blocks {
|
||||
total += int64(b.NumStmt)
|
||||
if b.Count > 0 {
|
||||
covered += int64(b.NumStmt)
|
||||
}
|
||||
}
|
||||
if total == 0 {
|
||||
return 0
|
||||
}
|
||||
return float64(covered) / float64(total) * 100
|
||||
}
|
||||
|
||||
// htmlGen generates an HTML coverage report with the provided filename,
|
||||
// source code, and tokens, and writes it to the given Writer.
|
||||
func htmlGen(w io.Writer, src []byte, boundaries []cover.Boundary) error {
|
||||
dst := bufio.NewWriter(w)
|
||||
for i := range src {
|
||||
for len(boundaries) > 0 && boundaries[0].Offset == i {
|
||||
b := boundaries[0]
|
||||
if b.Start {
|
||||
n := 0
|
||||
if b.Count > 0 {
|
||||
n = int(math.Floor(b.Norm*9)) + 1
|
||||
}
|
||||
fmt.Fprintf(dst, `<span class="cov%v" title="%v">`, n, b.Count)
|
||||
} else {
|
||||
dst.WriteString("</span>")
|
||||
}
|
||||
boundaries = boundaries[1:]
|
||||
}
|
||||
switch b := src[i]; b {
|
||||
case '>':
|
||||
dst.WriteString(">")
|
||||
case '<':
|
||||
dst.WriteString("<")
|
||||
case '&':
|
||||
dst.WriteString("&")
|
||||
case '\t':
|
||||
dst.WriteString(" ")
|
||||
default:
|
||||
dst.WriteByte(b)
|
||||
}
|
||||
}
|
||||
return dst.Flush()
|
||||
}
|
||||
|
||||
// startBrowser tries to open the URL in a browser
|
||||
// and reports whether it succeeds.
|
||||
func startBrowser(url string) bool {
|
||||
// try to start the browser
|
||||
var args []string
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
args = []string{"open"}
|
||||
case "windows":
|
||||
args = []string{"cmd", "/c", "start"}
|
||||
default:
|
||||
args = []string{"xdg-open"}
|
||||
}
|
||||
cmd := exec.Command(args[0], append(args[1:], url)...)
|
||||
return cmd.Start() == nil
|
||||
}
|
||||
|
||||
// rgb returns an rgb value for the specified coverage value
|
||||
// between 0 (no coverage) and 10 (max coverage).
|
||||
func rgb(n int) string {
|
||||
if n == 0 {
|
||||
return "rgb(192, 0, 0)" // Red
|
||||
}
|
||||
// Gradient from gray to green.
|
||||
r := 128 - 12*(n-1)
|
||||
g := 128 + 12*(n-1)
|
||||
b := 128 + 3*(n-1)
|
||||
return fmt.Sprintf("rgb(%v, %v, %v)", r, g, b)
|
||||
}
|
||||
|
||||
// colors generates the CSS rules for coverage colors.
|
||||
func colors() template.CSS {
|
||||
var buf bytes.Buffer
|
||||
for i := 0; i < 11; i++ {
|
||||
fmt.Fprintf(&buf, ".cov%v { color: %v }\n", i, rgb(i))
|
||||
}
|
||||
return template.CSS(buf.String())
|
||||
}
|
||||
|
||||
var htmlTemplate = template.Must(template.New("html").Funcs(template.FuncMap{
|
||||
"colors": colors,
|
||||
}).Parse(tmplHTML))
|
||||
|
||||
type templateData struct {
|
||||
Files []*templateFile
|
||||
Set bool
|
||||
}
|
||||
|
||||
type templateFile struct {
|
||||
Name string
|
||||
Body template.HTML
|
||||
Coverage float64
|
||||
}
|
||||
|
||||
const tmplHTML = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<style>
|
||||
body {
|
||||
background: black;
|
||||
color: rgb(80, 80, 80);
|
||||
}
|
||||
body, pre, #legend span {
|
||||
font-family: Menlo, monospace;
|
||||
font-weight: bold;
|
||||
}
|
||||
#topbar {
|
||||
background: black;
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0;
|
||||
height: 42px;
|
||||
border-bottom: 1px solid rgb(80, 80, 80);
|
||||
}
|
||||
#content {
|
||||
margin-top: 50px;
|
||||
}
|
||||
#nav, #legend {
|
||||
float: left;
|
||||
margin-left: 10px;
|
||||
}
|
||||
#legend {
|
||||
margin-top: 12px;
|
||||
}
|
||||
#nav {
|
||||
margin-top: 10px;
|
||||
}
|
||||
#legend span {
|
||||
margin: 0 5px;
|
||||
}
|
||||
{{colors}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="topbar">
|
||||
<div id="nav">
|
||||
<select id="files">
|
||||
{{range $i, $f := .Files}}
|
||||
<option value="file{{$i}}">{{$f.Name}} ({{printf "%.1f" $f.Coverage}}%)</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div id="legend">
|
||||
<span>not tracked</span>
|
||||
{{if .Set}}
|
||||
<span class="cov0">not covered</span>
|
||||
<span class="cov8">covered</span>
|
||||
{{else}}
|
||||
<span class="cov0">no coverage</span>
|
||||
<span class="cov1">low coverage</span>
|
||||
<span class="cov2">*</span>
|
||||
<span class="cov3">*</span>
|
||||
<span class="cov4">*</span>
|
||||
<span class="cov5">*</span>
|
||||
<span class="cov6">*</span>
|
||||
<span class="cov7">*</span>
|
||||
<span class="cov8">*</span>
|
||||
<span class="cov9">*</span>
|
||||
<span class="cov10">high coverage</span>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div id="content">
|
||||
{{range $i, $f := .Files}}
|
||||
<pre class="file" id="file{{$i}}" {{if $i}}style="display: none"{{end}}>{{$f.Body}}</pre>
|
||||
{{end}}
|
||||
</div>
|
||||
</body>
|
||||
<script>
|
||||
(function() {
|
||||
var files = document.getElementById('files');
|
||||
var visible = document.getElementById('file0');
|
||||
files.addEventListener('change', onChange, false);
|
||||
function onChange() {
|
||||
visible.style.display = 'none';
|
||||
visible = document.getElementById(files.value);
|
||||
visible.style.display = 'block';
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</html>
|
||||
`
|
|
@ -0,0 +1,112 @@
|
|||
// 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.
|
||||
|
||||
// Test runner for coverage test. This file is not coverage-annotated; test.go is.
|
||||
// It knows the coverage counter is called "coverTest".
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
testAll()
|
||||
verify()
|
||||
}
|
||||
|
||||
type block struct {
|
||||
count uint32
|
||||
line uint32
|
||||
}
|
||||
|
||||
var counters = make(map[block]bool)
|
||||
|
||||
// check records the location and expected value for a counter.
|
||||
func check(line, count uint32) {
|
||||
b := block{
|
||||
count,
|
||||
line,
|
||||
}
|
||||
counters[b] = true
|
||||
}
|
||||
|
||||
// checkVal is a version of check that returns its extra argument,
|
||||
// so it can be used in conditionals.
|
||||
func checkVal(line, count uint32, val int) int {
|
||||
b := block{
|
||||
count,
|
||||
line,
|
||||
}
|
||||
counters[b] = true
|
||||
return val
|
||||
}
|
||||
|
||||
var PASS = true
|
||||
|
||||
// verify checks the expected counts against the actual. It runs after the test has completed.
|
||||
func verify() {
|
||||
for b := range counters {
|
||||
got, index := count(b.line)
|
||||
if b.count == anything && got != 0 {
|
||||
got = anything
|
||||
}
|
||||
if got != b.count {
|
||||
fmt.Fprintf(os.Stderr, "test_go:%d expected count %d got %d [counter %d]\n", b.line, b.count, got, index)
|
||||
PASS = false
|
||||
}
|
||||
}
|
||||
verifyPanic()
|
||||
if !PASS {
|
||||
fmt.Fprintf(os.Stderr, "FAIL\n")
|
||||
os.Exit(2)
|
||||
}
|
||||
}
|
||||
|
||||
// verifyPanic is a special check for the known counter that should be
|
||||
// after the panic call in testPanic.
|
||||
func verifyPanic() {
|
||||
if coverTest.Count[panicIndex-1] != 1 {
|
||||
// Sanity check for test before panic.
|
||||
fmt.Fprintf(os.Stderr, "bad before panic")
|
||||
PASS = false
|
||||
}
|
||||
if coverTest.Count[panicIndex] != 0 {
|
||||
fmt.Fprintf(os.Stderr, "bad at panic: %d should be 0\n", coverTest.Count[panicIndex])
|
||||
PASS = false
|
||||
}
|
||||
if coverTest.Count[panicIndex+1] != 1 {
|
||||
fmt.Fprintf(os.Stderr, "bad after panic")
|
||||
PASS = false
|
||||
}
|
||||
}
|
||||
|
||||
// count returns the count and index for the counter at the specified line.
|
||||
func count(line uint32) (uint32, int) {
|
||||
// Linear search is fine. Choose perfect fit over approximate.
|
||||
// We can have a closing brace for a range on the same line as a condition for an "else if"
|
||||
// and we don't want that brace to steal the count for the condition on the "if".
|
||||
// Therefore we test for a perfect (lo==line && hi==line) match, but if we can't
|
||||
// find that we take the first imperfect match.
|
||||
index := -1
|
||||
indexLo := uint32(1e9)
|
||||
for i := range coverTest.Count {
|
||||
lo, hi := coverTest.Pos[3*i], coverTest.Pos[3*i+1]
|
||||
if lo == line && line == hi {
|
||||
return coverTest.Count[i], i
|
||||
}
|
||||
// Choose the earliest match (the counters are in unpredictable order).
|
||||
if lo <= line && line <= hi && indexLo > lo {
|
||||
index = i
|
||||
indexLo = lo
|
||||
}
|
||||
}
|
||||
if index == -1 {
|
||||
fmt.Fprintln(os.Stderr, "cover_test: no counter for line", line)
|
||||
PASS = false
|
||||
return 0, 0
|
||||
}
|
||||
return coverTest.Count[index], index
|
||||
}
|
|
@ -0,0 +1,218 @@
|
|||
// 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 program is processed by the cover command, and then testAll is called.
|
||||
// The test driver in main.go can then compare the coverage statistics with expectation.
|
||||
|
||||
// The word LINE is replaced by the line number in this file. When the file is executed,
|
||||
// the coverage processing has changed the line numbers, so we can't use runtime.Caller.
|
||||
|
||||
package main
|
||||
|
||||
const anything = 1e9 // Just some unlikely value that means "we got here, don't care how often"
|
||||
|
||||
func testAll() {
|
||||
testSimple()
|
||||
testBlockRun()
|
||||
testIf()
|
||||
testFor()
|
||||
testRange()
|
||||
testSwitch()
|
||||
testTypeSwitch()
|
||||
testSelect1()
|
||||
testSelect2()
|
||||
testPanic()
|
||||
testEmptySwitches()
|
||||
}
|
||||
|
||||
// The indexes of the counters in testPanic are known to main.go
|
||||
const panicIndex = 3
|
||||
|
||||
// This test appears first because the index of its counters is known to main.go
|
||||
func testPanic() {
|
||||
defer func() {
|
||||
recover()
|
||||
}()
|
||||
check(LINE, 1)
|
||||
panic("should not get next line")
|
||||
check(LINE, 0) // this is GoCover.Count[panicIndex]
|
||||
// The next counter is in testSimple and it will be non-zero.
|
||||
// If the panic above does not trigger a counter, the test will fail
|
||||
// because GoCover.Count[panicIndex] will be the one in testSimple.
|
||||
}
|
||||
|
||||
func testSimple() {
|
||||
check(LINE, 1)
|
||||
}
|
||||
|
||||
func testIf() {
|
||||
if true {
|
||||
check(LINE, 1)
|
||||
} else {
|
||||
check(LINE, 0)
|
||||
}
|
||||
if false {
|
||||
check(LINE, 0)
|
||||
} else {
|
||||
check(LINE, 1)
|
||||
}
|
||||
for i := 0; i < 3; i++ {
|
||||
if checkVal(LINE, 3, i) <= 2 {
|
||||
check(LINE, 3)
|
||||
}
|
||||
if checkVal(LINE, 3, i) <= 1 {
|
||||
check(LINE, 2)
|
||||
}
|
||||
if checkVal(LINE, 3, i) <= 0 {
|
||||
check(LINE, 1)
|
||||
}
|
||||
}
|
||||
for i := 0; i < 3; i++ {
|
||||
if checkVal(LINE, 3, i) <= 1 {
|
||||
check(LINE, 2)
|
||||
} else {
|
||||
check(LINE, 1)
|
||||
}
|
||||
}
|
||||
for i := 0; i < 3; i++ {
|
||||
if checkVal(LINE, 3, i) <= 0 {
|
||||
check(LINE, 1)
|
||||
} else if checkVal(LINE, 2, i) <= 1 {
|
||||
check(LINE, 1)
|
||||
} else if checkVal(LINE, 1, i) <= 2 {
|
||||
check(LINE, 1)
|
||||
} else if checkVal(LINE, 0, i) <= 3 {
|
||||
check(LINE, 0)
|
||||
}
|
||||
}
|
||||
if func(a, b int) bool { return a < b }(3, 4) {
|
||||
check(LINE, 1)
|
||||
}
|
||||
}
|
||||
|
||||
func testFor() {
|
||||
for i := 0; i < 10; func() { i++; check(LINE, 10) }() {
|
||||
check(LINE, 10)
|
||||
}
|
||||
}
|
||||
|
||||
func testRange() {
|
||||
for _, f := range []func(){
|
||||
func() { check(LINE, 1) },
|
||||
} {
|
||||
f()
|
||||
check(LINE, 1)
|
||||
}
|
||||
}
|
||||
|
||||
func testBlockRun() {
|
||||
check(LINE, 1)
|
||||
{
|
||||
check(LINE, 1)
|
||||
}
|
||||
{
|
||||
check(LINE, 1)
|
||||
}
|
||||
check(LINE, 1)
|
||||
{
|
||||
check(LINE, 1)
|
||||
}
|
||||
{
|
||||
check(LINE, 1)
|
||||
}
|
||||
check(LINE, 1)
|
||||
}
|
||||
|
||||
func testSwitch() {
|
||||
for i := 0; i < 5; func() { i++; check(LINE, 5) }() {
|
||||
switch i {
|
||||
case 0:
|
||||
check(LINE, 1)
|
||||
case 1:
|
||||
check(LINE, 1)
|
||||
case 2:
|
||||
check(LINE, 1)
|
||||
default:
|
||||
check(LINE, 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testTypeSwitch() {
|
||||
var x = []interface{}{1, 2.0, "hi"}
|
||||
for _, v := range x {
|
||||
switch func() { check(LINE, 3) }(); v.(type) {
|
||||
case int:
|
||||
check(LINE, 1)
|
||||
case float64:
|
||||
check(LINE, 1)
|
||||
case string:
|
||||
check(LINE, 1)
|
||||
case complex128:
|
||||
check(LINE, 0)
|
||||
default:
|
||||
check(LINE, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testSelect1() {
|
||||
c := make(chan int)
|
||||
go func() {
|
||||
for i := 0; i < 1000; i++ {
|
||||
c <- i
|
||||
}
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case <-c:
|
||||
check(LINE, anything)
|
||||
case <-c:
|
||||
check(LINE, anything)
|
||||
default:
|
||||
check(LINE, 1)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testSelect2() {
|
||||
c1 := make(chan int, 1000)
|
||||
c2 := make(chan int, 1000)
|
||||
for i := 0; i < 1000; i++ {
|
||||
c1 <- i
|
||||
c2 <- i
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case <-c1:
|
||||
check(LINE, 1000)
|
||||
case <-c2:
|
||||
check(LINE, 1000)
|
||||
default:
|
||||
check(LINE, 1)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Empty control statements created syntax errors. This function
|
||||
// is here just to be sure that those are handled correctly now.
|
||||
func testEmptySwitches() {
|
||||
check(LINE, 1)
|
||||
switch 3 {
|
||||
}
|
||||
check(LINE, 1)
|
||||
switch i := (interface{})(3).(int); i {
|
||||
}
|
||||
check(LINE, 1)
|
||||
c := make(chan int)
|
||||
go func() {
|
||||
check(LINE, 1)
|
||||
c <- 1
|
||||
select {}
|
||||
}()
|
||||
<-c
|
||||
check(LINE, 1)
|
||||
}
|
|
@ -0,0 +1,540 @@
|
|||
// The digraph command performs queries over unlabelled directed graphs
|
||||
// represented in text form. It is intended to integrate nicely with
|
||||
// typical UNIX command pipelines.
|
||||
//
|
||||
// Since directed graphs (import graphs, reference graphs, call graphs,
|
||||
// etc) often arise during software tool development and debugging, this
|
||||
// command is included in the go.tools repository.
|
||||
//
|
||||
// TODO(adonovan):
|
||||
// - support input files other than stdin
|
||||
// - suport alternative formats (AT&T GraphViz, CSV, etc),
|
||||
// a comment syntax, etc.
|
||||
// - allow queries to nest, like Blaze query language.
|
||||
//
|
||||
package main // import "golang.org/x/tools/cmd/digraph"
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
const Usage = `digraph: queries over directed graphs in text form.
|
||||
|
||||
Graph 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 field 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.
|
||||
|
||||
Supported queries:
|
||||
|
||||
nodes
|
||||
the set of all nodes
|
||||
degree
|
||||
the in-degree and out-degree of each node.
|
||||
preds <label> ...
|
||||
the set of immediate predecessors of the specified nodes
|
||||
succs <label> ...
|
||||
the set of immediate successors of the specified nodes
|
||||
forward <label> ...
|
||||
the set of nodes transitively reachable from the specified nodes
|
||||
reverse <label> ...
|
||||
the set of nodes that transitively reach the specified nodes
|
||||
somepath <label> <label>
|
||||
the list of nodes on some arbitrary path from the first node to the second
|
||||
allpaths <label> <label>
|
||||
the set of nodes on all paths from the first node to the second
|
||||
sccs
|
||||
all strongly connected components (one per line)
|
||||
scc <label>
|
||||
the set of nodes nodes strongly connected to the specified one
|
||||
|
||||
Example usage:
|
||||
|
||||
Show the transitive closure of imports of the digraph tool itself:
|
||||
% go list -f '{{.ImportPath}}{{.Imports}}' ... | tr '[]' ' ' |
|
||||
digraph forward golang.org/x/tools/cmd/digraph
|
||||
|
||||
Show which clothes (see above) must be donned before a jacket:
|
||||
% digraph reverse jacket <clothes.txt
|
||||
|
||||
`
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
args := flag.Args()
|
||||
if len(args) == 0 {
|
||||
fmt.Println(Usage)
|
||||
return
|
||||
}
|
||||
|
||||
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, label := range l {
|
||||
if i > 0 {
|
||||
fmt.Fprint(stdout, sep)
|
||||
}
|
||||
fmt.Fprint(stdout, label)
|
||||
}
|
||||
fmt.Fprintln(stdout)
|
||||
}
|
||||
|
||||
type nodeset map[string]bool
|
||||
|
||||
func (s nodeset) sort() nodelist {
|
||||
labels := make(nodelist, len(s))
|
||||
var i int
|
||||
for label := range s {
|
||||
labels[i] = label
|
||||
i++
|
||||
}
|
||||
sort.Strings(labels)
|
||||
return labels
|
||||
}
|
||||
|
||||
func (s nodeset) addAll(x nodeset) {
|
||||
for label := range x {
|
||||
s[label] = true
|
||||
}
|
||||
}
|
||||
|
||||
// A graph maps nodes to the non-nil set of their immediate successors.
|
||||
type graph map[string]nodeset
|
||||
|
||||
func (g graph) addNode(label string) nodeset {
|
||||
edges := g[label]
|
||||
if edges == nil {
|
||||
edges = make(nodeset)
|
||||
g[label] = 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) reachableFrom(roots nodeset) nodeset {
|
||||
seen := make(nodeset)
|
||||
var visit func(label string)
|
||||
visit = func(label string) {
|
||||
if !seen[label] {
|
||||
seen[label] = true
|
||||
for e := range g[label] {
|
||||
visit(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
for root := range roots {
|
||||
visit(root)
|
||||
}
|
||||
return seen
|
||||
}
|
||||
|
||||
func (g graph) transpose() graph {
|
||||
rev := make(graph)
|
||||
for label, edges := range g {
|
||||
rev.addNode(label)
|
||||
for succ := range edges {
|
||||
rev.addEdges(succ, label)
|
||||
}
|
||||
}
|
||||
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(label string)
|
||||
visit = func(label string) {
|
||||
if !seen[label] {
|
||||
seen[label] = true
|
||||
for e := range g[label] {
|
||||
visit(e)
|
||||
}
|
||||
S = append(S, label)
|
||||
}
|
||||
}
|
||||
for label := range g {
|
||||
visit(label)
|
||||
}
|
||||
|
||||
// Reverse pass.
|
||||
rev := g.transpose()
|
||||
var scc nodeset
|
||||
seen = make(nodeset)
|
||||
var rvisit func(label string)
|
||||
rvisit = func(label string) {
|
||||
if !seen[label] {
|
||||
seen[label] = true
|
||||
scc[label] = true
|
||||
for e := range rev[label] {
|
||||
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)
|
||||
sccs = append(sccs, scc)
|
||||
}
|
||||
}
|
||||
return sccs
|
||||
}
|
||||
|
||||
func parse(rd io.Reader) (graph, error) {
|
||||
g := make(graph)
|
||||
|
||||
var linenum int
|
||||
in := bufio.NewScanner(rd)
|
||||
for in.Scan() {
|
||||
linenum++
|
||||
// Split into words, honoring double-quotes per Go spec.
|
||||
words, err := split(in.Text())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("at line %d: %v", linenum, err)
|
||||
}
|
||||
if len(words) > 0 {
|
||||
g.addEdges(words[0], words[1:]...)
|
||||
}
|
||||
}
|
||||
if err := in.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return g, nil
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
nodes := make(nodeset)
|
||||
for label := range g {
|
||||
nodes[label] = true
|
||||
}
|
||||
nodes.sort().println("\n")
|
||||
|
||||
case "degree":
|
||||
if len(args) != 0 {
|
||||
return fmt.Errorf("usage: digraph degree")
|
||||
}
|
||||
nodes := make(nodeset)
|
||||
for label := range g {
|
||||
nodes[label] = true
|
||||
}
|
||||
rev := g.transpose()
|
||||
for _, label := range nodes.sort() {
|
||||
fmt.Fprintf(stdout, "%d\t%d\t%s\n", len(rev[label]), len(g[label]), label)
|
||||
}
|
||||
|
||||
case "succs", "preds":
|
||||
if len(args) == 0 {
|
||||
return fmt.Errorf("usage: digraph %s <label> ...", 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 <label> ...", 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)
|
||||
}
|
||||
|
||||
seen := make(nodeset)
|
||||
var visit func(path nodelist, label string) bool
|
||||
visit = func(path nodelist, label string) bool {
|
||||
if !seen[label] {
|
||||
seen[label] = true
|
||||
if label == to {
|
||||
append(path, label).println("\n")
|
||||
return true // unwind
|
||||
}
|
||||
for e := range g[label] {
|
||||
if visit(append(path, label), e) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
if !visit(make(nodelist, 0, 100), from) {
|
||||
return fmt.Errorf("no path from %q to %q", args[0], args[1])
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
seen := make(nodeset) // value of seen[x] indicates whether x is on some path to 'to'
|
||||
var visit func(label string) bool
|
||||
visit = func(label string) bool {
|
||||
reachesTo, ok := seen[label]
|
||||
if !ok {
|
||||
reachesTo = label == to
|
||||
|
||||
seen[label] = reachesTo
|
||||
for e := range g[label] {
|
||||
if visit(e) {
|
||||
reachesTo = true
|
||||
}
|
||||
}
|
||||
seen[label] = reachesTo
|
||||
}
|
||||
return reachesTo
|
||||
}
|
||||
if !visit(from) {
|
||||
return fmt.Errorf("no path from %q to %q", from, to)
|
||||
}
|
||||
for label, reachesTo := range seen {
|
||||
if !reachesTo {
|
||||
delete(seen, label)
|
||||
}
|
||||
}
|
||||
seen.sort().println("\n")
|
||||
|
||||
case "sccs":
|
||||
if len(args) != 0 {
|
||||
return fmt.Errorf("usage: digraph sccs")
|
||||
}
|
||||
for _, scc := range g.sccs() {
|
||||
scc.sort().println(" ")
|
||||
}
|
||||
|
||||
case "scc":
|
||||
if len(args) != 1 {
|
||||
return fmt.Errorf("usage: digraph scc <label>")
|
||||
}
|
||||
label := args[0]
|
||||
if g[label] == nil {
|
||||
return fmt.Errorf("no such node %q", label)
|
||||
}
|
||||
for _, scc := range g.sccs() {
|
||||
if scc[label] {
|
||||
scc.sort().println("\n")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
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,121 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"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
|
||||
`
|
||||
|
||||
for _, test := range []struct {
|
||||
input string
|
||||
cmd string
|
||||
args []string
|
||||
want string
|
||||
}{
|
||||
{g1, "nodes", nil, "belt\nhat\njacket\npants\nshirt\nshoes\nshorts\nsocks\nsweater\ntie\n"},
|
||||
{g1, "reverse", []string{"jacket"}, "jacket\nshirt\nsweater\n"},
|
||||
{g1, "forward", []string{"socks"}, "shoes\nsocks\n"},
|
||||
{g1, "forward", []string{"socks", "sweater"}, "jacket\nshoes\nsocks\nsweater\n"},
|
||||
|
||||
{g2, "allpaths", []string{"a", "d"}, "a\nb\nc\nd\n"},
|
||||
|
||||
{g2, "sccs", nil, "a\nb\nc d\n"},
|
||||
{g2, "scc", []string{"d"}, "c\nd\n"},
|
||||
{g2, "succs", []string{"a"}, "b\nc\n"},
|
||||
{g2, "preds", []string{"c"}, "a\nd\n"},
|
||||
{g2, "preds", []string{"c", "d"}, "a\nb\nc\nd\n"},
|
||||
} {
|
||||
stdin = strings.NewReader(test.input)
|
||||
stdout = new(bytes.Buffer)
|
||||
if err := digraph(test.cmd, test.args); err != nil {
|
||||
t.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
got := stdout.(fmt.Stringer).String()
|
||||
if got != test.want {
|
||||
t.Errorf("digraph(%s, %s) = %q, want %q", test.cmd, test.args, got, test.want)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(adonovan):
|
||||
// - test somepath (it's nondeterministic).
|
||||
// - test errors
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,150 @@
|
|||
// 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/build"
|
||||
"go/format"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/tools/go/buildutil"
|
||||
"golang.org/x/tools/go/loader"
|
||||
"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")
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.Var((*buildutil.TagsFlag)(&build.Default.BuildTags), "tags", buildutil.TagsFlagDoc)
|
||||
}
|
||||
|
||||
const usage = `eg: an example-based refactoring tool.
|
||||
|
||||
Usage: eg -t template.go [-w] [-transitive] <args>...
|
||||
|
||||
-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.
|
||||
` + loader.FromArgsUsage
|
||||
|
||||
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 {
|
||||
fmt.Fprint(os.Stderr, eg.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")
|
||||
}
|
||||
|
||||
conf := loader.Config{
|
||||
Fset: token.NewFileSet(),
|
||||
ParserMode: parser.ParseComments,
|
||||
}
|
||||
|
||||
// The first Created package is the template.
|
||||
conf.CreateFromFilenames("template", *templateFlag)
|
||||
|
||||
if _, err := conf.FromArgs(args, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Load, parse and type-check the whole program.
|
||||
iprog, err := conf.Load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Analyze the template.
|
||||
template := iprog.Created[0]
|
||||
xform, err := eg.NewTransformer(iprog.Fset, template.Pkg, template.Files[0], &template.Info, *verboseFlag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Apply it to the input packages.
|
||||
var pkgs []*loader.PackageInfo
|
||||
if *transitiveFlag {
|
||||
for _, info := range iprog.AllPackages {
|
||||
pkgs = append(pkgs, info)
|
||||
}
|
||||
} else {
|
||||
pkgs = iprog.InitialPackages()
|
||||
}
|
||||
var hadErrors bool
|
||||
for _, pkg := range pkgs {
|
||||
if pkg == template {
|
||||
continue
|
||||
}
|
||||
for _, file := range pkg.Files {
|
||||
n := xform.Transform(&pkg.Info, pkg.Pkg, file)
|
||||
if n == 0 {
|
||||
continue
|
||||
}
|
||||
filename := iprog.Fset.File(file.Pos()).Name()
|
||||
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(iprog.Fset, filename, file); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "eg: %s\n", err)
|
||||
hadErrors = true
|
||||
}
|
||||
} else {
|
||||
format.Node(os.Stdout, iprog.Fset, file)
|
||||
}
|
||||
}
|
||||
}
|
||||
if hadErrors {
|
||||
os.Exit(1)
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,511 @@
|
|||
// 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 canical 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/build"
|
||||
"go/format"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"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 = ioutil.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 http://godoc.org/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.Fprintf(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[*build.Package]bool)
|
||||
|
||||
// addEdge adds an edge to the import graph.
|
||||
addEdge := func(from *build.Package, to string) {
|
||||
if to == "C" || to == "unsafe" {
|
||||
return // fake
|
||||
}
|
||||
pkgs := importedBy[to]
|
||||
if pkgs == nil {
|
||||
pkgs = make(map[*build.Package]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.Package.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, msg)
|
||||
}
|
||||
}
|
||||
|
||||
for _, imp := range p.Imports {
|
||||
addEdge(&p.Package, imp)
|
||||
}
|
||||
for _, imp := range p.TestImports {
|
||||
addEdge(&p.Package, imp)
|
||||
}
|
||||
for _, imp := range p.XTestImports {
|
||||
addEdge(&p.Package, imp)
|
||||
}
|
||||
|
||||
// Does package have an explicit import comment?
|
||||
if p.ImportComment != "" {
|
||||
if p.ImportComment != p.ImportPath {
|
||||
canonical[p.ImportPath] = canonicalName{
|
||||
path: p.Package.ImportComment,
|
||||
name: p.Package.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 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[*build.Package]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 *build.Package, 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
|
||||
}
|
||||
|
||||
// rewrite 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 is a copy of cmd/go/list.Package.
|
||||
// It has more fields than build.Package and we need some of them.
|
||||
type listPackage struct {
|
||||
build.Package
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
var cwd string
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
cwd, err = os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatalf("os.Getwd: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 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,243 @@
|
|||
// 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.
|
||||
|
||||
// +build !android
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// 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
|
||||
|
||||
func TestFixImports(t *testing.T) {
|
||||
gopath := filepath.Join(cwd, "testdata")
|
||||
if err := os.Setenv("GOPATH", gopath); err != nil {
|
||||
t.Fatalf("os.Setenv: %v", err)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
// Check status code.
|
||||
if fiximports(test.packages...) != test.wantOK {
|
||||
t.Errorf("#%d. fiximports() = %t", i, !test.wantOK)
|
||||
}
|
||||
|
||||
// Compare stderr output.
|
||||
if got := 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 <<%s>>, want <<%s>>",
|
||||
i, stderr, 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) {
|
||||
gopath := filepath.Join(cwd, "testdata")
|
||||
if err := os.Setenv("GOPATH", gopath); err != nil {
|
||||
t.Fatalf("os.Setenv: %v", err)
|
||||
}
|
||||
|
||||
*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)
|
||||
}
|
||||
}
|
7
vendor/golang.org/x/tools/cmd/fiximports/testdata/src/fruit.io/banana/banana.go
generated
vendored
Normal file
7
vendor/golang.org/x/tools/cmd/fiximports/testdata/src/fruit.io/banana/banana.go
generated
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
package banana
|
||||
|
||||
import (
|
||||
_ "old.com/one"
|
||||
_ "titanic.biz/bar"
|
||||
_ "titanic.biz/foo"
|
||||
)
|
3
vendor/golang.org/x/tools/cmd/fiximports/testdata/src/fruit.io/orange/orange.go
generated
vendored
Normal file
3
vendor/golang.org/x/tools/cmd/fiximports/testdata/src/fruit.io/orange/orange.go
generated
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
package orange
|
||||
|
||||
import _ "fruit.io/pear"
|
3
vendor/golang.org/x/tools/cmd/fiximports/testdata/src/fruit.io/pear/pear.go
generated
vendored
Normal file
3
vendor/golang.org/x/tools/cmd/fiximports/testdata/src/fruit.io/pear/pear.go
generated
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
package pear
|
||||
|
||||
import _ "fruit.io/banana"
|
1
vendor/golang.org/x/tools/cmd/fiximports/testdata/src/new.com/one/one.go
generated
vendored
Normal file
1
vendor/golang.org/x/tools/cmd/fiximports/testdata/src/new.com/one/one.go
generated
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
package one // import "new.com/one"
|
2
vendor/golang.org/x/tools/cmd/fiximports/testdata/src/old.com/bad/bad.go
generated
vendored
Normal file
2
vendor/golang.org/x/tools/cmd/fiximports/testdata/src/old.com/bad/bad.go
generated
vendored
Normal file
|
@ -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
vendor/golang.org/x/tools/cmd/fiximports/testdata/src/old.com/one/one.go
generated
vendored
Normal file
1
vendor/golang.org/x/tools/cmd/fiximports/testdata/src/old.com/one/one.go
generated
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
package one // import "new.com/one"
|
2
vendor/golang.org/x/tools/cmd/fiximports/testdata/src/titanic.biz/bar/bar.go
generated
vendored
Normal file
2
vendor/golang.org/x/tools/cmd/fiximports/testdata/src/titanic.biz/bar/bar.go
generated
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
// This package is moving to new.com too.
|
||||
package bar // import "new.com/bar"
|
2
vendor/golang.org/x/tools/cmd/fiximports/testdata/src/titanic.biz/foo/foo.go
generated
vendored
Normal file
2
vendor/golang.org/x/tools/cmd/fiximports/testdata/src/titanic.biz/foo/foo.go
generated
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
// This package hasn't jumped ship yet.
|
||||
package foo
|
|
@ -0,0 +1,5 @@
|
|||
.git
|
||||
.dockerignore
|
||||
LICENSE
|
||||
README.md
|
||||
.gitignore
|
|
@ -0,0 +1,3 @@
|
|||
build
|
||||
testgetgo
|
||||
getgo
|
|
@ -0,0 +1,20 @@
|
|||
FROM golang:latest
|
||||
|
||||
ENV SHELL /bin/bash
|
||||
ENV HOME /root
|
||||
WORKDIR $HOME
|
||||
|
||||
COPY . /go/src/golang.org/x/tools/cmd/getgo
|
||||
|
||||
RUN ( \
|
||||
cd /go/src/golang.org/x/tools/cmd/getgo \
|
||||
&& go build \
|
||||
&& mv getgo /usr/local/bin/getgo \
|
||||
)
|
||||
|
||||
# undo the adding of GOPATH to env for testing
|
||||
ENV PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
ENV GOPATH ""
|
||||
|
||||
# delete /go and /usr/local/go for testing
|
||||
RUN rm -rf /go /usr/local/go
|
|
@ -0,0 +1,27 @@
|
|||
Copyright (c) 2017 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,71 @@
|
|||
# getgo
|
||||
|
||||
A proof-of-concept command-line installer for Go.
|
||||
|
||||
This installer is designed to both install Go as well as do the initial configuration
|
||||
of setting up the right environment variables and paths.
|
||||
|
||||
It will install the Go distribution (tools & stdlib) to "/.go" inside your home directory by default.
|
||||
|
||||
It will setup "$HOME/go" as your GOPATH.
|
||||
This is where third party libraries and apps will be installed as well as where you will write your Go code.
|
||||
|
||||
If Go is already installed via this installer it will upgrade it to the latest version of Go.
|
||||
|
||||
Currently the installer supports Windows, \*nix and macOS on x86 & x64.
|
||||
It supports Bash and Zsh on all of these platforms as well as powershell & cmd.exe on Windows.
|
||||
|
||||
## Usage
|
||||
|
||||
Windows Powershell/cmd.exe:
|
||||
|
||||
`(New-Object System.Net.WebClient).DownloadFile('https://get.golang.org/installer.exe', 'installer.exe'); Start-Process -Wait -NonewWindow installer.exe; Remove-Item installer.exe`
|
||||
|
||||
Shell (Linux/macOS/Windows):
|
||||
|
||||
`curl -LO https://get.golang.org/$(uname)/go_installer && chmod +x go_installer && ./go_installer && rm go_installer`
|
||||
|
||||
## To Do
|
||||
|
||||
* Check if Go is already installed (via a different method) and update it in place or at least notify the user
|
||||
* Lots of testing. It's only had limited testing so far.
|
||||
* Add support for additional shells.
|
||||
|
||||
## Development instructions
|
||||
|
||||
### Testing
|
||||
|
||||
There are integration tests in [`main_test.go`](main_test.go). Please add more
|
||||
tests there.
|
||||
|
||||
#### On unix/linux with the Dockerfile
|
||||
|
||||
The Dockerfile automatically builds the binary, moves it to
|
||||
`/usr/local/bin/getgo` and then unsets `$GOPATH` and removes all `$GOPATH` from
|
||||
`$PATH`.
|
||||
|
||||
```bash
|
||||
$ docker build --rm --force-rm -t getgo .
|
||||
...
|
||||
$ docker run --rm -it getgo bash
|
||||
root@78425260fad0:~# getgo -v
|
||||
Welcome to the Go installer!
|
||||
Downloading Go version go1.8.3 to /usr/local/go
|
||||
This may take a bit of time...
|
||||
Adding "export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/go/bin" to /root/.bashrc
|
||||
Downloaded!
|
||||
Setting up GOPATH
|
||||
Adding "export GOPATH=/root/go" to /root/.bashrc
|
||||
Adding "export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/go/bin:/root/go/bin" to /root/.bashrc
|
||||
GOPATH has been setup!
|
||||
root@78425260fad0:~# which go
|
||||
/usr/local/go/bin/go
|
||||
root@78425260fad0:~# echo $GOPATH
|
||||
/root/go
|
||||
root@78425260fad0:~# echo $PATH
|
||||
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/go/bin:/root/go/bin
|
||||
```
|
||||
|
||||
## Release instructions
|
||||
|
||||
To upload a new release of getgo, run `./make.bash && ./upload.bash`.
|
|
@ -0,0 +1,184 @@
|
|||
// 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.
|
||||
|
||||
// +build !plan9
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"archive/zip"
|
||||
"compress/gzip"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
currentVersionURL = "https://golang.org/VERSION?m=text"
|
||||
downloadURLPrefix = "https://dl.google.com/go"
|
||||
)
|
||||
|
||||
// downloadGoVersion downloads and upacks the specific go version to dest/go.
|
||||
func downloadGoVersion(version, ops, arch, dest string) error {
|
||||
suffix := "tar.gz"
|
||||
if ops == "windows" {
|
||||
suffix = "zip"
|
||||
}
|
||||
uri := fmt.Sprintf("%s/%s.%s-%s.%s", downloadURLPrefix, version, ops, arch, suffix)
|
||||
|
||||
verbosef("Downloading %s", uri)
|
||||
|
||||
req, err := http.NewRequest("GET", uri, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Add("User-Agent", fmt.Sprintf("golang.org-getgo/%s", version))
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Downloading Go from %s failed: %v", uri, err)
|
||||
}
|
||||
if resp.StatusCode > 299 {
|
||||
return fmt.Errorf("Downloading Go from %s failed with HTTP status %s", uri, resp.Status)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
tmpf, err := ioutil.TempFile("", "go")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(tmpf.Name())
|
||||
|
||||
h := sha256.New()
|
||||
|
||||
w := io.MultiWriter(tmpf, h)
|
||||
if _, err := io.Copy(w, resp.Body); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
verbosef("Downloading SHA %s.sha256", uri)
|
||||
|
||||
sresp, err := http.Get(uri + ".sha256")
|
||||
if err != nil {
|
||||
return fmt.Errorf("Downloading Go sha256 from %s.sha256 failed: %v", uri, err)
|
||||
}
|
||||
defer sresp.Body.Close()
|
||||
if sresp.StatusCode > 299 {
|
||||
return fmt.Errorf("Downloading Go sha256 from %s.sha256 failed with HTTP status %s", uri, sresp.Status)
|
||||
}
|
||||
|
||||
shasum, err := ioutil.ReadAll(sresp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check the shasum.
|
||||
sum := fmt.Sprintf("%x", h.Sum(nil))
|
||||
if sum != string(shasum) {
|
||||
return fmt.Errorf("Shasum mismatch %s vs. %s", sum, string(shasum))
|
||||
}
|
||||
|
||||
unpackFunc := unpackTar
|
||||
if ops == "windows" {
|
||||
unpackFunc = unpackZip
|
||||
}
|
||||
if err := unpackFunc(tmpf.Name(), dest); err != nil {
|
||||
return fmt.Errorf("Unpacking Go to %s failed: %v", dest, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func unpack(dest, name string, fi os.FileInfo, r io.Reader) error {
|
||||
if strings.HasPrefix(name, "go/") {
|
||||
name = name[len("go/"):]
|
||||
}
|
||||
|
||||
path := filepath.Join(dest, name)
|
||||
if fi.IsDir() {
|
||||
return os.MkdirAll(path, fi.Mode())
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, fi.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
_, err = io.Copy(f, r)
|
||||
return err
|
||||
}
|
||||
|
||||
func unpackTar(src, dest string) error {
|
||||
r, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
archive, err := gzip.NewReader(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer archive.Close()
|
||||
|
||||
tarReader := tar.NewReader(archive)
|
||||
|
||||
for {
|
||||
header, err := tarReader.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := unpack(dest, header.Name, header.FileInfo(), tarReader); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func unpackZip(src, dest string) error {
|
||||
zr, err := zip.OpenReader(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, f := range zr.File {
|
||||
fr, err := f.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := unpack(dest, f.Name, f.FileInfo(), fr); err != nil {
|
||||
return err
|
||||
}
|
||||
fr.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getLatestGoVersion() (string, error) {
|
||||
resp, err := http.Get(currentVersionURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Getting current Go version failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode > 299 {
|
||||
b, _ := ioutil.ReadAll(io.LimitReader(resp.Body, 1024))
|
||||
return "", fmt.Errorf("Could not get current Go version: HTTP %d: %q", resp.StatusCode, b)
|
||||
}
|
||||
version, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(string(version)), nil
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
// 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.
|
||||
|
||||
// +build !plan9
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDownloadGoVersion(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skipf("Skipping download in short mode")
|
||||
}
|
||||
|
||||
tmpd, err := ioutil.TempDir("", "go")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpd)
|
||||
|
||||
if err := downloadGoVersion("go1.8.1", "linux", "amd64", filepath.Join(tmpd, "go")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Ensure the VERSION file exists.
|
||||
vf := filepath.Join(tmpd, "go", "VERSION")
|
||||
if _, err := os.Stat(vf); os.IsNotExist(err) {
|
||||
t.Fatalf("file %s does not exist and should", vf)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
// 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.
|
||||
|
||||
// +build !plan9
|
||||
|
||||
// The getgo command installs Go to the user's system.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
interactive = flag.Bool("i", false, "Interactive mode, prompt for inputs.")
|
||||
verbose = flag.Bool("v", false, "Verbose.")
|
||||
setupOnly = flag.Bool("skip-dl", false, "Don't download - only set up environment variables")
|
||||
goVersion = flag.String("version", "", `Version of Go to install (e.g. "1.8.3"). If empty, uses the latest version.`)
|
||||
|
||||
version = "devel"
|
||||
)
|
||||
|
||||
var exitCleanly error = errors.New("exit cleanly sentinel value")
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
if *goVersion != "" && !strings.HasPrefix(*goVersion, "go") {
|
||||
*goVersion = "go" + *goVersion
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
verbosef("version " + version)
|
||||
|
||||
runStep := func(s step) {
|
||||
err := s(ctx)
|
||||
if err == exitCleanly {
|
||||
os.Exit(0)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(2)
|
||||
}
|
||||
}
|
||||
|
||||
if !*setupOnly {
|
||||
runStep(welcome)
|
||||
runStep(checkOthers)
|
||||
runStep(chooseVersion)
|
||||
runStep(downloadGo)
|
||||
}
|
||||
|
||||
runStep(setupGOPATH)
|
||||
}
|
||||
|
||||
func verbosef(format string, v ...interface{}) {
|
||||
if !*verbose {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf(format+"\n", v...)
|
||||
}
|
||||
|
||||
func prompt(ctx context.Context, query, defaultAnswer string) (string, error) {
|
||||
if !*interactive {
|
||||
return defaultAnswer, nil
|
||||
}
|
||||
|
||||
fmt.Printf("%s [%s]: ", query, defaultAnswer)
|
||||
|
||||
type result struct {
|
||||
answer string
|
||||
err error
|
||||
}
|
||||
ch := make(chan result, 1)
|
||||
go func() {
|
||||
s := bufio.NewScanner(os.Stdin)
|
||||
if !s.Scan() {
|
||||
ch <- result{"", s.Err()}
|
||||
return
|
||||
}
|
||||
answer := s.Text()
|
||||
if answer == "" {
|
||||
answer = defaultAnswer
|
||||
}
|
||||
ch <- result{answer, nil}
|
||||
}()
|
||||
|
||||
select {
|
||||
case r := <-ch:
|
||||
return r.answer, r.err
|
||||
case <-ctx.Done():
|
||||
return "", ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
func runCommand(ctx context.Context, prog string, args ...string) ([]byte, error) {
|
||||
verbosef("Running command: %s %v", prog, args)
|
||||
|
||||
cmd := exec.CommandContext(ctx, prog, args...)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("running cmd '%s %s' failed: %s err: %v", prog, strings.Join(args, " "), string(out), err)
|
||||
}
|
||||
if out != nil && err == nil && len(out) != 0 {
|
||||
verbosef("%s", out)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
|
@ -0,0 +1,173 @@
|
|||
// 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.
|
||||
|
||||
// +build !plan9
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const (
|
||||
testbin = "testgetgo"
|
||||
)
|
||||
|
||||
var (
|
||||
exeSuffix string // ".exe" on Windows
|
||||
)
|
||||
|
||||
func init() {
|
||||
if runtime.GOOS == "windows" {
|
||||
exeSuffix = ".exe"
|
||||
}
|
||||
}
|
||||
|
||||
// TestMain creates a getgo command for testing purposes and
|
||||
// deletes it after the tests have been run.
|
||||
func TestMain(m *testing.M) {
|
||||
if os.Getenv("GOGET_INTEGRATION") == "" {
|
||||
fmt.Fprintln(os.Stderr, "main_test: Skipping integration tests with GOGET_INTEGRATION unset")
|
||||
return
|
||||
}
|
||||
|
||||
args := []string{"build", "-tags", testbin, "-o", testbin + exeSuffix}
|
||||
out, err := exec.Command("go", args...).CombinedOutput()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "building %s failed: %v\n%s", testbin, err, out)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
// Don't let these environment variables confuse the test.
|
||||
os.Unsetenv("GOBIN")
|
||||
os.Unsetenv("GOPATH")
|
||||
os.Unsetenv("GIT_ALLOW_PROTOCOL")
|
||||
os.Unsetenv("PATH")
|
||||
|
||||
r := m.Run()
|
||||
|
||||
os.Remove(testbin + exeSuffix)
|
||||
|
||||
os.Exit(r)
|
||||
}
|
||||
|
||||
func createTmpHome(t *testing.T) string {
|
||||
tmpd, err := ioutil.TempDir("", "testgetgo")
|
||||
if err != nil {
|
||||
t.Fatalf("creating test tempdir failed: %v", err)
|
||||
}
|
||||
|
||||
os.Setenv("HOME", tmpd)
|
||||
return tmpd
|
||||
}
|
||||
|
||||
// doRun runs the test getgo command, recording stdout and stderr and
|
||||
// returning exit status.
|
||||
func doRun(t *testing.T, args ...string) error {
|
||||
var stdout, stderr bytes.Buffer
|
||||
t.Logf("running %s %v", testbin, args)
|
||||
cmd := exec.Command("./"+testbin+exeSuffix, args...)
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
cmd.Env = os.Environ()
|
||||
status := cmd.Run()
|
||||
if stdout.Len() > 0 {
|
||||
t.Log("standard output:")
|
||||
t.Log(stdout.String())
|
||||
}
|
||||
if stderr.Len() > 0 {
|
||||
t.Log("standard error:")
|
||||
t.Log(stderr.String())
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
func TestCommandVerbose(t *testing.T) {
|
||||
tmpd := createTmpHome(t)
|
||||
defer os.RemoveAll(tmpd)
|
||||
|
||||
err := doRun(t, "-v")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// make sure things are in path
|
||||
shellConfig, err := shellConfigFile()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b, err := ioutil.ReadFile(shellConfig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
home, err := getHomeDir()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expected := fmt.Sprintf(`
|
||||
export PATH=$PATH:%s/.go/bin
|
||||
|
||||
export GOPATH=%s/go
|
||||
|
||||
export PATH=$PATH:%s/go/bin
|
||||
`, home, home, home)
|
||||
|
||||
if string(b) != expected {
|
||||
t.Fatalf("%s expected %q, got %q", shellConfig, expected, string(b))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandPathExists(t *testing.T) {
|
||||
tmpd := createTmpHome(t)
|
||||
defer os.RemoveAll(tmpd)
|
||||
|
||||
// run once
|
||||
err := doRun(t, "-skip-dl")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// make sure things are in path
|
||||
shellConfig, err := shellConfigFile()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b, err := ioutil.ReadFile(shellConfig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
home, err := getHomeDir()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expected := fmt.Sprintf(`
|
||||
export GOPATH=%s/go
|
||||
|
||||
export PATH=$PATH:%s/go/bin
|
||||
`, home, home)
|
||||
|
||||
if string(b) != expected {
|
||||
t.Fatalf("%s expected %q, got %q", shellConfig, expected, string(b))
|
||||
}
|
||||
|
||||
// run twice
|
||||
if err := doRun(t, "-skip-dl"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
b, err = ioutil.ReadFile(shellConfig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(b) != expected {
|
||||
t.Fatalf("%s expected %q, got %q", shellConfig, expected, string(b))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
#!/bin/bash
|
||||
|
||||
# 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.
|
||||
|
||||
set -e -o -x
|
||||
|
||||
LDFLAGS="-X main.version=$(git describe --always --dirty='*')"
|
||||
|
||||
GOOS=windows GOARCH=386 go build -o build/installer.exe -ldflags="$LDFLAGS"
|
||||
GOOS=linux GOARCH=386 go build -o build/installer_linux -ldflags="$LDFLAGS"
|
||||
GOOS=darwin GOARCH=386 go build -o build/installer_darwin -ldflags="$LDFLAGS"
|
|
@ -0,0 +1,155 @@
|
|||
// 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.
|
||||
|
||||
// +build !plan9
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
bashConfig = ".bash_profile"
|
||||
zshConfig = ".zshrc"
|
||||
)
|
||||
|
||||
// appendToPATH adds the given path to the PATH environment variable and
|
||||
// persists it for future sessions.
|
||||
func appendToPATH(value string) error {
|
||||
if isInPATH(value) {
|
||||
return nil
|
||||
}
|
||||
return persistEnvVar("PATH", pathVar+envSeparator+value)
|
||||
}
|
||||
|
||||
func isInPATH(dir string) bool {
|
||||
p := os.Getenv("PATH")
|
||||
|
||||
paths := strings.Split(p, envSeparator)
|
||||
for _, d := range paths {
|
||||
if d == dir {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func getHomeDir() (string, error) {
|
||||
home := os.Getenv(homeKey)
|
||||
if home != "" {
|
||||
return home, nil
|
||||
}
|
||||
|
||||
u, err := user.Current()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return u.HomeDir, nil
|
||||
}
|
||||
|
||||
func checkStringExistsFile(filename, value string) (bool, error) {
|
||||
file, err := os.OpenFile(filename, os.O_RDONLY, 0600)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if line == value {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, scanner.Err()
|
||||
}
|
||||
|
||||
func appendToFile(filename, value string) error {
|
||||
verbosef("Adding %q to %s", value, filename)
|
||||
|
||||
ok, err := checkStringExistsFile(filename, value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ok {
|
||||
// Nothing to do.
|
||||
return nil
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
_, err = f.WriteString(lineEnding + value + lineEnding)
|
||||
return err
|
||||
}
|
||||
|
||||
func isShell(name string) bool {
|
||||
return strings.Contains(currentShell(), name)
|
||||
}
|
||||
|
||||
// persistEnvVarWindows sets an environment variable in the Windows
|
||||
// registry.
|
||||
func persistEnvVarWindows(name, value string) error {
|
||||
_, err := runCommand(context.Background(), "powershell", "-command",
|
||||
fmt.Sprintf(`[Environment]::SetEnvironmentVariable("%s", "%s", "User")`, name, value))
|
||||
return err
|
||||
}
|
||||
|
||||
func persistEnvVar(name, value string) error {
|
||||
if runtime.GOOS == "windows" {
|
||||
if err := persistEnvVarWindows(name, value); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if isShell("cmd.exe") || isShell("powershell.exe") {
|
||||
return os.Setenv(strings.ToUpper(name), value)
|
||||
}
|
||||
// User is in bash, zsh, etc.
|
||||
// Also set the environment variable in their shell config.
|
||||
}
|
||||
|
||||
rc, err := shellConfigFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
line := fmt.Sprintf("export %s=%s", strings.ToUpper(name), value)
|
||||
if err := appendToFile(rc, line); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.Setenv(strings.ToUpper(name), value)
|
||||
}
|
||||
|
||||
func shellConfigFile() (string, error) {
|
||||
home, err := getHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
switch {
|
||||
case isShell("bash"):
|
||||
return filepath.Join(home, bashConfig), nil
|
||||
case isShell("zsh"):
|
||||
return filepath.Join(home, zshConfig), nil
|
||||
default:
|
||||
return "", fmt.Errorf("%q is not a supported shell", currentShell())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
// 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.
|
||||
|
||||
// +build !plan9
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAppendPath(t *testing.T) {
|
||||
tmpd, err := ioutil.TempDir("", "go")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpd)
|
||||
|
||||
if err := os.Setenv("HOME", tmpd); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
GOPATH := os.Getenv("GOPATH")
|
||||
if err := appendToPATH(filepath.Join(GOPATH, "bin")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
shellConfig, err := shellConfigFile()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b, err := ioutil.ReadFile(shellConfig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expected := "export PATH=" + pathVar + envSeparator + filepath.Join(GOPATH, "bin")
|
||||
if strings.TrimSpace(string(b)) != expected {
|
||||
t.Fatalf("expected: %q, got %q", expected, strings.TrimSpace(string(b)))
|
||||
}
|
||||
|
||||
// Check that appendToPATH is idempotent.
|
||||
if err := appendToPATH(filepath.Join(GOPATH, "bin")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b, err = ioutil.ReadFile(shellConfig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.TrimSpace(string(b)) != expected {
|
||||
t.Fatalf("expected: %q, got %q", expected, strings.TrimSpace(string(b)))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
# getgo server
|
||||
|
||||
## Deployment
|
||||
|
||||
```
|
||||
gcloud app deploy --promote --project golang-org
|
||||
```
|
|
@ -0,0 +1,7 @@
|
|||
runtime: go
|
||||
service: get
|
||||
api_version: go1
|
||||
|
||||
handlers:
|
||||
- url: /.*
|
||||
script: _go_app
|
|
@ -0,0 +1,61 @@
|
|||
// 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.
|
||||
|
||||
// Command server serves get.golang.org, redirecting users to the appropriate
|
||||
// getgo installer based on the request path.
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
base = "https://dl.google.com/go/getgo/"
|
||||
windowsInstaller = base + "installer.exe"
|
||||
linuxInstaller = base + "installer_linux"
|
||||
macInstaller = base + "installer_darwin"
|
||||
)
|
||||
|
||||
// substring-based redirects.
|
||||
var stringMatch = map[string]string{
|
||||
// via uname, from bash
|
||||
"MINGW": windowsInstaller, // Reported as MINGW64_NT-10.0 in git bash
|
||||
"Linux": linuxInstaller,
|
||||
"Darwin": macInstaller,
|
||||
}
|
||||
|
||||
func init() {
|
||||
http.HandleFunc("/", handler)
|
||||
}
|
||||
|
||||
func handler(w http.ResponseWriter, r *http.Request) {
|
||||
if containsIgnoreCase(r.URL.Path, "installer.exe") {
|
||||
// cache bust
|
||||
http.Redirect(w, r, windowsInstaller+cacheBust(), http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
for match, redirect := range stringMatch {
|
||||
if containsIgnoreCase(r.URL.Path, match) {
|
||||
http.Redirect(w, r, redirect, http.StatusFound)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
|
||||
func containsIgnoreCase(s, substr string) bool {
|
||||
return strings.Contains(
|
||||
strings.ToLower(s),
|
||||
strings.ToLower(substr),
|
||||
)
|
||||
}
|
||||
|
||||
func cacheBust() string {
|
||||
return fmt.Sprintf("?%d", time.Now().Nanosecond())
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
// 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.
|
||||
|
||||
// +build !plan9
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type step func(context.Context) error
|
||||
|
||||
func welcome(ctx context.Context) error {
|
||||
fmt.Println("Welcome to the Go installer!")
|
||||
answer, err := prompt(ctx, "Would you like to install Go? Y/n", "Y")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.ToLower(answer) != "y" {
|
||||
fmt.Println("Exiting install.")
|
||||
return exitCleanly
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkOthers(ctx context.Context) error {
|
||||
// TODO: if go is currently installed install new version over that
|
||||
path, err := whichGo(ctx)
|
||||
if err != nil {
|
||||
fmt.Printf("Cannot check if Go is already installed:\n%v\n", err)
|
||||
}
|
||||
if path == "" {
|
||||
return nil
|
||||
}
|
||||
if path != installPath {
|
||||
fmt.Printf("Go is already installed at %v; remove it from your PATH.\n", path)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func chooseVersion(ctx context.Context) error {
|
||||
if *goVersion != "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
*goVersion, err = getLatestGoVersion()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
answer, err := prompt(ctx, fmt.Sprintf("The latest Go version is %s, install that? Y/n", *goVersion), "Y")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if strings.ToLower(answer) != "y" {
|
||||
// TODO: handle passing a version
|
||||
fmt.Println("Aborting install.")
|
||||
return exitCleanly
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func downloadGo(ctx context.Context) error {
|
||||
answer, err := prompt(ctx, fmt.Sprintf("Download Go version %s to %s? Y/n", *goVersion, installPath), "Y")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if strings.ToLower(answer) != "y" {
|
||||
fmt.Println("Aborting install.")
|
||||
return exitCleanly
|
||||
}
|
||||
|
||||
fmt.Printf("Downloading Go version %s to %s\n", *goVersion, installPath)
|
||||
fmt.Println("This may take a bit of time...")
|
||||
|
||||
if err := downloadGoVersion(*goVersion, runtime.GOOS, arch, installPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := appendToPATH(filepath.Join(installPath, "bin")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("Downloaded!")
|
||||
return nil
|
||||
}
|
||||
|
||||
func setupGOPATH(ctx context.Context) error {
|
||||
answer, err := prompt(ctx, "Would you like us to setup your GOPATH? Y/n", "Y")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if strings.ToLower(answer) != "y" {
|
||||
fmt.Println("Exiting and not setting up GOPATH.")
|
||||
return exitCleanly
|
||||
}
|
||||
|
||||
fmt.Println("Setting up GOPATH")
|
||||
home, err := getHomeDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gopath := os.Getenv("GOPATH")
|
||||
if gopath == "" {
|
||||
// set $GOPATH
|
||||
gopath = filepath.Join(home, "go")
|
||||
if err := persistEnvVar("GOPATH", gopath); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("GOPATH has been set up!")
|
||||
} else {
|
||||
verbosef("GOPATH is already set to %s", gopath)
|
||||
}
|
||||
|
||||
if err := appendToPATH(filepath.Join(gopath, "bin")); err != nil {
|
||||
return err
|
||||
}
|
||||
return persistEnvChangesForSession()
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
// 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.
|
||||
|
||||
// +build !plan9
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// arch contains either amd64 or 386.
|
||||
var arch = func() string {
|
||||
cmd := exec.Command("uname", "-m") // "x86_64"
|
||||
if runtime.GOOS == "windows" {
|
||||
cmd = exec.Command("powershell", "-command", "(Get-WmiObject -Class Win32_ComputerSystem).SystemType") // "x64-based PC"
|
||||
}
|
||||
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
// a sensible default?
|
||||
return "amd64"
|
||||
}
|
||||
if bytes.Contains(out, []byte("64")) {
|
||||
return "amd64"
|
||||
}
|
||||
return "386"
|
||||
}()
|
||||
|
||||
func findGo(ctx context.Context, cmd string) (string, error) {
|
||||
out, err := exec.CommandContext(ctx, cmd, "go").CombinedOutput()
|
||||
return strings.TrimSpace(string(out)), err
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
// 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.
|
||||
|
||||
// +build darwin dragonfly freebsd linux nacl netbsd openbsd solaris
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const (
|
||||
envSeparator = ":"
|
||||
homeKey = "HOME"
|
||||
lineEnding = "\n"
|
||||
pathVar = "$PATH"
|
||||
)
|
||||
|
||||
var installPath = func() string {
|
||||
home, err := getHomeDir()
|
||||
if err != nil {
|
||||
return "/usr/local/go"
|
||||
}
|
||||
|
||||
return filepath.Join(home, ".go")
|
||||
}()
|
||||
|
||||
func whichGo(ctx context.Context) (string, error) {
|
||||
return findGo(ctx, "which")
|
||||
}
|
||||
|
||||
func isWindowsXP() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func currentShell() string {
|
||||
return os.Getenv("SHELL")
|
||||
}
|
||||
|
||||
func persistEnvChangesForSession() error {
|
||||
shellConfig, err := shellConfigFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println()
|
||||
fmt.Printf("One more thing! Run `source %s` to persist the\n", shellConfig)
|
||||
fmt.Println("new environment variables to your current session, or open a")
|
||||
fmt.Println("new shell prompt.")
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
// 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.
|
||||
|
||||
// +build windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
envSeparator = ";"
|
||||
homeKey = "USERPROFILE"
|
||||
lineEnding = "/r/n"
|
||||
pathVar = "$env:Path"
|
||||
)
|
||||
|
||||
var installPath = `c:\go`
|
||||
|
||||
func isWindowsXP() bool {
|
||||
v, err := syscall.GetVersion()
|
||||
if err != nil {
|
||||
log.Fatalf("GetVersion failed: %v", err)
|
||||
}
|
||||
major := byte(v)
|
||||
return major < 6
|
||||
}
|
||||
|
||||
func whichGo(ctx context.Context) (string, error) {
|
||||
return findGo(ctx, "where")
|
||||
}
|
||||
|
||||
// currentShell reports the current shell.
|
||||
// It might be "powershell.exe", "cmd.exe" or any of the *nix shells.
|
||||
//
|
||||
// Returns empty string if the shell is unknown.
|
||||
func currentShell() string {
|
||||
shell := os.Getenv("SHELL")
|
||||
if shell != "" {
|
||||
return shell
|
||||
}
|
||||
|
||||
pid := os.Getppid()
|
||||
pe, err := getProcessEntry(pid)
|
||||
if err != nil {
|
||||
verbosef("getting shell from process entry failed: %v", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
return syscall.UTF16ToString(pe.ExeFile[:])
|
||||
}
|
||||
|
||||
func getProcessEntry(pid int) (*syscall.ProcessEntry32, error) {
|
||||
// From https://go.googlesource.com/go/+/go1.8.3/src/syscall/syscall_windows.go#941
|
||||
snapshot, err := syscall.CreateToolhelp32Snapshot(syscall.TH32CS_SNAPPROCESS, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer syscall.CloseHandle(snapshot)
|
||||
|
||||
var procEntry syscall.ProcessEntry32
|
||||
procEntry.Size = uint32(unsafe.Sizeof(procEntry))
|
||||
if err = syscall.Process32First(snapshot, &procEntry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for {
|
||||
if procEntry.ProcessID == uint32(pid) {
|
||||
return &procEntry, nil
|
||||
}
|
||||
|
||||
if err := syscall.Process32Next(snapshot, &procEntry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func persistEnvChangesForSession() error {
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
#!/bin/bash
|
||||
|
||||
# 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.
|
||||
|
||||
if ! command -v gsutil 2>&1 > /dev/null; then
|
||||
echo "Install gsutil:"
|
||||
echo
|
||||
echo " https://cloud.google.com/storage/docs/gsutil_install#sdk-install"
|
||||
fi
|
||||
|
||||
if [ ! -d build ]; then
|
||||
echo "Run make.bash first"
|
||||
fi
|
||||
|
||||
set -e -o -x
|
||||
|
||||
gsutil -m cp -a public-read build/* gs://golang/getgo
|
|
@ -0,0 +1,289 @@
|
|||
// 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"
|
||||
"io/ioutil"
|
||||
"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 := ioutil.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 := ioutil.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,35 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
// 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,13 @@
|
|||
// 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"
|
||||
|
||||
func init() {
|
||||
register("gc", importer.For("gc", nil))
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
// 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/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
register("gccgo", importer.For("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,211 @@
|
|||
// 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"
|
||||
"io/ioutil"
|
||||
"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
|
||||
importFailed = 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 = importFailed
|
||||
}
|
||||
}()
|
||||
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 := ioutil.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,13 @@
|
|||
// 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.
|
||||
|
||||
// +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,13 @@
|
|||
// 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.
|
||||
|
||||
// +build go1.9
|
||||
|
||||
package main
|
||||
|
||||
import "go/types"
|
||||
|
||||
func isAlias(obj *types.TypeName) bool {
|
||||
return obj.IsAlias()
|
||||
}
|
|
@ -0,0 +1,373 @@
|
|||
// 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"
|
||||
exact "go/constant"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"io"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
// 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, _ := 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 exact.Value) *big.Int {
|
||||
// compute big-endian representation of v
|
||||
b := exact.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 exact.Value) string {
|
||||
if exact.Sign(v) == 0 {
|
||||
return "0.0"
|
||||
}
|
||||
// x != 0
|
||||
|
||||
// convert |v| into a big.Rat x
|
||||
x := new(big.Rat).SetFrac(absInt(exact.Num(v)), absInt(exact.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 exact.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 exact.
|
||||
func valString(v exact.Value, floatFmt bool) string {
|
||||
switch v.Kind() {
|
||||
case exact.Int:
|
||||
if floatFmt {
|
||||
return floatString(v)
|
||||
}
|
||||
case exact.Float:
|
||||
return floatString(v)
|
||||
case exact.Complex:
|
||||
re := exact.Real(v)
|
||||
im := exact.Imag(v)
|
||||
var s string
|
||||
if exact.Sign(re) != 0 {
|
||||
s = floatString(re)
|
||||
if exact.Sign(im) >= 0 {
|
||||
s += " + "
|
||||
} else {
|
||||
s += " - "
|
||||
im = exact.UnaryOp(token.SUB, im, 0) // negate im
|
||||
}
|
||||
}
|
||||
// im != 0, otherwise v would be exact.Int or exact.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,242 @@
|
|||
// 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"
|
||||
|
||||
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.Embedded(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 *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,56 @@
|
|||
godoc on appengine
|
||||
------------------
|
||||
|
||||
Prerequisites
|
||||
-------------
|
||||
|
||||
* Go appengine SDK
|
||||
https://developers.google.com/appengine/downloads#Google_App_Engine_SDK_for_Go
|
||||
|
||||
* Go sources at tip under $GOROOT
|
||||
|
||||
* Godoc sources at tip inside $GOPATH
|
||||
(go get -d golang.org/x/tools/cmd/godoc)
|
||||
|
||||
|
||||
Directory structure
|
||||
-------------------
|
||||
|
||||
* Let $APPDIR be the directory containing the app engine files.
|
||||
(e.g., $APPDIR=$HOME/godoc-app)
|
||||
|
||||
* $APPDIR contains the following entries (this may change depending on
|
||||
app-engine release and version of godoc):
|
||||
|
||||
app.yaml
|
||||
golang.org/x/tools/cmd/godoc
|
||||
godoc.zip
|
||||
index.split.*
|
||||
|
||||
* The app.yaml file is set up per app engine documentation.
|
||||
For instance:
|
||||
|
||||
application: godoc-app
|
||||
version: 1
|
||||
runtime: go
|
||||
api_version: go1
|
||||
|
||||
handlers:
|
||||
- url: /.*
|
||||
script: _go_app
|
||||
|
||||
|
||||
Configuring and running godoc
|
||||
-----------------------------
|
||||
|
||||
To configure godoc, run
|
||||
|
||||
bash setup-godoc-app.bash
|
||||
|
||||
to prepare an $APPDIR as described above. See the script for details on usage.
|
||||
|
||||
To run godoc locally, using the App Engine development server, run
|
||||
|
||||
<path to go_appengine>/dev_appserver.py $APPDIR
|
||||
|
||||
godoc should come up at http://localhost:8080 .
|
|
@ -0,0 +1,82 @@
|
|||
// 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.
|
||||
|
||||
// +build appengine
|
||||
|
||||
package main
|
||||
|
||||
// This file replaces main.go when running godoc under app-engine.
|
||||
// See README.godoc-app for details.
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"log"
|
||||
"net/http"
|
||||
"path"
|
||||
"regexp"
|
||||
|
||||
"golang.org/x/tools/godoc"
|
||||
"golang.org/x/tools/godoc/dl"
|
||||
"golang.org/x/tools/godoc/proxy"
|
||||
"golang.org/x/tools/godoc/short"
|
||||
"golang.org/x/tools/godoc/static"
|
||||
"golang.org/x/tools/godoc/vfs"
|
||||
"golang.org/x/tools/godoc/vfs/mapfs"
|
||||
"golang.org/x/tools/godoc/vfs/zipfs"
|
||||
|
||||
"google.golang.org/appengine"
|
||||
)
|
||||
|
||||
func init() {
|
||||
enforceHosts = !appengine.IsDevAppServer()
|
||||
playEnabled = true
|
||||
|
||||
log.Println("initializing godoc ...")
|
||||
log.Printf(".zip file = %s", zipFilename)
|
||||
log.Printf(".zip GOROOT = %s", zipGoroot)
|
||||
log.Printf("index files = %s", indexFilenames)
|
||||
|
||||
goroot := path.Join("/", zipGoroot) // fsHttp paths are relative to '/'
|
||||
|
||||
// read .zip file and set up file systems
|
||||
const zipfile = zipFilename
|
||||
rc, err := zip.OpenReader(zipfile)
|
||||
if err != nil {
|
||||
log.Fatalf("%s: %s\n", zipfile, err)
|
||||
}
|
||||
// rc is never closed (app running forever)
|
||||
fs.Bind("/", zipfs.New(rc, zipFilename), goroot, vfs.BindReplace)
|
||||
fs.Bind("/lib/godoc", mapfs.New(static.Files), "/", vfs.BindReplace)
|
||||
|
||||
corpus := godoc.NewCorpus(fs)
|
||||
corpus.Verbose = false
|
||||
corpus.MaxResults = 10000 // matches flag default in main.go
|
||||
corpus.IndexEnabled = true
|
||||
corpus.IndexFiles = indexFilenames
|
||||
if err := corpus.Init(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
corpus.IndexDirectory = indexDirectoryDefault
|
||||
go corpus.RunIndexer()
|
||||
|
||||
pres = godoc.NewPresentation(corpus)
|
||||
pres.TabWidth = 8
|
||||
pres.ShowPlayground = true
|
||||
pres.ShowExamples = true
|
||||
pres.DeclLinks = true
|
||||
pres.NotesRx = regexp.MustCompile("BUG")
|
||||
|
||||
readTemplates(pres, true)
|
||||
|
||||
mux := registerHandlers(pres)
|
||||
dl.RegisterHandlers(mux)
|
||||
short.RegisterHandlers(mux)
|
||||
|
||||
// Register /compile and /share handlers against the default serve mux
|
||||
// so that other app modules can make plain HTTP requests to those
|
||||
// hosts. (For reasons, HTTPS communication between modules is broken.)
|
||||
proxy.RegisterHandlers(http.DefaultServeMux)
|
||||
|
||||
log.Println("godoc initialization complete")
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
// 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.
|
||||
|
||||
// +build autocert
|
||||
|
||||
// This file adds automatic TLS certificate support (using
|
||||
// golang.org/x/crypto/acme/autocert), conditional on the use of the
|
||||
// autocert build tag. It sets the serveAutoCertHook func variable
|
||||
// non-nil. It is used by main.go.
|
||||
//
|
||||
// TODO: make this the default? We're in the Go 1.8 freeze now, so
|
||||
// this is too invasive to be default, but we want it for
|
||||
// https://beta.golang.org/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"flag"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
var (
|
||||
autoCertDirFlag = flag.String("autocert_cache_dir", "/var/cache/autocert", "Directory to cache TLS certs")
|
||||
autoCertHostFlag = flag.String("autocert_hostname", "", "optional hostname to require in autocert SNI requests")
|
||||
)
|
||||
|
||||
func init() {
|
||||
serveAutoCertHook = serveAutoCert
|
||||
}
|
||||
|
||||
func serveAutoCert(h http.Handler) error {
|
||||
m := autocert.Manager{
|
||||
Cache: autocert.DirCache(*autoCertDirFlag),
|
||||
Prompt: autocert.AcceptTOS,
|
||||
}
|
||||
if *autoCertHostFlag != "" {
|
||||
m.HostPolicy = autocert.HostWhitelist(*autoCertHostFlag)
|
||||
}
|
||||
srv := &http.Server{
|
||||
Handler: h,
|
||||
TLSConfig: &tls.Config{
|
||||
GetCertificate: m.GetCertificate,
|
||||
},
|
||||
IdleTimeout: 60 * time.Second,
|
||||
}
|
||||
http2.ConfigureServer(srv, &http2.Server{})
|
||||
ln, err := net.Listen("tcp", ":443")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return srv.Serve(tls.NewListener(tcpKeepAliveListener{ln.(*net.TCPListener)}, srv.TLSConfig))
|
||||
}
|
||||
|
||||
// tcpKeepAliveListener sets TCP keep-alive timeouts on accepted
|
||||
// connections. It's used by ListenAndServe and ListenAndServeTLS so
|
||||
// dead TCP connections (e.g. closing laptop mid-download) eventually
|
||||
// go away.
|
||||
type tcpKeepAliveListener struct {
|
||||
*net.TCPListener
|
||||
}
|
||||
|
||||
func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) {
|
||||
tc, err := ln.AcceptTCP()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
tc.SetKeepAlive(true)
|
||||
tc.SetKeepAlivePeriod(3 * time.Minute)
|
||||
return tc, nil
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
// 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 (
|
||||
"fmt"
|
||||
"go/build"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/tools/blog"
|
||||
"golang.org/x/tools/godoc/redirect"
|
||||
)
|
||||
|
||||
const (
|
||||
blogRepo = "golang.org/x/blog"
|
||||
blogURL = "http://blog.golang.org/"
|
||||
blogPath = "/blog/"
|
||||
)
|
||||
|
||||
var (
|
||||
blogServer http.Handler // set by blogInit
|
||||
blogInitOnce sync.Once
|
||||
playEnabled bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Initialize blog only when first accessed.
|
||||
http.HandleFunc(blogPath, func(w http.ResponseWriter, r *http.Request) {
|
||||
blogInitOnce.Do(blogInit)
|
||||
blogServer.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func blogInit() {
|
||||
// Binary distributions will include the blog content in "/blog".
|
||||
root := filepath.Join(runtime.GOROOT(), "blog")
|
||||
|
||||
// Prefer content from go.blog repository if present.
|
||||
if pkg, err := build.Import(blogRepo, "", build.FindOnly); err == nil {
|
||||
root = pkg.Dir
|
||||
}
|
||||
|
||||
// If content is not available fall back to redirect.
|
||||
if fi, err := os.Stat(root); err != nil || !fi.IsDir() {
|
||||
fmt.Fprintf(os.Stderr, "Blog content not available locally. "+
|
||||
"To install, run \n\tgo get %v\n", blogRepo)
|
||||
blogServer = http.HandlerFunc(blogRedirectHandler)
|
||||
return
|
||||
}
|
||||
|
||||
s, err := blog.NewServer(blog.Config{
|
||||
BaseURL: blogPath,
|
||||
BasePath: strings.TrimSuffix(blogPath, "/"),
|
||||
ContentPath: filepath.Join(root, "content"),
|
||||
TemplatePath: filepath.Join(root, "template"),
|
||||
HomeArticles: 5,
|
||||
PlayEnabled: playEnabled,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
blogServer = s
|
||||
}
|
||||
|
||||
func blogRedirectHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == blogPath {
|
||||
http.Redirect(w, r, blogURL, http.StatusFound)
|
||||
return
|
||||
}
|
||||
blogPrefixHandler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
var blogPrefixHandler = redirect.PrefixHandler(blogPath, blogURL)
|
|
@ -0,0 +1,523 @@
|
|||
// 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.
|
||||
|
||||
// The /doc/codewalk/ tree is synthesized from codewalk descriptions,
|
||||
// files named $GOROOT/doc/codewalk/*.xml.
|
||||
// For an example and a description of the format, see
|
||||
// http://golang.org/doc/codewalk/codewalk or run godoc -http=:6060
|
||||
// and see http://localhost:6060/doc/codewalk/codewalk .
|
||||
// That page is itself a codewalk; the source code for it is
|
||||
// $GOROOT/doc/codewalk/codewalk.xml.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
pathpkg "path"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
"unicode/utf8"
|
||||
|
||||
"golang.org/x/tools/godoc"
|
||||
"golang.org/x/tools/godoc/vfs"
|
||||
)
|
||||
|
||||
var codewalkHTML, codewalkdirHTML *template.Template
|
||||
|
||||
// Handler for /doc/codewalk/ and below.
|
||||
func codewalk(w http.ResponseWriter, r *http.Request) {
|
||||
relpath := r.URL.Path[len("/doc/codewalk/"):]
|
||||
abspath := r.URL.Path
|
||||
|
||||
r.ParseForm()
|
||||
if f := r.FormValue("fileprint"); f != "" {
|
||||
codewalkFileprint(w, r, f)
|
||||
return
|
||||
}
|
||||
|
||||
// If directory exists, serve list of code walks.
|
||||
dir, err := fs.Lstat(abspath)
|
||||
if err == nil && dir.IsDir() {
|
||||
codewalkDir(w, r, relpath, abspath)
|
||||
return
|
||||
}
|
||||
|
||||
// If file exists, serve using standard file server.
|
||||
if err == nil {
|
||||
pres.ServeFile(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise append .xml and hope to find
|
||||
// a codewalk description, but before trim
|
||||
// the trailing /.
|
||||
abspath = strings.TrimRight(abspath, "/")
|
||||
cw, err := loadCodewalk(abspath + ".xml")
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
pres.ServeError(w, r, relpath, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Canonicalize the path and redirect if changed
|
||||
if redir(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
pres.ServePage(w, godoc.Page{
|
||||
Title: "Codewalk: " + cw.Title,
|
||||
Tabtitle: cw.Title,
|
||||
Body: applyTemplate(codewalkHTML, "codewalk", cw),
|
||||
})
|
||||
}
|
||||
|
||||
func redir(w http.ResponseWriter, r *http.Request) (redirected bool) {
|
||||
canonical := pathpkg.Clean(r.URL.Path)
|
||||
if !strings.HasSuffix(canonical, "/") {
|
||||
canonical += "/"
|
||||
}
|
||||
if r.URL.Path != canonical {
|
||||
url := *r.URL
|
||||
url.Path = canonical
|
||||
http.Redirect(w, r, url.String(), http.StatusMovedPermanently)
|
||||
redirected = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func applyTemplate(t *template.Template, name string, data interface{}) []byte {
|
||||
var buf bytes.Buffer
|
||||
if err := t.Execute(&buf, data); err != nil {
|
||||
log.Printf("%s.Execute: %s", name, err)
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// A Codewalk represents a single codewalk read from an XML file.
|
||||
type Codewalk struct {
|
||||
Title string `xml:"title,attr"`
|
||||
File []string `xml:"file"`
|
||||
Step []*Codestep `xml:"step"`
|
||||
}
|
||||
|
||||
// A Codestep is a single step in a codewalk.
|
||||
type Codestep struct {
|
||||
// Filled in from XML
|
||||
Src string `xml:"src,attr"`
|
||||
Title string `xml:"title,attr"`
|
||||
XML string `xml:",innerxml"`
|
||||
|
||||
// Derived from Src; not in XML.
|
||||
Err error
|
||||
File string
|
||||
Lo int
|
||||
LoByte int
|
||||
Hi int
|
||||
HiByte int
|
||||
Data []byte
|
||||
}
|
||||
|
||||
// String method for printing in template.
|
||||
// Formats file address nicely.
|
||||
func (st *Codestep) String() string {
|
||||
s := st.File
|
||||
if st.Lo != 0 || st.Hi != 0 {
|
||||
s += fmt.Sprintf(":%d", st.Lo)
|
||||
if st.Lo != st.Hi {
|
||||
s += fmt.Sprintf(",%d", st.Hi)
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// loadCodewalk reads a codewalk from the named XML file.
|
||||
func loadCodewalk(filename string) (*Codewalk, error) {
|
||||
f, err := fs.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
cw := new(Codewalk)
|
||||
d := xml.NewDecoder(f)
|
||||
d.Entity = xml.HTMLEntity
|
||||
err = d.Decode(cw)
|
||||
if err != nil {
|
||||
return nil, &os.PathError{Op: "parsing", Path: filename, Err: err}
|
||||
}
|
||||
|
||||
// Compute file list, evaluate line numbers for addresses.
|
||||
m := make(map[string]bool)
|
||||
for _, st := range cw.Step {
|
||||
i := strings.Index(st.Src, ":")
|
||||
if i < 0 {
|
||||
i = len(st.Src)
|
||||
}
|
||||
filename := st.Src[0:i]
|
||||
data, err := vfs.ReadFile(fs, filename)
|
||||
if err != nil {
|
||||
st.Err = err
|
||||
continue
|
||||
}
|
||||
if i < len(st.Src) {
|
||||
lo, hi, err := addrToByteRange(st.Src[i+1:], 0, data)
|
||||
if err != nil {
|
||||
st.Err = err
|
||||
continue
|
||||
}
|
||||
// Expand match to line boundaries.
|
||||
for lo > 0 && data[lo-1] != '\n' {
|
||||
lo--
|
||||
}
|
||||
for hi < len(data) && (hi == 0 || data[hi-1] != '\n') {
|
||||
hi++
|
||||
}
|
||||
st.Lo = byteToLine(data, lo)
|
||||
st.Hi = byteToLine(data, hi-1)
|
||||
}
|
||||
st.Data = data
|
||||
st.File = filename
|
||||
m[filename] = true
|
||||
}
|
||||
|
||||
// Make list of files
|
||||
cw.File = make([]string, len(m))
|
||||
i := 0
|
||||
for f := range m {
|
||||
cw.File[i] = f
|
||||
i++
|
||||
}
|
||||
sort.Strings(cw.File)
|
||||
|
||||
return cw, nil
|
||||
}
|
||||
|
||||
// codewalkDir serves the codewalk directory listing.
|
||||
// It scans the directory for subdirectories or files named *.xml
|
||||
// and prepares a table.
|
||||
func codewalkDir(w http.ResponseWriter, r *http.Request, relpath, abspath string) {
|
||||
type elem struct {
|
||||
Name string
|
||||
Title string
|
||||
}
|
||||
|
||||
dir, err := fs.ReadDir(abspath)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
pres.ServeError(w, r, relpath, err)
|
||||
return
|
||||
}
|
||||
var v []interface{}
|
||||
for _, fi := range dir {
|
||||
name := fi.Name()
|
||||
if fi.IsDir() {
|
||||
v = append(v, &elem{name + "/", ""})
|
||||
} else if strings.HasSuffix(name, ".xml") {
|
||||
cw, err := loadCodewalk(abspath + "/" + name)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
v = append(v, &elem{name[0 : len(name)-len(".xml")], cw.Title})
|
||||
}
|
||||
}
|
||||
|
||||
pres.ServePage(w, godoc.Page{
|
||||
Title: "Codewalks",
|
||||
Body: applyTemplate(codewalkdirHTML, "codewalkdir", v),
|
||||
})
|
||||
}
|
||||
|
||||
// codewalkFileprint serves requests with ?fileprint=f&lo=lo&hi=hi.
|
||||
// The filename f has already been retrieved and is passed as an argument.
|
||||
// Lo and hi are the numbers of the first and last line to highlight
|
||||
// in the response. This format is used for the middle window pane
|
||||
// of the codewalk pages. It is a separate iframe and does not get
|
||||
// the usual godoc HTML wrapper.
|
||||
func codewalkFileprint(w http.ResponseWriter, r *http.Request, f string) {
|
||||
abspath := f
|
||||
data, err := vfs.ReadFile(fs, abspath)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
pres.ServeError(w, r, f, err)
|
||||
return
|
||||
}
|
||||
lo, _ := strconv.Atoi(r.FormValue("lo"))
|
||||
hi, _ := strconv.Atoi(r.FormValue("hi"))
|
||||
if hi < lo {
|
||||
hi = lo
|
||||
}
|
||||
lo = lineToByte(data, lo)
|
||||
hi = lineToByte(data, hi+1)
|
||||
|
||||
// Put the mark 4 lines before lo, so that the iframe
|
||||
// shows a few lines of context before the highlighted
|
||||
// section.
|
||||
n := 4
|
||||
mark := lo
|
||||
for ; mark > 0 && n > 0; mark-- {
|
||||
if data[mark-1] == '\n' {
|
||||
if n--; n == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
io.WriteString(w, `<style type="text/css">@import "/doc/codewalk/codewalk.css";</style><pre>`)
|
||||
template.HTMLEscape(w, data[0:mark])
|
||||
io.WriteString(w, "<a name='mark'></a>")
|
||||
template.HTMLEscape(w, data[mark:lo])
|
||||
if lo < hi {
|
||||
io.WriteString(w, "<div class='codewalkhighlight'>")
|
||||
template.HTMLEscape(w, data[lo:hi])
|
||||
io.WriteString(w, "</div>")
|
||||
}
|
||||
template.HTMLEscape(w, data[hi:])
|
||||
io.WriteString(w, "</pre>")
|
||||
}
|
||||
|
||||
// addrToByte evaluates the given address starting at offset start in data.
|
||||
// It returns the lo and hi byte offset of the matched region within data.
|
||||
// See http://plan9.bell-labs.com/sys/doc/sam/sam.html Table II
|
||||
// for details on the syntax.
|
||||
func addrToByteRange(addr string, start int, data []byte) (lo, hi int, err error) {
|
||||
var (
|
||||
dir byte
|
||||
prevc byte
|
||||
charOffset bool
|
||||
)
|
||||
lo = start
|
||||
hi = start
|
||||
for addr != "" && err == nil {
|
||||
c := addr[0]
|
||||
switch c {
|
||||
default:
|
||||
err = errors.New("invalid address syntax near " + string(c))
|
||||
case ',':
|
||||
if len(addr) == 1 {
|
||||
hi = len(data)
|
||||
} else {
|
||||
_, hi, err = addrToByteRange(addr[1:], hi, data)
|
||||
}
|
||||
return
|
||||
|
||||
case '+', '-':
|
||||
if prevc == '+' || prevc == '-' {
|
||||
lo, hi, err = addrNumber(data, lo, hi, prevc, 1, charOffset)
|
||||
}
|
||||
dir = c
|
||||
|
||||
case '$':
|
||||
lo = len(data)
|
||||
hi = len(data)
|
||||
if len(addr) > 1 {
|
||||
dir = '+'
|
||||
}
|
||||
|
||||
case '#':
|
||||
charOffset = true
|
||||
|
||||
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
|
||||
var i int
|
||||
for i = 1; i < len(addr); i++ {
|
||||
if addr[i] < '0' || addr[i] > '9' {
|
||||
break
|
||||
}
|
||||
}
|
||||
var n int
|
||||
n, err = strconv.Atoi(addr[0:i])
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
lo, hi, err = addrNumber(data, lo, hi, dir, n, charOffset)
|
||||
dir = 0
|
||||
charOffset = false
|
||||
prevc = c
|
||||
addr = addr[i:]
|
||||
continue
|
||||
|
||||
case '/':
|
||||
var i, j int
|
||||
Regexp:
|
||||
for i = 1; i < len(addr); i++ {
|
||||
switch addr[i] {
|
||||
case '\\':
|
||||
i++
|
||||
case '/':
|
||||
j = i + 1
|
||||
break Regexp
|
||||
}
|
||||
}
|
||||
if j == 0 {
|
||||
j = i
|
||||
}
|
||||
pattern := addr[1:i]
|
||||
lo, hi, err = addrRegexp(data, lo, hi, dir, pattern)
|
||||
prevc = c
|
||||
addr = addr[j:]
|
||||
continue
|
||||
}
|
||||
prevc = c
|
||||
addr = addr[1:]
|
||||
}
|
||||
|
||||
if err == nil && dir != 0 {
|
||||
lo, hi, err = addrNumber(data, lo, hi, dir, 1, charOffset)
|
||||
}
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return lo, hi, nil
|
||||
}
|
||||
|
||||
// addrNumber applies the given dir, n, and charOffset to the address lo, hi.
|
||||
// dir is '+' or '-', n is the count, and charOffset is true if the syntax
|
||||
// used was #n. Applying +n (or +#n) means to advance n lines
|
||||
// (or characters) after hi. Applying -n (or -#n) means to back up n lines
|
||||
// (or characters) before lo.
|
||||
// The return value is the new lo, hi.
|
||||
func addrNumber(data []byte, lo, hi int, dir byte, n int, charOffset bool) (int, int, error) {
|
||||
switch dir {
|
||||
case 0:
|
||||
lo = 0
|
||||
hi = 0
|
||||
fallthrough
|
||||
|
||||
case '+':
|
||||
if charOffset {
|
||||
pos := hi
|
||||
for ; n > 0 && pos < len(data); n-- {
|
||||
_, size := utf8.DecodeRune(data[pos:])
|
||||
pos += size
|
||||
}
|
||||
if n == 0 {
|
||||
return pos, pos, nil
|
||||
}
|
||||
break
|
||||
}
|
||||
// find next beginning of line
|
||||
if hi > 0 {
|
||||
for hi < len(data) && data[hi-1] != '\n' {
|
||||
hi++
|
||||
}
|
||||
}
|
||||
lo = hi
|
||||
if n == 0 {
|
||||
return lo, hi, nil
|
||||
}
|
||||
for ; hi < len(data); hi++ {
|
||||
if data[hi] != '\n' {
|
||||
continue
|
||||
}
|
||||
switch n--; n {
|
||||
case 1:
|
||||
lo = hi + 1
|
||||
case 0:
|
||||
return lo, hi + 1, nil
|
||||
}
|
||||
}
|
||||
|
||||
case '-':
|
||||
if charOffset {
|
||||
// Scan backward for bytes that are not UTF-8 continuation bytes.
|
||||
pos := lo
|
||||
for ; pos > 0 && n > 0; pos-- {
|
||||
if data[pos]&0xc0 != 0x80 {
|
||||
n--
|
||||
}
|
||||
}
|
||||
if n == 0 {
|
||||
return pos, pos, nil
|
||||
}
|
||||
break
|
||||
}
|
||||
// find earlier beginning of line
|
||||
for lo > 0 && data[lo-1] != '\n' {
|
||||
lo--
|
||||
}
|
||||
hi = lo
|
||||
if n == 0 {
|
||||
return lo, hi, nil
|
||||
}
|
||||
for ; lo >= 0; lo-- {
|
||||
if lo > 0 && data[lo-1] != '\n' {
|
||||
continue
|
||||
}
|
||||
switch n--; n {
|
||||
case 1:
|
||||
hi = lo
|
||||
case 0:
|
||||
return lo, hi, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0, 0, errors.New("address out of range")
|
||||
}
|
||||
|
||||
// addrRegexp searches for pattern in the given direction starting at lo, hi.
|
||||
// The direction dir is '+' (search forward from hi) or '-' (search backward from lo).
|
||||
// Backward searches are unimplemented.
|
||||
func addrRegexp(data []byte, lo, hi int, dir byte, pattern string) (int, int, error) {
|
||||
re, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
if dir == '-' {
|
||||
// Could implement reverse search using binary search
|
||||
// through file, but that seems like overkill.
|
||||
return 0, 0, errors.New("reverse search not implemented")
|
||||
}
|
||||
m := re.FindIndex(data[hi:])
|
||||
if len(m) > 0 {
|
||||
m[0] += hi
|
||||
m[1] += hi
|
||||
} else if hi > 0 {
|
||||
// No match. Wrap to beginning of data.
|
||||
m = re.FindIndex(data)
|
||||
}
|
||||
if len(m) == 0 {
|
||||
return 0, 0, errors.New("no match for " + pattern)
|
||||
}
|
||||
return m[0], m[1], nil
|
||||
}
|
||||
|
||||
// lineToByte returns the byte index of the first byte of line n.
|
||||
// Line numbers begin at 1.
|
||||
func lineToByte(data []byte, n int) int {
|
||||
if n <= 1 {
|
||||
return 0
|
||||
}
|
||||
n--
|
||||
for i, c := range data {
|
||||
if c == '\n' {
|
||||
if n--; n == 0 {
|
||||
return i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return len(data)
|
||||
}
|
||||
|
||||
// byteToLine returns the number of the line containing the byte at index i.
|
||||
func byteToLine(data []byte, i int) int {
|
||||
l := 1
|
||||
for j, c := range data {
|
||||
if j == i {
|
||||
return l
|
||||
}
|
||||
if c == '\n' {
|
||||
l++
|
||||
}
|
||||
}
|
||||
return l
|
||||
}
|
|
@ -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.
|
||||
|
||||
// +build !appengine
|
||||
|
||||
package main
|
||||
|
||||
import "net/http"
|
||||
|
||||
// Register a redirect handler for /dl/ to the golang.org download page.
|
||||
// This file will not be included when deploying godoc to golang.org.
|
||||
|
||||
func init() {
|
||||
http.Handle("/dl/", http.RedirectHandler("https://golang.org/dl/", http.StatusFound))
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
// 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 has two modes.
|
||||
|
||||
Without the -http flag, it runs in command-line mode and prints plain text
|
||||
documentation to standard output and exits. If both a library package and
|
||||
a command with the same name exists, using the prefix cmd/ will force
|
||||
documentation on the command rather than the library package. If the -src
|
||||
flag is specified, godoc prints the exported interface of a package in Go
|
||||
source form, or the implementation of a specific exported language entity:
|
||||
|
||||
godoc fmt # documentation for package fmt
|
||||
godoc fmt Printf # documentation for fmt.Printf
|
||||
godoc cmd/go # force documentation for the go command
|
||||
godoc -src fmt # fmt package interface in Go source form
|
||||
godoc -src fmt Printf # implementation of fmt.Printf
|
||||
|
||||
In command-line mode, the -q flag enables search queries against a godoc running
|
||||
as a webserver. If no explicit server address is specified with the -server flag,
|
||||
godoc first tries localhost:6060 and then http://golang.org.
|
||||
|
||||
godoc -q Reader
|
||||
godoc -q math.Sin
|
||||
godoc -server=:6060 -q sin
|
||||
|
||||
With the -http flag, it runs as a web server and presents the documentation as a
|
||||
web page.
|
||||
|
||||
godoc -http=:6060
|
||||
|
||||
Usage:
|
||||
|
||||
godoc [flag] package [name ...]
|
||||
|
||||
The flags are:
|
||||
|
||||
-v
|
||||
verbose mode
|
||||
-q
|
||||
arguments are considered search queries: a legal query is a
|
||||
single identifier (such as ToLower) or a qualified identifier
|
||||
(such as math.Sin)
|
||||
-src
|
||||
print (exported) source in command-line mode
|
||||
-tabwidth=4
|
||||
width of tabs in units of spaces
|
||||
-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)
|
||||
-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", ".*")
|
||||
-html
|
||||
print HTML in command-line mode
|
||||
-goroot=$GOROOT
|
||||
Go root directory
|
||||
-http=addr
|
||||
HTTP service address (e.g., '127.0.0.1:6060' or just ':6060')
|
||||
-server=addr
|
||||
webserver address for command line searches
|
||||
-analysis=type,pointer
|
||||
comma-separated list of analyses to perform
|
||||
"type": display identifier resolution, type info, method sets,
|
||||
'implements', and static callees
|
||||
"pointer": display channel peers, callers and dynamic callees
|
||||
(significantly slower)
|
||||
See http://golang.org/lib/godoc/analysis/help.html for details.
|
||||
-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 godoc runs as a web server and -index 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; in command-line mode you can
|
||||
set the GOOS/GOARCH environment variables to get output for the system specified.
|
||||
If -http was specified you can provide the URL parameters "GOOS" and "GOARCH"
|
||||
to set the output on the web page.
|
||||
|
||||
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 then the extracted documentation
|
||||
text present the page in textual (command-line) form rather than HTML
|
||||
flat present flat (not indented) directory listings using full paths
|
||||
|
||||
For instance, http://golang.org/pkg/math/big/?m=all,text shows the documentation
|
||||
for all (not just the exported) declarations of package big, in textual form (as
|
||||
it would appear when using godoc from the command line: "godoc -src math/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 http://golang.org/pkg/go/doc/#ToHTML for the exact rules.
|
||||
Godoc also shows example code that is runnable by the testing package;
|
||||
see http://golang.org/pkg/testing/#hdr-Examples for the conventions.
|
||||
See "Godoc: documenting Go code" for how to write good comments for godoc:
|
||||
http://golang.org/doc/articles/godoc_documenting_go_code.html
|
||||
|
||||
*/
|
||||
package main // import "golang.org/x/tools/cmd/godoc"
|
|
@ -0,0 +1,9 @@
|
|||
// 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.
|
||||
|
||||
// +build go1.9
|
||||
|
||||
package main_test
|
||||
|
||||
func init() { isGo19 = true }
|
|
@ -0,0 +1,489 @@
|
|||
// 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_test
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// buildGodoc builds the godoc executable.
|
||||
// It returns its path, and a cleanup function.
|
||||
//
|
||||
// TODO(adonovan): opt: do this at most once, and do the cleanup
|
||||
// exactly once. How though? There's no atexit.
|
||||
func buildGodoc(t *testing.T) (bin string, cleanup func()) {
|
||||
if runtime.GOARCH == "arm" {
|
||||
t.Skip("skipping test on arm platforms; too slow")
|
||||
}
|
||||
tmp, err := ioutil.TempDir("", "godoc-regtest-")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if cleanup == nil { // probably, go build failed.
|
||||
os.RemoveAll(tmp)
|
||||
}
|
||||
}()
|
||||
|
||||
bin = filepath.Join(tmp, "godoc")
|
||||
if runtime.GOOS == "windows" {
|
||||
bin += ".exe"
|
||||
}
|
||||
cmd := exec.Command("go", "build", "-o", bin)
|
||||
if err := cmd.Run(); err != nil {
|
||||
t.Fatalf("Building godoc: %v", err)
|
||||
}
|
||||
|
||||
return bin, func() { os.RemoveAll(tmp) }
|
||||
}
|
||||
|
||||
var isGo19 bool // godoc19_test.go sets it to true.
|
||||
|
||||
// Basic regression test for godoc command-line tool.
|
||||
func TestCLI(t *testing.T) {
|
||||
bin, cleanup := buildGodoc(t)
|
||||
defer cleanup()
|
||||
|
||||
// condStr returns s if cond is true, otherwise empty string.
|
||||
condStr := func(cond bool, s string) string {
|
||||
if !cond {
|
||||
return ""
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
args []string
|
||||
matches []string // regular expressions
|
||||
dontmatch []string // regular expressions
|
||||
}{
|
||||
{
|
||||
args: []string{"fmt"},
|
||||
matches: []string{
|
||||
`import "fmt"`,
|
||||
`Package fmt implements formatted I/O`,
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"io", "WriteString"},
|
||||
matches: []string{
|
||||
`func WriteString\(`,
|
||||
`WriteString writes the contents of the string s to w`,
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"nonexistingpkg"},
|
||||
matches: []string{
|
||||
`cannot find package` +
|
||||
// TODO: Remove this when support for Go 1.8 is dropped.
|
||||
condStr(!isGo19,
|
||||
// For Go 1.8 and older, because it doesn't have CL 33158 change applied to go/build.
|
||||
// The last pattern (does not e) is for plan9:
|
||||
// http://build.golang.org/log/2d8e5e14ed365bfa434b37ec0338cd9e6f8dd9bf
|
||||
`|no such file or directory|does not exist|cannot find the file|(?:' does not e)`),
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"fmt", "NonexistentSymbol"},
|
||||
matches: []string{
|
||||
`No match found\.`,
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"-src", "syscall", "Open"},
|
||||
matches: []string{
|
||||
`func Open\(`,
|
||||
},
|
||||
dontmatch: []string{
|
||||
`No match found\.`,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
cmd := exec.Command(bin, test.args...)
|
||||
cmd.Args[0] = "godoc"
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Errorf("Running with args %#v: %v", test.args, err)
|
||||
continue
|
||||
}
|
||||
for _, pat := range test.matches {
|
||||
re := regexp.MustCompile(pat)
|
||||
if !re.Match(out) {
|
||||
t.Errorf("godoc %v =\n%s\nwanted /%v/", strings.Join(test.args, " "), out, pat)
|
||||
}
|
||||
}
|
||||
for _, pat := range test.dontmatch {
|
||||
re := regexp.MustCompile(pat)
|
||||
if re.Match(out) {
|
||||
t.Errorf("godoc %v =\n%s\ndid not want /%v/", strings.Join(test.args, " "), out, pat)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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, addr string) {
|
||||
waitForServer(t,
|
||||
fmt.Sprintf("http://%v/", addr),
|
||||
"The Go Programming Language",
|
||||
15*time.Second,
|
||||
false)
|
||||
}
|
||||
|
||||
func waitForSearchReady(t *testing.T, addr string) {
|
||||
waitForServer(t,
|
||||
fmt.Sprintf("http://%v/search?q=FALLTHROUGH", addr),
|
||||
"The list of tokens.",
|
||||
2*time.Minute,
|
||||
false)
|
||||
}
|
||||
|
||||
func waitUntilScanComplete(t *testing.T, addr string) {
|
||||
waitForServer(t,
|
||||
fmt.Sprintf("http://%v/pkg", addr),
|
||||
"Scan is not yet complete",
|
||||
2*time.Minute,
|
||||
true,
|
||||
)
|
||||
// setting reverse as true, which means this waits
|
||||
// until the string is not returned in the response anymore
|
||||
}
|
||||
|
||||
const pollInterval = 200 * time.Millisecond
|
||||
|
||||
func waitForServer(t *testing.T, url, match string, timeout time.Duration, reverse bool) {
|
||||
// "health check" duplicated from x/tools/cmd/tipgodoc/tip.go
|
||||
deadline := time.Now().Add(timeout)
|
||||
for time.Now().Before(deadline) {
|
||||
time.Sleep(pollInterval)
|
||||
res, err := http.Get(url)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
rbody, err := ioutil.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
if err == nil && res.StatusCode == http.StatusOK {
|
||||
if bytes.Contains(rbody, []byte(match)) && !reverse {
|
||||
return
|
||||
}
|
||||
if !bytes.Contains(rbody, []byte(match)) && reverse {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
t.Fatalf("Server failed to respond in %v", timeout)
|
||||
}
|
||||
|
||||
func killAndWait(cmd *exec.Cmd) {
|
||||
cmd.Process.Kill()
|
||||
cmd.Wait()
|
||||
}
|
||||
|
||||
// Basic integration test for godoc HTTP interface.
|
||||
func TestWeb(t *testing.T) {
|
||||
testWeb(t, false)
|
||||
}
|
||||
|
||||
// Basic integration test for godoc HTTP interface.
|
||||
func TestWebIndex(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in -short mode")
|
||||
}
|
||||
testWeb(t, true)
|
||||
}
|
||||
|
||||
// Basic integration test for godoc HTTP interface.
|
||||
func testWeb(t *testing.T, withIndex bool) {
|
||||
if runtime.GOOS == "plan9" {
|
||||
t.Skip("skipping on plan9; files to start up quickly enough")
|
||||
}
|
||||
bin, cleanup := buildGodoc(t)
|
||||
defer cleanup()
|
||||
addr := serverAddress(t)
|
||||
args := []string{fmt.Sprintf("-http=%s", addr)}
|
||||
if withIndex {
|
||||
args = append(args, "-index", "-index_interval=-1s")
|
||||
}
|
||||
cmd := exec.Command(bin, args...)
|
||||
cmd.Stdout = os.Stderr
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Args[0] = "godoc"
|
||||
cmd.Env = godocEnv()
|
||||
if err := cmd.Start(); err != nil {
|
||||
t.Fatalf("failed to start godoc: %s", err)
|
||||
}
|
||||
defer killAndWait(cmd)
|
||||
|
||||
if withIndex {
|
||||
waitForSearchReady(t, addr)
|
||||
} else {
|
||||
waitForServerReady(t, addr)
|
||||
waitUntilScanComplete(t, addr)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
path string
|
||||
match []string
|
||||
dontmatch []string
|
||||
needIndex bool
|
||||
}{
|
||||
{
|
||||
path: "/",
|
||||
match: []string{"Go is an open source programming language"},
|
||||
},
|
||||
{
|
||||
path: "/pkg/fmt/",
|
||||
match: []string{"Package fmt implements formatted I/O"},
|
||||
},
|
||||
{
|
||||
path: "/src/fmt/",
|
||||
match: []string{"scan_test.go"},
|
||||
},
|
||||
{
|
||||
path: "/src/fmt/print.go",
|
||||
match: []string{"// Println formats using"},
|
||||
},
|
||||
{
|
||||
path: "/pkg",
|
||||
match: []string{
|
||||
"Standard library",
|
||||
"Package fmt implements formatted I/O",
|
||||
},
|
||||
dontmatch: []string{
|
||||
"internal/syscall",
|
||||
"cmd/gc",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/pkg/?m=all",
|
||||
match: []string{
|
||||
"Standard library",
|
||||
"Package fmt implements formatted I/O",
|
||||
"internal/syscall/?m=all",
|
||||
},
|
||||
dontmatch: []string{
|
||||
"cmd/gc",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/search?q=ListenAndServe",
|
||||
match: []string{
|
||||
"/src",
|
||||
},
|
||||
dontmatch: []string{
|
||||
"/pkg/bootstrap",
|
||||
},
|
||||
needIndex: true,
|
||||
},
|
||||
{
|
||||
path: "/pkg/strings/",
|
||||
match: []string{
|
||||
`href="/src/strings/strings.go"`,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/cmd/compile/internal/amd64/",
|
||||
match: []string{
|
||||
`href="/src/cmd/compile/internal/amd64/ssa.go"`,
|
||||
},
|
||||
},
|
||||
}
|
||||
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 := ioutil.ReadAll(resp.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.match {
|
||||
if !bytes.Contains(body, []byte(substr)) {
|
||||
t.Errorf("GET %s: wanted substring %q in body", url, substr)
|
||||
isErr = true
|
||||
}
|
||||
}
|
||||
for _, substr := range test.dontmatch {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Basic integration test for godoc -analysis=type (via HTTP interface).
|
||||
func TestTypeAnalysis(t *testing.T) {
|
||||
if runtime.GOOS == "plan9" {
|
||||
t.Skip("skipping test on plan9 (issue #11974)") // see comment re: Plan 9 below
|
||||
}
|
||||
|
||||
// Write a fake GOROOT/GOPATH.
|
||||
tmpdir, err := ioutil.TempDir("", "godoc-analysis")
|
||||
if err != nil {
|
||||
t.Fatalf("ioutil.TempDir failed: %s", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpdir)
|
||||
for _, f := range []struct{ file, content string }{
|
||||
{"goroot/src/lib/lib.go", `
|
||||
package lib
|
||||
type T struct{}
|
||||
const C = 3
|
||||
var V T
|
||||
func (T) F() int { return C }
|
||||
`},
|
||||
{"gopath/src/app/main.go", `
|
||||
package main
|
||||
import "lib"
|
||||
func main() { print(lib.V) }
|
||||
`},
|
||||
} {
|
||||
file := filepath.Join(tmpdir, f.file)
|
||||
if err := os.MkdirAll(filepath.Dir(file), 0755); err != nil {
|
||||
t.Fatalf("MkdirAll(%s) failed: %s", filepath.Dir(file), err)
|
||||
}
|
||||
if err := ioutil.WriteFile(file, []byte(f.content), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Start the server.
|
||||
bin, cleanup := buildGodoc(t)
|
||||
defer cleanup()
|
||||
addr := serverAddress(t)
|
||||
cmd := exec.Command(bin, fmt.Sprintf("-http=%s", addr), "-analysis=type")
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("GOROOT=%s", filepath.Join(tmpdir, "goroot")))
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("GOPATH=%s", filepath.Join(tmpdir, "gopath")))
|
||||
for _, e := range os.Environ() {
|
||||
if strings.HasPrefix(e, "GOROOT=") || strings.HasPrefix(e, "GOPATH=") {
|
||||
continue
|
||||
}
|
||||
cmd.Env = append(cmd.Env, e)
|
||||
}
|
||||
cmd.Stdout = os.Stderr
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmd.Args[0] = "godoc"
|
||||
if err := cmd.Start(); err != nil {
|
||||
t.Fatalf("failed to start godoc: %s", err)
|
||||
}
|
||||
defer killAndWait(cmd)
|
||||
waitForServerReady(t, addr)
|
||||
|
||||
// Wait for type analysis to complete.
|
||||
reader := bufio.NewReader(stderr)
|
||||
for {
|
||||
s, err := reader.ReadString('\n') // on Plan 9 this fails
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fmt.Fprint(os.Stderr, s)
|
||||
if strings.Contains(s, "Type analysis complete.") {
|
||||
break
|
||||
}
|
||||
}
|
||||
go io.Copy(os.Stderr, reader)
|
||||
|
||||
t0 := time.Now()
|
||||
|
||||
// Make an HTTP request and check for a regular expression match.
|
||||
// The patterns are very crude checks that basic type information
|
||||
// has been annotated onto the source view.
|
||||
tryagain:
|
||||
for _, test := range []struct{ url, pattern string }{
|
||||
{"/src/lib/lib.go", "L2.*package .*Package docs for lib.*/lib"},
|
||||
{"/src/lib/lib.go", "L3.*type .*type info for T.*struct"},
|
||||
{"/src/lib/lib.go", "L5.*var V .*type T struct"},
|
||||
{"/src/lib/lib.go", "L6.*func .*type T struct.*T.*return .*const C untyped int.*C"},
|
||||
|
||||
{"/src/app/main.go", "L2.*package .*Package docs for app"},
|
||||
{"/src/app/main.go", "L3.*import .*Package docs for lib.*lib"},
|
||||
{"/src/app/main.go", "L4.*func main.*package lib.*lib.*var lib.V lib.T.*V"},
|
||||
} {
|
||||
url := fmt.Sprintf("http://%s%s", addr, test.url)
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
t.Errorf("GET %s failed: %s", url, err)
|
||||
continue
|
||||
}
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
t.Errorf("GET %s: failed to read body: %s (response: %v)", url, err, resp)
|
||||
continue
|
||||
}
|
||||
|
||||
if !bytes.Contains(body, []byte("Static analysis features")) {
|
||||
// Type analysis results usually become available within
|
||||
// ~4ms after godoc startup (for this input on my machine).
|
||||
if elapsed := time.Since(t0); elapsed > 500*time.Millisecond {
|
||||
t.Fatalf("type analysis results still unavailable after %s", elapsed)
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
goto tryagain
|
||||
}
|
||||
|
||||
match, err := regexp.Match(test.pattern, body)
|
||||
if err != nil {
|
||||
t.Errorf("regexp.Match(%q) failed: %s", test.pattern, err)
|
||||
continue
|
||||
}
|
||||
if !match {
|
||||
// This is a really ugly failure message.
|
||||
t.Errorf("GET %s: body doesn't match %q, got:\n%s",
|
||||
url, test.pattern, string(body))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// godocEnv returns the process environment without the GOPATH variable.
|
||||
// (We don't want the indexer looking at the local workspace during tests.)
|
||||
func godocEnv() (env []string) {
|
||||
for _, v := range os.Environ() {
|
||||
if strings.HasPrefix(v, "GOPATH=") {
|
||||
continue
|
||||
}
|
||||
env = append(env, v)
|
||||
}
|
||||
return
|
||||
}
|
|
@ -0,0 +1,148 @@
|
|||
// 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.
|
||||
|
||||
// The /doc/codewalk/ tree is synthesized from codewalk descriptions,
|
||||
// files named $GOROOT/doc/codewalk/*.xml.
|
||||
// For an example and a description of the format, see
|
||||
// http://golang.org/doc/codewalk/codewalk or run godoc -http=:6060
|
||||
// and see http://localhost:6060/doc/codewalk/codewalk .
|
||||
// That page is itself a codewalk; the source code for it is
|
||||
// $GOROOT/doc/codewalk/codewalk.xml.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"go/format"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"golang.org/x/tools/godoc"
|
||||
"golang.org/x/tools/godoc/redirect"
|
||||
"golang.org/x/tools/godoc/vfs"
|
||||
)
|
||||
|
||||
var (
|
||||
pres *godoc.Presentation
|
||||
fs = vfs.NameSpace{}
|
||||
)
|
||||
|
||||
var enforceHosts = false // set true in production on app engine
|
||||
|
||||
// hostEnforcerHandler redirects requests to "http://foo.golang.org/bar"
|
||||
// to "https://golang.org/bar".
|
||||
// It permits requests to the host "godoc-test.golang.org" for testing and
|
||||
// golang.google.cn for Chinese users.
|
||||
type hostEnforcerHandler struct {
|
||||
h http.Handler
|
||||
}
|
||||
|
||||
func (h hostEnforcerHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if !enforceHosts {
|
||||
h.h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
if r.TLS == nil || !h.validHost(r.Host) {
|
||||
r.URL.Scheme = "https"
|
||||
if h.validHost(r.Host) {
|
||||
r.URL.Host = r.Host
|
||||
} else {
|
||||
r.URL.Host = "golang.org"
|
||||
}
|
||||
http.Redirect(w, r, r.URL.String(), http.StatusFound)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Strict-Transport-Security", "max-age=31536000; preload")
|
||||
h.h.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (h hostEnforcerHandler) validHost(host string) bool {
|
||||
switch strings.ToLower(host) {
|
||||
case "golang.org", "godoc-test.golang.org", "golang.google.cn":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func registerHandlers(pres *godoc.Presentation) *http.ServeMux {
|
||||
if pres == nil {
|
||||
panic("nil Presentation")
|
||||
}
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/doc/codewalk/", codewalk)
|
||||
mux.Handle("/doc/play/", pres.FileServer())
|
||||
mux.Handle("/robots.txt", pres.FileServer())
|
||||
mux.Handle("/", pres)
|
||||
mux.Handle("/pkg/C/", redirect.Handler("/cmd/cgo/"))
|
||||
mux.HandleFunc("/fmt", fmtHandler)
|
||||
redirect.Register(mux)
|
||||
|
||||
http.Handle("/", hostEnforcerHandler{mux})
|
||||
|
||||
return 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, html bool) {
|
||||
p.PackageText = readTemplate("package.txt")
|
||||
p.SearchText = readTemplate("search.txt")
|
||||
|
||||
if html || p.HTMLMode {
|
||||
codewalkHTML = readTemplate("codewalk.html")
|
||||
codewalkdirHTML = readTemplate("codewalkdir.html")
|
||||
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.SearchHTML = readTemplate("search.html")
|
||||
p.SearchDocHTML = readTemplate("searchdoc.html")
|
||||
p.SearchCodeHTML = readTemplate("searchcode.html")
|
||||
p.SearchTxtHTML = readTemplate("searchtxt.html")
|
||||
p.SearchDescXML = readTemplate("opensearch.xml")
|
||||
}
|
||||
}
|
||||
|
||||
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,11 @@
|
|||
// 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 "strings"
|
||||
|
||||
func indexDirectoryDefault(dir string) bool {
|
||||
return dir != "/pkg" && !strings.HasPrefix(dir, "/pkg/")
|
||||
}
|
|
@ -0,0 +1,357 @@
|
|||
// 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/ main landing page
|
||||
// http://godoc/doc/ serve from $GOROOT/doc - spec, mem, etc.
|
||||
// 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)
|
||||
//
|
||||
// Command-line interface:
|
||||
//
|
||||
// godoc packagepath [name ...]
|
||||
//
|
||||
// godoc compress/zlib
|
||||
// - prints doc for package compress/zlib
|
||||
// godoc crypto/block Cipher NewCMAC
|
||||
// - prints doc for Cipher and NewCMAC in package crypto/block
|
||||
|
||||
// +build !appengine
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
_ "expvar" // to serve /debug/vars
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/build"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
_ "net/http/pprof" // to serve /debug/pprof/*
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/tools/godoc"
|
||||
"golang.org/x/tools/godoc/analysis"
|
||||
"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"
|
||||
)
|
||||
|
||||
const defaultAddr = ":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")
|
||||
|
||||
analysisFlag = flag.String("analysis", "", `comma-separated list of analyses to perform (supported: type, pointer). See http://golang.org/lib/godoc/analysis/help.html`)
|
||||
|
||||
// network
|
||||
httpAddr = flag.String("http", "", "HTTP service address (e.g., '"+defaultAddr+"')")
|
||||
serverAddr = flag.String("server", "", "webserver address for command line searches")
|
||||
|
||||
// layout control
|
||||
html = flag.Bool("html", false, "print HTML in command-line mode")
|
||||
srcMode = flag.Bool("src", false, "print (exported) source in command-line mode")
|
||||
urlFlag = flag.String("url", "", "print HTML for named URL")
|
||||
|
||||
// command-line searches
|
||||
query = flag.Bool("q", false, "arguments are considered search queries")
|
||||
|
||||
verbose = flag.Bool("v", false, "verbose mode")
|
||||
|
||||
// file system roots
|
||||
// TODO(gri) consider the invariant that goroot always end in '/'
|
||||
goroot = flag.String("goroot", runtime.GOROOT(), "Go root directory")
|
||||
|
||||
// layout control
|
||||
tabWidth = flag.Int("tabwidth", 4, "tab width")
|
||||
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 in web interface")
|
||||
showExamples = flag.Bool("ex", false, "show examples in command line mode")
|
||||
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")
|
||||
)
|
||||
|
||||
func usage() {
|
||||
fmt.Fprintf(os.Stderr,
|
||||
"usage: godoc package [name ...]\n"+
|
||||
" 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 := httptest.NewRecorder()
|
||||
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.HeaderMap.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()
|
||||
|
||||
playEnabled = *showPlayground
|
||||
|
||||
// Check usage: server and no args.
|
||||
if (*httpAddr != "" || *urlFlag != "") && (flag.NArg() > 0) {
|
||||
fmt.Fprintln(os.Stderr, "can't use -http with args.")
|
||||
usage()
|
||||
}
|
||||
|
||||
// Check usage: command line args or index creation mode.
|
||||
if (*httpAddr != "" || *urlFlag != "") != (flag.NArg() == 0) && !*writeIndex {
|
||||
fmt.Fprintln(os.Stderr, "missing args.")
|
||||
usage()
|
||||
}
|
||||
|
||||
var fsGate chan bool
|
||||
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)
|
||||
} else {
|
||||
fs.Bind("/lib/godoc", mapfs.New(static.Files), "/", vfs.BindReplace)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
httpMode := *httpAddr != ""
|
||||
|
||||
var typeAnalysis, pointerAnalysis bool
|
||||
if *analysisFlag != "" {
|
||||
for _, a := range strings.Split(*analysisFlag, ",") {
|
||||
switch a {
|
||||
case "type":
|
||||
typeAnalysis = true
|
||||
case "pointer":
|
||||
pointerAnalysis = true
|
||||
default:
|
||||
log.Fatalf("unknown analysis: %s", a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
corpus := godoc.NewCorpus(fs)
|
||||
corpus.Verbose = *verbose
|
||||
corpus.MaxResults = *maxResults
|
||||
corpus.IndexEnabled = *indexEnabled && httpMode
|
||||
if *maxResults == 0 {
|
||||
corpus.IndexFullText = false
|
||||
}
|
||||
corpus.IndexFiles = *indexFiles
|
||||
corpus.IndexDirectory = indexDirectoryDefault
|
||||
corpus.IndexThrottle = *indexThrottle
|
||||
corpus.IndexInterval = *indexInterval
|
||||
if *writeIndex {
|
||||
corpus.IndexThrottle = 1.0
|
||||
corpus.IndexEnabled = true
|
||||
}
|
||||
if *writeIndex || httpMode || *urlFlag != "" {
|
||||
if httpMode {
|
||||
go initCorpus(corpus)
|
||||
} else {
|
||||
initCorpus(corpus)
|
||||
}
|
||||
}
|
||||
|
||||
pres = godoc.NewPresentation(corpus)
|
||||
pres.TabWidth = *tabWidth
|
||||
pres.ShowTimestamps = *showTimestamps
|
||||
pres.ShowPlayground = *showPlayground
|
||||
pres.ShowExamples = *showExamples
|
||||
pres.DeclLinks = *declLinks
|
||||
pres.SrcMode = *srcMode
|
||||
pres.HTMLMode = *html
|
||||
if *notesRx != "" {
|
||||
pres.NotesRx = regexp.MustCompile(*notesRx)
|
||||
}
|
||||
|
||||
readTemplates(pres, httpMode || *urlFlag != "")
|
||||
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
|
||||
}
|
||||
|
||||
if httpMode {
|
||||
// HTTP server mode.
|
||||
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)
|
||||
log.Printf("tabwidth = %d", *tabWidth)
|
||||
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 type/pointer analysis.
|
||||
if typeAnalysis || pointerAnalysis {
|
||||
go analysis.Run(pointerAnalysis, &corpus.Analysis)
|
||||
}
|
||||
|
||||
if serveAutoCertHook != nil {
|
||||
go func() {
|
||||
if err := serveAutoCertHook(handler); err != nil {
|
||||
log.Fatalf("ListenAndServe TLS: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if *query {
|
||||
handleRemoteSearch()
|
||||
return
|
||||
}
|
||||
|
||||
if err := godoc.CommandLine(os.Stdout, fs, pres, flag.Args()); err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
}
|
||||
|
||||
// serveAutoCertHook if non-nil specifies a function to listen on port 443.
|
||||
// See autocert.go.
|
||||
var serveAutoCertHook func(http.Handler) error
|
|
@ -0,0 +1,11 @@
|
|||
// Copyright 2012 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !appengine
|
||||
|
||||
package main
|
||||
|
||||
// This package registers "/compile" and "/share" handlers
|
||||
// that redirect to the golang.org playground.
|
||||
import _ "golang.org/x/tools/playground"
|
|
@ -0,0 +1,72 @@
|
|||
// 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.
|
||||
|
||||
// +build !appengine
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
)
|
||||
|
||||
func handleRemoteSearch() {
|
||||
// Command-line queries.
|
||||
for i := 0; i < flag.NArg(); i++ {
|
||||
res, err := remoteSearch(flag.Arg(i))
|
||||
if err != nil {
|
||||
log.Fatalf("remoteSearch: %s", err)
|
||||
}
|
||||
io.Copy(os.Stdout, res.Body)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// remoteSearchURL returns the search URL for a given query as needed by
|
||||
// remoteSearch. If html is set, an html result is requested; otherwise
|
||||
// the result is in textual form.
|
||||
// Adjust this function as necessary if modeNames or FormValue parameters
|
||||
// change.
|
||||
func remoteSearchURL(query string, html bool) string {
|
||||
s := "/search?m=text&q="
|
||||
if html {
|
||||
s = "/search?q="
|
||||
}
|
||||
return s + url.QueryEscape(query)
|
||||
}
|
||||
|
||||
func remoteSearch(query string) (res *http.Response, err error) {
|
||||
// list of addresses to try
|
||||
var addrs []string
|
||||
if *serverAddr != "" {
|
||||
// explicit server address - only try this one
|
||||
addrs = []string{*serverAddr}
|
||||
} else {
|
||||
addrs = []string{
|
||||
defaultAddr,
|
||||
"golang.org",
|
||||
}
|
||||
}
|
||||
|
||||
// remote search
|
||||
search := remoteSearchURL(query, *html)
|
||||
for _, addr := range addrs {
|
||||
url := "http://" + addr + search
|
||||
res, err = http.Get(url)
|
||||
if err == nil && res.StatusCode == http.StatusOK {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err == nil && res.StatusCode != http.StatusOK {
|
||||
err = errors.New(res.Status)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# 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.
|
||||
|
||||
# This script creates a complete godoc app in $APPDIR.
|
||||
# It copies the cmd/godoc and src/go/... sources from GOROOT,
|
||||
# synthesizes an app.yaml file, and creates the .zip, index, and
|
||||
# configuration files.
|
||||
#
|
||||
# If an argument is provided it is assumed to be the app-engine godoc directory.
|
||||
# Without an argument, $APPDIR is used instead. If GOROOT is not set, "go env"
|
||||
# is consulted to find the $GOROOT.
|
||||
#
|
||||
# The script creates a .zip file representing the $GOROOT file system
|
||||
# and computes the correspondig search index files. These files are then
|
||||
# copied to $APPDIR. A corresponding godoc configuration file is created
|
||||
# in $APPDIR/appconfig.go.
|
||||
|
||||
ZIPFILE=godoc.zip
|
||||
INDEXFILE=godoc.index
|
||||
SPLITFILES=index.split.
|
||||
GODOC=golang.org/x/tools/cmd/godoc
|
||||
CONFIGFILE=$GODOC/appconfig.go
|
||||
|
||||
error() {
|
||||
echo "error: $1"
|
||||
exit 2
|
||||
}
|
||||
|
||||
getArgs() {
|
||||
if [ -z $APPENGINE_SDK ]; then
|
||||
error "APPENGINE_SDK environment variable not set"
|
||||
fi
|
||||
if [ ! -x $APPENGINE_SDK/goapp ]; then
|
||||
error "couldn't find goapp command in $APPENGINE_SDK"
|
||||
fi
|
||||
if [ -z $GOROOT ]; then
|
||||
GOROOT=$(go env GOROOT)
|
||||
echo "GOROOT not set explicitly, using go env value instead"
|
||||
fi
|
||||
if [ -z $APPDIR ]; then
|
||||
if [ $# == 0 ]; then
|
||||
error "APPDIR not set, and no argument provided"
|
||||
fi
|
||||
APPDIR=$1
|
||||
echo "APPDIR not set, using argument instead"
|
||||
fi
|
||||
|
||||
# safety checks
|
||||
if [ ! -d $GOROOT ]; then
|
||||
error "$GOROOT is not a directory"
|
||||
fi
|
||||
if [ -e $APPDIR ]; then
|
||||
error "$APPDIR exists; check and remove it before trying again"
|
||||
fi
|
||||
|
||||
# reporting
|
||||
echo "GOROOT = $GOROOT"
|
||||
echo "APPDIR = $APPDIR"
|
||||
}
|
||||
|
||||
fetchGodoc() {
|
||||
echo "*** Fetching godoc (if not already in GOPATH)"
|
||||
unset GOBIN
|
||||
go=$APPENGINE_SDK/goapp
|
||||
$go get -d -tags appengine $GODOC
|
||||
mkdir -p $APPDIR/$GODOC
|
||||
cp $(find $($go list -f '{{.Dir}}' $GODOC) -mindepth 1 -maxdepth 1 -type f) $APPDIR/$GODOC/
|
||||
}
|
||||
|
||||
makeAppYaml() {
|
||||
echo "*** make $APPDIR/app.yaml"
|
||||
cat > $APPDIR/app.yaml <<EOF
|
||||
application: godoc
|
||||
version: 1
|
||||
runtime: go
|
||||
api_version: go1
|
||||
|
||||
handlers:
|
||||
- url: /.*
|
||||
script: _go_app
|
||||
EOF
|
||||
}
|
||||
|
||||
makeZipfile() {
|
||||
echo "*** make $APPDIR/$ZIPFILE"
|
||||
zip -q -r $APPDIR/$ZIPFILE $GOROOT/*
|
||||
}
|
||||
|
||||
makeIndexfile() {
|
||||
echo "*** make $APPDIR/$INDEXFILE"
|
||||
GOPATH= godoc -write_index -index_files=$APPDIR/$INDEXFILE -zip=$APPDIR/$ZIPFILE
|
||||
}
|
||||
|
||||
splitIndexfile() {
|
||||
echo "*** split $APPDIR/$INDEXFILE"
|
||||
split -b8m $APPDIR/$INDEXFILE $APPDIR/$SPLITFILES
|
||||
}
|
||||
|
||||
makeConfigfile() {
|
||||
echo "*** make $APPDIR/$CONFIGFILE"
|
||||
cat > $APPDIR/$CONFIGFILE <<EOF
|
||||
package main
|
||||
|
||||
// GENERATED FILE - DO NOT MODIFY BY HAND.
|
||||
// (generated by golang.org/x/tools/cmd/godoc/setup-godoc-app.bash)
|
||||
|
||||
const (
|
||||
// .zip filename
|
||||
zipFilename = "$ZIPFILE"
|
||||
|
||||
// goroot directory in .zip file
|
||||
zipGoroot = "$GOROOT"
|
||||
|
||||
// glob pattern describing search index files
|
||||
// (if empty, the index is built at run-time)
|
||||
indexFilenames = "$SPLITFILES*"
|
||||
)
|
||||
EOF
|
||||
}
|
||||
|
||||
getArgs "$@"
|
||||
set -e
|
||||
mkdir $APPDIR
|
||||
fetchGodoc
|
||||
makeAppYaml
|
||||
makeZipfile
|
||||
makeIndexfile
|
||||
splitIndexfile
|
||||
makeConfigfile
|
||||
|
||||
echo "*** setup complete"
|
|
@ -0,0 +1,92 @@
|
|||
// 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 file contains the handlers that serve go-import redirects for Go
|
||||
// sub-repositories. It specifies the mapping from import paths like
|
||||
// "golang.org/x/tools" to the actual repository locations.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const xPrefix = "/x/"
|
||||
|
||||
type xRepo struct {
|
||||
URL, VCS string
|
||||
}
|
||||
|
||||
var xMap = map[string]xRepo{
|
||||
"codereview": {"https://code.google.com/p/go.codereview", "hg"},
|
||||
|
||||
"arch": {"https://go.googlesource.com/arch", "git"},
|
||||
"benchmarks": {"https://go.googlesource.com/benchmarks", "git"},
|
||||
"blog": {"https://go.googlesource.com/blog", "git"},
|
||||
"build": {"https://go.googlesource.com/build", "git"},
|
||||
"crypto": {"https://go.googlesource.com/crypto", "git"},
|
||||
"debug": {"https://go.googlesource.com/debug", "git"},
|
||||
"exp": {"https://go.googlesource.com/exp", "git"},
|
||||
"image": {"https://go.googlesource.com/image", "git"},
|
||||
"lint": {"https://go.googlesource.com/lint", "git"},
|
||||
"mobile": {"https://go.googlesource.com/mobile", "git"},
|
||||
"net": {"https://go.googlesource.com/net", "git"},
|
||||
"oauth2": {"https://go.googlesource.com/oauth2", "git"},
|
||||
"perf": {"https://go.googlesource.com/perf", "git"},
|
||||
"playground": {"https://go.googlesource.com/playground", "git"},
|
||||
"review": {"https://go.googlesource.com/review", "git"},
|
||||
"sync": {"https://go.googlesource.com/sync", "git"},
|
||||
"sys": {"https://go.googlesource.com/sys", "git"},
|
||||
"talks": {"https://go.googlesource.com/talks", "git"},
|
||||
"term": {"https://go.googlesource.com/term", "git"},
|
||||
"text": {"https://go.googlesource.com/text", "git"},
|
||||
"time": {"https://go.googlesource.com/time", "git"},
|
||||
"tools": {"https://go.googlesource.com/tools", "git"},
|
||||
"tour": {"https://go.googlesource.com/tour", "git"},
|
||||
"vgo": {"https://go.googlesource.com/vgo", "git"},
|
||||
}
|
||||
|
||||
func init() {
|
||||
http.HandleFunc(xPrefix, xHandler)
|
||||
}
|
||||
|
||||
func xHandler(w http.ResponseWriter, r *http.Request) {
|
||||
head, tail := strings.TrimPrefix(r.URL.Path, xPrefix), ""
|
||||
if i := strings.Index(head, "/"); i != -1 {
|
||||
head, tail = head[:i], head[i:]
|
||||
}
|
||||
if head == "" {
|
||||
http.Redirect(w, r, "https://godoc.org/-/subrepo", http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
repo, ok := xMap[head]
|
||||
if !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
data := struct {
|
||||
Prefix, Head, Tail string
|
||||
Repo xRepo
|
||||
}{xPrefix, head, tail, repo}
|
||||
if err := xTemplate.Execute(w, data); err != nil {
|
||||
log.Println("xHandler:", err)
|
||||
}
|
||||
}
|
||||
|
||||
var xTemplate = template.Must(template.New("x").Parse(`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||
<meta name="go-import" content="golang.org{{.Prefix}}{{.Head}} {{.Repo.VCS}} {{.Repo.URL}}">
|
||||
<meta name="go-source" content="golang.org{{.Prefix}}{{.Head}} https://github.com/golang/{{.Head}}/ https://github.com/golang/{{.Head}}/tree/master{/dir} https://github.com/golang/{{.Head}}/blob/master{/dir}/{file}#L{line}">
|
||||
<meta http-equiv="refresh" content="0; url=https://godoc.org/golang.org{{.Prefix}}{{.Head}}{{.Tail}}">
|
||||
</head>
|
||||
<body>
|
||||
Nothing to see here; <a href="https://godoc.org/golang.org{{.Prefix}}{{.Head}}{{.Tail}}">move along</a>.
|
||||
</body>
|
||||
</html>
|
||||
`))
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
|
||||
Command goimports updates your Go import lines,
|
||||
adding missing ones and removing unreferenced ones.
|
||||
|
||||
$ go get golang.org/x/tools/cmd/goimports
|
||||
|
||||
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-to-list 'load-path "/home/you/somewhere/emacs/")
|
||||
(require 'go-mode-autoloads)
|
||||
(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,369 @@
|
|||
// 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"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/tools/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,
|
||||
}
|
||||
exitCode = 0
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&options.AllErrors, "e", false, "report all errors (not just the first 10 on different lines)")
|
||||
flag.StringVar(&imports.LocalPrefix, "local", "", "put imports beginning with this string after 3rd-party packages; comma-separated list")
|
||||
}
|
||||
|
||||
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 := ioutil.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")
|
||||
}
|
||||
err = ioutil.WriteFile(filename, res, 0)
|
||||
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)
|
||||
imports.Debug = true
|
||||
}
|
||||
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 := ioutil.TempFile(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,26 @@
|
|||
// 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.
|
||||
|
||||
// +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() {
|
||||
flush()
|
||||
trace.Stop()
|
||||
}
|
||||
}
|
||||
return func() {}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
// 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.
|
||||
|
||||
// +build !gc
|
||||
|
||||
package main
|
||||
|
||||
func doTrace() func() {
|
||||
return func() {}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue