➕ 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