initial vendor commit
This commit is contained in:
1 parent
747c805a82
commit
21e1fb3451
660 files changed
+133146
No files matched your search
@@ -0,0 +1,7 @@
|
||||
language: go
|
||||
|
||||
install:
|
||||
- go get -t -v ./...
|
||||
|
||||
script:
|
||||
- go test -v ./...
|
||||
@@ -0,0 +1,15 @@
|
||||
# Contributing to Golint
|
||||
|
||||
## Before filing an issue:
|
||||
|
||||
### Are you having trouble building golint?
|
||||
|
||||
Check you have the latest version of its dependencies. Run
|
||||
```
|
||||
go get -u github.com/golang/lint
|
||||
```
|
||||
If you still have problems, consider searching for existing issues before filing a new issue.
|
||||
|
||||
## Before sending a pull request:
|
||||
|
||||
Have you understood the purpose of golint? Make sure to carefully read `README`.
|
||||
@@ -0,0 +1,27 @@
|
||||
Copyright (c) 2013 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,67 @@
|
||||
Golint is a linter for Go source code.
|
||||
|
||||
[](https://travis-ci.org/golang/lint)
|
||||
|
||||
To install, run
|
||||
go get -u github.com/golang/lint/golint
|
||||
|
||||
Invoke golint with one or more filenames, a directory, or a package named
|
||||
by its import path. Golint uses the same import path syntax as the go
|
||||
command (https://golang.org/cmd/go/#hdr-Import_path_syntax) and therefore
|
||||
also supports relative import paths like "./...". Additionally the "..."
|
||||
wildcard can be used as suffix on relative and absolute file paths to recurse
|
||||
into them.
|
||||
|
||||
The output of this tool is a list of suggestions in Vim quickfix format,
|
||||
which is accepted by lots of different editors.
|
||||
|
||||
Golint differs from gofmt. Gofmt reformats Go source code, whereas
|
||||
golint prints out style mistakes.
|
||||
|
||||
Golint differs from govet. Govet is concerned with correctness, whereas
|
||||
golint is concerned with coding style. Golint is in use at Google, and it
|
||||
seeks to match the accepted style of the open source Go project.
|
||||
|
||||
The suggestions made by golint are exactly that: suggestions.
|
||||
Golint is not perfect, and has both false positives and false negatives.
|
||||
Do not treat its output as a gold standard. We will not be adding pragmas
|
||||
or other knobs to suppress specific warnings, so do not expect or require
|
||||
code to be completely "lint-free".
|
||||
In short, this tool is not, and will never be, trustworthy enough for its
|
||||
suggestions to be enforced automatically, for example as part of a build process.
|
||||
|
||||
If you find an established style that is frequently violated, and which
|
||||
you think golint could statically check, file an issue at
|
||||
https://github.com/golang/lint/issues
|
||||
|
||||
|
||||
Contributions
|
||||
-------------
|
||||
Contributions to this project are welcome, though please send mail before
|
||||
starting work on anything major. Contributors retain their copyright, so we
|
||||
need you to fill out a short form before we can accept your contribution:
|
||||
https://developers.google.com/open-source/cla/individual
|
||||
|
||||
|
||||
Vim
|
||||
---
|
||||
Add this to your ~/.vimrc:
|
||||
set rtp+=$GOPATH/src/github.com/golang/lint/misc/vim
|
||||
If you have multiple entries in your GOPATH, replace $GOPATH with the right value.
|
||||
|
||||
Running :Lint will run golint on the current file and populate the quickfix list.
|
||||
|
||||
Optionally, add this to your ~/.vimrc to automatically run golint on :w
|
||||
autocmd BufWritePost,FileWritePost *.go execute 'Lint' | cwindow
|
||||
|
||||
|
||||
Emacs
|
||||
-----
|
||||
Add this to your .emacs file:
|
||||
(add-to-list 'load-path (concat (getenv "GOPATH") "/src/github.com/golang/lint/misc/emacs"))
|
||||
(require 'golint)
|
||||
If you have multiple entries in your GOPATH, replace $GOPATH with the right value.
|
||||
|
||||
Running M-x golint will run golint on the current file.
|
||||
For more usage, see Compilation-Mode:
|
||||
http://www.gnu.org/software/emacs/manual/html_node/emacs/Compilation-Mode.html
|
||||
@@ -0,0 +1,126 @@
|
||||
// Copyright (c) 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 or at
|
||||
// https://developers.google.com/open-source/licenses/bsd.
|
||||
|
||||
// golint lints the Go source files named on its command line.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/build"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/lint"
|
||||
)
|
||||
|
||||
var minConfidence = flag.Float64("min_confidence", 0.8, "minimum confidence of a problem to print it")
|
||||
|
||||
func usage() {
|
||||
fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
|
||||
fmt.Fprintf(os.Stderr, "\tgolint [flags] # runs on package in current directory\n")
|
||||
fmt.Fprintf(os.Stderr, "\tgolint [flags] package\n")
|
||||
fmt.Fprintf(os.Stderr, "\tgolint [flags] directory\n")
|
||||
fmt.Fprintf(os.Stderr, "\tgolint [flags] files... # must be a single package\n")
|
||||
fmt.Fprintf(os.Stderr, "Flags:\n")
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
|
||||
switch flag.NArg() {
|
||||
case 0:
|
||||
lintDir(".")
|
||||
case 1:
|
||||
arg := flag.Arg(0)
|
||||
if strings.HasSuffix(arg, "/...") && isDir(arg[:len(arg)-4]) {
|
||||
for _, dirname := range allPackagesInFS(arg) {
|
||||
lintDir(dirname)
|
||||
}
|
||||
} else if isDir(arg) {
|
||||
lintDir(arg)
|
||||
} else if exists(arg) {
|
||||
lintFiles(arg)
|
||||
} else {
|
||||
for _, pkgname := range importPaths([]string{arg}) {
|
||||
lintPackage(pkgname)
|
||||
}
|
||||
}
|
||||
default:
|
||||
lintFiles(flag.Args()...)
|
||||
}
|
||||
}
|
||||
|
||||
func isDir(filename string) bool {
|
||||
fi, err := os.Stat(filename)
|
||||
return err == nil && fi.IsDir()
|
||||
}
|
||||
|
||||
func exists(filename string) bool {
|
||||
_, err := os.Stat(filename)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func lintFiles(filenames ...string) {
|
||||
files := make(map[string][]byte)
|
||||
for _, filename := range filenames {
|
||||
src, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
continue
|
||||
}
|
||||
files[filename] = src
|
||||
}
|
||||
|
||||
l := new(lint.Linter)
|
||||
ps, err := l.LintFiles(files)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||
return
|
||||
}
|
||||
for _, p := range ps {
|
||||
if p.Confidence >= *minConfidence {
|
||||
fmt.Printf("%v: %s\n", p.Position, p.Text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func lintDir(dirname string) {
|
||||
pkg, err := build.ImportDir(dirname, 0)
|
||||
lintImportedPackage(pkg, err)
|
||||
}
|
||||
|
||||
func lintPackage(pkgname string) {
|
||||
pkg, err := build.Import(pkgname, ".", 0)
|
||||
lintImportedPackage(pkg, err)
|
||||
}
|
||||
|
||||
func lintImportedPackage(pkg *build.Package, err error) {
|
||||
if err != nil {
|
||||
if _, nogo := err.(*build.NoGoError); nogo {
|
||||
// Don't complain if the failure is due to no Go source files.
|
||||
return
|
||||
}
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
return
|
||||
}
|
||||
|
||||
var files []string
|
||||
files = append(files, pkg.GoFiles...)
|
||||
files = append(files, pkg.TestGoFiles...)
|
||||
if pkg.Dir != "." {
|
||||
for i, f := range files {
|
||||
files[i] = filepath.Join(pkg.Dir, f)
|
||||
}
|
||||
}
|
||||
// TODO(dsymonds): Do foo_test too (pkg.XTestGoFiles)
|
||||
|
||||
lintFiles(files...)
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
package main
|
||||
|
||||
/*
|
||||
|
||||
This file holds a direct copy of the import path matching code of
|
||||
https://code.google.com/p/go/source/browse/src/cmd/go/main.go. It can be
|
||||
replaced when https://code.google.com/p/go/issues/detail?id=8768 is resolved.
|
||||
|
||||
*/
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/build"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var buildContext = build.Default
|
||||
|
||||
var (
|
||||
goroot = filepath.Clean(runtime.GOROOT())
|
||||
gorootSrcPkg = filepath.Join(goroot, "src/pkg")
|
||||
)
|
||||
|
||||
// importPathsNoDotExpansion returns the import paths to use for the given
|
||||
// command line, but it does no ... expansion.
|
||||
func importPathsNoDotExpansion(args []string) []string {
|
||||
if len(args) == 0 {
|
||||
return []string{"."}
|
||||
}
|
||||
var out []string
|
||||
for _, a := range args {
|
||||
// Arguments are supposed to be import paths, but
|
||||
// as a courtesy to Windows developers, rewrite \ to /
|
||||
// in command-line arguments. Handles .\... and so on.
|
||||
if filepath.Separator == '\\' {
|
||||
a = strings.Replace(a, `\`, `/`, -1)
|
||||
}
|
||||
|
||||
// Put argument in canonical form, but preserve leading ./.
|
||||
if strings.HasPrefix(a, "./") {
|
||||
a = "./" + path.Clean(a)
|
||||
if a == "./." {
|
||||
a = "."
|
||||
}
|
||||
} else {
|
||||
a = path.Clean(a)
|
||||
}
|
||||
if a == "all" || a == "std" {
|
||||
out = append(out, allPackages(a)...)
|
||||
continue
|
||||
}
|
||||
out = append(out, a)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// importPaths returns the import paths to use for the given command line.
|
||||
func importPaths(args []string) []string {
|
||||
args = importPathsNoDotExpansion(args)
|
||||
var out []string
|
||||
for _, a := range args {
|
||||
if strings.Contains(a, "...") {
|
||||
if build.IsLocalImport(a) {
|
||||
out = append(out, allPackagesInFS(a)...)
|
||||
} else {
|
||||
out = append(out, allPackages(a)...)
|
||||
}
|
||||
continue
|
||||
}
|
||||
out = append(out, a)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// matchPattern(pattern)(name) reports whether
|
||||
// name matches pattern. Pattern is a limited glob
|
||||
// pattern in which '...' means 'any string' and there
|
||||
// is no other special syntax.
|
||||
func matchPattern(pattern string) func(name string) bool {
|
||||
re := regexp.QuoteMeta(pattern)
|
||||
re = strings.Replace(re, `\.\.\.`, `.*`, -1)
|
||||
// Special case: foo/... matches foo too.
|
||||
if strings.HasSuffix(re, `/.*`) {
|
||||
re = re[:len(re)-len(`/.*`)] + `(/.*)?`
|
||||
}
|
||||
reg := regexp.MustCompile(`^` + re + `$`)
|
||||
return func(name string) bool {
|
||||
return reg.MatchString(name)
|
||||
}
|
||||
}
|
||||
|
||||
// hasPathPrefix reports whether the path s begins with the
|
||||
// elements in prefix.
|
||||
func hasPathPrefix(s, prefix string) bool {
|
||||
switch {
|
||||
default:
|
||||
return false
|
||||
case len(s) == len(prefix):
|
||||
return s == prefix
|
||||
case len(s) > len(prefix):
|
||||
if prefix != "" && prefix[len(prefix)-1] == '/' {
|
||||
return strings.HasPrefix(s, prefix)
|
||||
}
|
||||
return s[len(prefix)] == '/' && s[:len(prefix)] == prefix
|
||||
}
|
||||
}
|
||||
|
||||
// treeCanMatchPattern(pattern)(name) reports whether
|
||||
// name or children of name can possibly match pattern.
|
||||
// Pattern is the same limited glob accepted by matchPattern.
|
||||
func treeCanMatchPattern(pattern string) func(name string) bool {
|
||||
wildCard := false
|
||||
if i := strings.Index(pattern, "..."); i >= 0 {
|
||||
wildCard = true
|
||||
pattern = pattern[:i]
|
||||
}
|
||||
return func(name string) bool {
|
||||
return len(name) <= len(pattern) && hasPathPrefix(pattern, name) ||
|
||||
wildCard && strings.HasPrefix(name, pattern)
|
||||
}
|
||||
}
|
||||
|
||||
// allPackages returns all the packages that can be found
|
||||
// under the $GOPATH directories and $GOROOT matching pattern.
|
||||
// The pattern is either "all" (all packages), "std" (standard packages)
|
||||
// or a path including "...".
|
||||
func allPackages(pattern string) []string {
|
||||
pkgs := matchPackages(pattern)
|
||||
if len(pkgs) == 0 {
|
||||
fmt.Fprintf(os.Stderr, "warning: %q matched no packages\n", pattern)
|
||||
}
|
||||
return pkgs
|
||||
}
|
||||
|
||||
func matchPackages(pattern string) []string {
|
||||
match := func(string) bool { return true }
|
||||
treeCanMatch := func(string) bool { return true }
|
||||
if pattern != "all" && pattern != "std" {
|
||||
match = matchPattern(pattern)
|
||||
treeCanMatch = treeCanMatchPattern(pattern)
|
||||
}
|
||||
|
||||
have := map[string]bool{
|
||||
"builtin": true, // ignore pseudo-package that exists only for documentation
|
||||
}
|
||||
if !buildContext.CgoEnabled {
|
||||
have["runtime/cgo"] = true // ignore during walk
|
||||
}
|
||||
var pkgs []string
|
||||
|
||||
// Commands
|
||||
cmd := filepath.Join(goroot, "src/cmd") + string(filepath.Separator)
|
||||
filepath.Walk(cmd, func(path string, fi os.FileInfo, err error) error {
|
||||
if err != nil || !fi.IsDir() || path == cmd {
|
||||
return nil
|
||||
}
|
||||
name := path[len(cmd):]
|
||||
if !treeCanMatch(name) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
// Commands are all in cmd/, not in subdirectories.
|
||||
if strings.Contains(name, string(filepath.Separator)) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
// We use, e.g., cmd/gofmt as the pseudo import path for gofmt.
|
||||
name = "cmd/" + name
|
||||
if have[name] {
|
||||
return nil
|
||||
}
|
||||
have[name] = true
|
||||
if !match(name) {
|
||||
return nil
|
||||
}
|
||||
_, err = buildContext.ImportDir(path, 0)
|
||||
if err != nil {
|
||||
if _, noGo := err.(*build.NoGoError); !noGo {
|
||||
log.Print(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
pkgs = append(pkgs, name)
|
||||
return nil
|
||||
})
|
||||
|
||||
for _, src := range buildContext.SrcDirs() {
|
||||
if pattern == "std" && src != gorootSrcPkg {
|
||||
continue
|
||||
}
|
||||
src = filepath.Clean(src) + string(filepath.Separator)
|
||||
filepath.Walk(src, func(path string, fi os.FileInfo, err error) error {
|
||||
if err != nil || !fi.IsDir() || path == src {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Avoid .foo, _foo, and testdata directory trees.
|
||||
_, elem := filepath.Split(path)
|
||||
if strings.HasPrefix(elem, ".") || strings.HasPrefix(elem, "_") || elem == "testdata" {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
name := filepath.ToSlash(path[len(src):])
|
||||
if pattern == "std" && strings.Contains(name, ".") {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
if !treeCanMatch(name) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
if have[name] {
|
||||
return nil
|
||||
}
|
||||
have[name] = true
|
||||
if !match(name) {
|
||||
return nil
|
||||
}
|
||||
_, err = buildContext.ImportDir(path, 0)
|
||||
if err != nil {
|
||||
if _, noGo := err.(*build.NoGoError); noGo {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
pkgs = append(pkgs, name)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return pkgs
|
||||
}
|
||||
|
||||
// allPackagesInFS is like allPackages but is passed a pattern
|
||||
// beginning ./ or ../, meaning it should scan the tree rooted
|
||||
// at the given directory. There are ... in the pattern too.
|
||||
func allPackagesInFS(pattern string) []string {
|
||||
pkgs := matchPackagesInFS(pattern)
|
||||
if len(pkgs) == 0 {
|
||||
fmt.Fprintf(os.Stderr, "warning: %q matched no packages\n", pattern)
|
||||
}
|
||||
return pkgs
|
||||
}
|
||||
|
||||
func matchPackagesInFS(pattern string) []string {
|
||||
// Find directory to begin the scan.
|
||||
// Could be smarter but this one optimization
|
||||
// is enough for now, since ... is usually at the
|
||||
// end of a path.
|
||||
i := strings.Index(pattern, "...")
|
||||
dir, _ := path.Split(pattern[:i])
|
||||
|
||||
// pattern begins with ./ or ../.
|
||||
// path.Clean will discard the ./ but not the ../.
|
||||
// We need to preserve the ./ for pattern matching
|
||||
// and in the returned import paths.
|
||||
prefix := ""
|
||||
if strings.HasPrefix(pattern, "./") {
|
||||
prefix = "./"
|
||||
}
|
||||
match := matchPattern(pattern)
|
||||
|
||||
var pkgs []string
|
||||
filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error {
|
||||
if err != nil || !fi.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if path == dir {
|
||||
// filepath.Walk starts at dir and recurses. For the recursive case,
|
||||
// the path is the result of filepath.Join, which calls filepath.Clean.
|
||||
// The initial case is not Cleaned, though, so we do this explicitly.
|
||||
//
|
||||
// This converts a path like "./io/" to "io". Without this step, running
|
||||
// "cd $GOROOT/src/pkg; go list ./io/..." would incorrectly skip the io
|
||||
// package, because prepending the prefix "./" to the unclean path would
|
||||
// result in "././io", and match("././io") returns false.
|
||||
path = filepath.Clean(path)
|
||||
}
|
||||
|
||||
// Avoid .foo, _foo, and testdata directory trees, but do not avoid "." or "..".
|
||||
_, elem := filepath.Split(path)
|
||||
dot := strings.HasPrefix(elem, ".") && elem != "." && elem != ".."
|
||||
if dot || strings.HasPrefix(elem, "_") || elem == "testdata" {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
name := prefix + filepath.ToSlash(path)
|
||||
if !match(name) {
|
||||
return nil
|
||||
}
|
||||
if _, err = build.ImportDir(path, 0); err != nil {
|
||||
if _, noGo := err.(*build.NoGoError); !noGo {
|
||||
log.Print(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
pkgs = append(pkgs, name)
|
||||
return nil
|
||||
})
|
||||
return pkgs
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,262 @@
|
||||
// Copyright (c) 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 or at
|
||||
// https://developers.google.com/open-source/licenses/bsd.
|
||||
|
||||
package lint
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/printer"
|
||||
"go/token"
|
||||
"io/ioutil"
|
||||
"path"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/go/types"
|
||||
)
|
||||
|
||||
var lintMatch = flag.String("lint.match", "", "restrict testdata matches to this pattern")
|
||||
|
||||
func TestAll(t *testing.T) {
|
||||
l := new(Linter)
|
||||
rx, err := regexp.Compile(*lintMatch)
|
||||
if err != nil {
|
||||
t.Fatalf("Bad -lint.match value %q: %v", *lintMatch, err)
|
||||
}
|
||||
|
||||
baseDir := "testdata"
|
||||
fis, err := ioutil.ReadDir(baseDir)
|
||||
if err != nil {
|
||||
t.Fatalf("ioutil.ReadDir: %v", err)
|
||||
}
|
||||
if len(fis) == 0 {
|
||||
t.Fatalf("no files in %v", baseDir)
|
||||
}
|
||||
for _, fi := range fis {
|
||||
if !rx.MatchString(fi.Name()) {
|
||||
continue
|
||||
}
|
||||
//t.Logf("Testing %s", fi.Name())
|
||||
src, err := ioutil.ReadFile(path.Join(baseDir, fi.Name()))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed reading %s: %v", fi.Name(), err)
|
||||
}
|
||||
|
||||
ins := parseInstructions(t, fi.Name(), src)
|
||||
if ins == nil {
|
||||
t.Errorf("Test file %v does not have instructions", fi.Name())
|
||||
continue
|
||||
}
|
||||
|
||||
ps, err := l.Lint(fi.Name(), src)
|
||||
if err != nil {
|
||||
t.Errorf("Linting %s: %v", fi.Name(), err)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, in := range ins {
|
||||
ok := false
|
||||
for i, p := range ps {
|
||||
if p.Position.Line != in.Line {
|
||||
continue
|
||||
}
|
||||
if in.Match.MatchString(p.Text) {
|
||||
// remove this problem from ps
|
||||
copy(ps[i:], ps[i+1:])
|
||||
ps = ps[:len(ps)-1]
|
||||
|
||||
//t.Logf("/%v/ matched at %s:%d", in.Match, fi.Name(), in.Line)
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
t.Errorf("Lint failed at %s:%d; /%v/ did not match", fi.Name(), in.Line, in.Match)
|
||||
}
|
||||
}
|
||||
for _, p := range ps {
|
||||
t.Errorf("Unexpected problem at %s:%d: %v", fi.Name(), p.Position.Line, p.Text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type instruction struct {
|
||||
Line int // the line number this applies to
|
||||
Match *regexp.Regexp // what pattern to match
|
||||
}
|
||||
|
||||
// parseInstructions parses instructions from the comments in a Go source file.
|
||||
// It returns nil if none were parsed.
|
||||
func parseInstructions(t *testing.T, filename string, src []byte) []instruction {
|
||||
fset := token.NewFileSet()
|
||||
f, err := parser.ParseFile(fset, filename, src, parser.ParseComments)
|
||||
if err != nil {
|
||||
t.Fatalf("Test file %v does not parse: %v", filename, err)
|
||||
}
|
||||
var ins []instruction
|
||||
for _, cg := range f.Comments {
|
||||
ln := fset.Position(cg.Pos()).Line
|
||||
raw := cg.Text()
|
||||
for _, line := range strings.Split(raw, "\n") {
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
if line == "OK" && ins == nil {
|
||||
// so our return value will be non-nil
|
||||
ins = make([]instruction, 0)
|
||||
continue
|
||||
}
|
||||
if strings.Contains(line, "MATCH") {
|
||||
rx, err := extractPattern(line)
|
||||
if err != nil {
|
||||
t.Fatalf("At %v:%d: %v", filename, ln, err)
|
||||
}
|
||||
matchLine := ln
|
||||
if i := strings.Index(line, "MATCH:"); i >= 0 {
|
||||
// This is a match for a different line.
|
||||
lns := strings.TrimPrefix(line[i:], "MATCH:")
|
||||
lns = lns[:strings.Index(lns, " ")]
|
||||
matchLine, err = strconv.Atoi(lns)
|
||||
if err != nil {
|
||||
t.Fatalf("Bad match line number %q at %v:%d: %v", lns, filename, ln, err)
|
||||
}
|
||||
}
|
||||
ins = append(ins, instruction{
|
||||
Line: matchLine,
|
||||
Match: rx,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return ins
|
||||
}
|
||||
|
||||
func extractPattern(line string) (*regexp.Regexp, error) {
|
||||
a, b := strings.Index(line, "/"), strings.LastIndex(line, "/")
|
||||
if a == -1 || a == b {
|
||||
return nil, fmt.Errorf("malformed match instruction %q", line)
|
||||
}
|
||||
pat := line[a+1 : b]
|
||||
rx, err := regexp.Compile(pat)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("bad match pattern %q: %v", pat, err)
|
||||
}
|
||||
return rx, nil
|
||||
}
|
||||
|
||||
func render(fset *token.FileSet, x interface{}) string {
|
||||
var buf bytes.Buffer
|
||||
if err := printer.Fprint(&buf, fset, x); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func TestLine(t *testing.T) {
|
||||
tests := []struct {
|
||||
src string
|
||||
offset int
|
||||
want string
|
||||
}{
|
||||
{"single line file", 5, "single line file"},
|
||||
{"single line file with newline\n", 5, "single line file with newline\n"},
|
||||
{"first\nsecond\nthird\n", 2, "first\n"},
|
||||
{"first\nsecond\nthird\n", 9, "second\n"},
|
||||
{"first\nsecond\nthird\n", 14, "third\n"},
|
||||
{"first\nsecond\nthird with no newline", 16, "third with no newline"},
|
||||
{"first byte\n", 0, "first byte\n"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
got := srcLine([]byte(test.src), token.Position{Offset: test.offset})
|
||||
if got != test.want {
|
||||
t.Errorf("srcLine(%q, offset=%d) = %q, want %q", test.src, test.offset, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLintName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name, want string
|
||||
}{
|
||||
{"foo_bar", "fooBar"},
|
||||
{"foo_bar_baz", "fooBarBaz"},
|
||||
{"Foo_bar", "FooBar"},
|
||||
{"foo_WiFi", "fooWiFi"},
|
||||
{"id", "id"},
|
||||
{"Id", "ID"},
|
||||
{"foo_id", "fooID"},
|
||||
{"fooId", "fooID"},
|
||||
{"fooUid", "fooUID"},
|
||||
{"idFoo", "idFoo"},
|
||||
{"uidFoo", "uidFoo"},
|
||||
{"midIdDle", "midIDDle"},
|
||||
{"APIProxy", "APIProxy"},
|
||||
{"ApiProxy", "APIProxy"},
|
||||
{"apiProxy", "apiProxy"},
|
||||
{"_Leading", "_Leading"},
|
||||
{"___Leading", "_Leading"},
|
||||
{"trailing_", "trailing"},
|
||||
{"trailing___", "trailing"},
|
||||
{"a_b", "aB"},
|
||||
{"a__b", "aB"},
|
||||
{"a___b", "aB"},
|
||||
{"Rpc1150", "RPC1150"},
|
||||
{"case3_1", "case3_1"},
|
||||
{"case3__1", "case3_1"},
|
||||
{"IEEE802_16bit", "IEEE802_16bit"},
|
||||
{"IEEE802_16Bit", "IEEE802_16Bit"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
got := lintName(test.name)
|
||||
if got != test.want {
|
||||
t.Errorf("lintName(%q) = %q, want %q", test.name, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExportedType(t *testing.T) {
|
||||
tests := []struct {
|
||||
typString string
|
||||
exp bool
|
||||
}{
|
||||
{"int", true},
|
||||
{"string", false}, // references the shadowed builtin "string"
|
||||
{"T", true},
|
||||
{"t", false},
|
||||
{"*T", true},
|
||||
{"*t", false},
|
||||
{"map[int]complex128", true},
|
||||
}
|
||||
for _, test := range tests {
|
||||
src := `package foo; type T int; type t int; type string struct{}`
|
||||
fset := token.NewFileSet()
|
||||
file, err := parser.ParseFile(fset, "foo.go", src, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("Parsing %q: %v", src, err)
|
||||
}
|
||||
// use the package name as package path
|
||||
pkg, err := types.Check(file.Name.Name, fset, []*ast.File{file})
|
||||
if err != nil {
|
||||
t.Fatalf("Type checking %q: %v", src, err)
|
||||
}
|
||||
// Use the first child scope of the package, which will be the file scope.
|
||||
scope := pkg.Scope().Child(0)
|
||||
tv, err := types.Eval(test.typString, pkg, scope)
|
||||
if err != nil {
|
||||
t.Errorf("types.Eval(%q): %v", test.typString, err)
|
||||
continue
|
||||
}
|
||||
if got := exportedType(tv.Type); got != test.exp {
|
||||
t.Errorf("exportedType(%v) = %t, want %t", tv.Type, got, test.exp)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
;;; golint.el --- lint for the Go source code
|
||||
|
||||
;; 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.
|
||||
|
||||
;; URL: https://github.com/golang/lint
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; To install golint, add the following lines to your .emacs file:
|
||||
;; (add-to-list 'load-path "PATH CONTAINING golint.el" t)
|
||||
;; (require 'golint)
|
||||
;;
|
||||
;; After this, type M-x golint on Go source code.
|
||||
;;
|
||||
;; Usage:
|
||||
;; C-x `
|
||||
;; Jump directly to the line in your code which caused the first message.
|
||||
;;
|
||||
;; For more usage, see Compilation-Mode:
|
||||
;; http://www.gnu.org/software/emacs/manual/html_node/emacs/Compilation-Mode.html
|
||||
|
||||
;;; Code:
|
||||
(require 'compile)
|
||||
|
||||
(defun go-lint-buffer-name (mode)
|
||||
"*Golint*")
|
||||
|
||||
(defun golint-process-setup ()
|
||||
"Setup compilation variables and buffer for `golint'."
|
||||
(run-hooks 'golint-setup-hook))
|
||||
|
||||
(define-compilation-mode golint-mode "golint"
|
||||
"Golint is a linter for Go source code."
|
||||
(set (make-local-variable 'compilation-scroll-output) nil)
|
||||
(set (make-local-variable 'compilation-disable-input) t)
|
||||
(set (make-local-variable 'compilation-process-setup-function)
|
||||
'golint-process-setup)
|
||||
)
|
||||
|
||||
;;;###autoload
|
||||
(defun golint ()
|
||||
"Run golint on the current file and populate the fix list. Pressing C-x ` will jump directly to the line in your code which caused the first message."
|
||||
(interactive)
|
||||
(compilation-start (concat "golint " buffer-file-name)
|
||||
'golint-mode))
|
||||
|
||||
(provide 'golint)
|
||||
|
||||
;;; golint.el ends here
|
||||
@@ -0,0 +1,31 @@
|
||||
" 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.
|
||||
"
|
||||
" lint.vim: Vim command to lint Go files with golint.
|
||||
"
|
||||
" https://github.com/golang/lint
|
||||
"
|
||||
" This filetype plugin add a new commands for go buffers:
|
||||
"
|
||||
" :Lint
|
||||
"
|
||||
" Run golint for the current Go file.
|
||||
"
|
||||
if exists("b:did_ftplugin_go_lint")
|
||||
finish
|
||||
endif
|
||||
|
||||
if !executable("golint")
|
||||
finish
|
||||
endif
|
||||
|
||||
command! -buffer Lint call s:GoLint()
|
||||
|
||||
function! s:GoLint() abort
|
||||
cexpr system('golint ' . shellescape(expand('%')))
|
||||
endfunction
|
||||
|
||||
let b:did_ftplugin_go_lint = 1
|
||||
|
||||
" vim:ts=4:sw=4:et
|
||||
@@ -0,0 +1,34 @@
|
||||
// Test that exported names have correct comments.
|
||||
|
||||
// Package pkg does something.
|
||||
package pkg
|
||||
|
||||
import "time"
|
||||
|
||||
type T int // MATCH /exported type T.*should.*comment.*or.*unexport/
|
||||
|
||||
func (T) F() {} // MATCH /exported method T\.F.*should.*comment.*or.*unexport/
|
||||
|
||||
// this is a nice type.
|
||||
// MATCH /comment.*exported type U.*should.*form.*"U ..."/
|
||||
type U string
|
||||
|
||||
// this is a neat function.
|
||||
// MATCH /comment.*exported method U\.G.*should.*form.*"G ..."/
|
||||
func (U) G() {}
|
||||
|
||||
// A V is a string.
|
||||
type V string
|
||||
|
||||
// V.H has a pointer receiver
|
||||
|
||||
func (*V) H() {} // MATCH /exported method V\.H.*should.*comment.*or.*unexport/
|
||||
|
||||
var W = "foo" // MATCH /exported var W.*should.*comment.*or.*unexport/
|
||||
|
||||
const X = "bar" // MATCH /exported const X.*should.*comment.*or.*unexport/
|
||||
|
||||
var Y, Z int // MATCH /exported var Z.*own declaration/
|
||||
|
||||
// Location should be okay, since the other var name is an underscore.
|
||||
var Location, _ = time.LoadLocation("Europe/Istanbul") // not Constantinople
|
||||
@@ -0,0 +1,17 @@
|
||||
// This file ends in _test.go, so we should not warn about doc comments.
|
||||
// OK
|
||||
|
||||
package pkg
|
||||
|
||||
import "testing"
|
||||
|
||||
type H int
|
||||
|
||||
func TestSomething(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSomething_suffix(t *testing.T) {
|
||||
}
|
||||
|
||||
func ExampleBuffer_reader() {
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Test that blank imports in library packages are flagged.
|
||||
|
||||
// Package foo ...
|
||||
package foo
|
||||
|
||||
// The instructions need to go before the imports below so they will not be
|
||||
// mistaken for documentation.
|
||||
|
||||
/* MATCH /blank import/ */ import _ "encoding/json"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
/* MATCH /blank import/ */ _ "os"
|
||||
|
||||
/* MATCH /blank import/ */ _ "net/http"
|
||||
_ "path"
|
||||
)
|
||||
|
||||
import _ "encoding/base64" // Don't gripe about this
|
||||
|
||||
import (
|
||||
// Don't gripe about these next two lines.
|
||||
_ "compress/zlib"
|
||||
_ "syscall"
|
||||
|
||||
/* MATCH /blank import/ */ _ "path/filepath"
|
||||
)
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
_ "go/scanner" // Don't gripe about this or the following line.
|
||||
_ "go/token"
|
||||
)
|
||||
|
||||
var (
|
||||
_ fmt.Stringer // for "fmt"
|
||||
_ ast.Node // for "go/ast"
|
||||
)
|
||||
@@ -0,0 +1,25 @@
|
||||
// Test that blank imports in test packages are not flagged.
|
||||
// OK
|
||||
|
||||
// Package foo ...
|
||||
package foo
|
||||
|
||||
// These are essentially the same imports as in the "library" package, but
|
||||
// these should not trigger the warning because this is a test.
|
||||
|
||||
import _ "encoding/json"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
_ "os"
|
||||
|
||||
_ "net/http"
|
||||
_ "path"
|
||||
)
|
||||
|
||||
var (
|
||||
_ fmt.Stringer // for "fmt"
|
||||
_ testing.T // for "testing"
|
||||
)
|
||||
@@ -0,0 +1,14 @@
|
||||
// Test that blank imports in package main are not flagged.
|
||||
// OK
|
||||
|
||||
// Binary foo ...
|
||||
package main
|
||||
|
||||
import _ "fmt"
|
||||
|
||||
import (
|
||||
"os"
|
||||
_ "path"
|
||||
)
|
||||
|
||||
var _ os.File // for "os"
|
||||
@@ -0,0 +1,16 @@
|
||||
// Test that we don't nag for comments on common methods.
|
||||
// OK
|
||||
|
||||
// Package pkg ...
|
||||
package pkg
|
||||
|
||||
import "net/http"
|
||||
|
||||
// T is ...
|
||||
type T int
|
||||
|
||||
func (T) Error() string { return "" }
|
||||
func (T) String() string { return "" }
|
||||
func (T) ServeHTTP(w http.ResponseWriter, r *http.Request) {}
|
||||
func (T) Read(p []byte) (n int, err error) { return 0, nil }
|
||||
func (T) Write(p []byte) (n int, err error) { return 0, nil }
|
||||
@@ -0,0 +1,36 @@
|
||||
// Test for docs in const blocks
|
||||
|
||||
// Package foo ...
|
||||
package foo
|
||||
|
||||
const (
|
||||
// Prefix for something.
|
||||
// MATCH /InlineWhatever.*form/
|
||||
InlineWhatever = "blah"
|
||||
|
||||
Whatsit = "missing_comment" // MATCH /Whatsit.*should have comment.*block/
|
||||
|
||||
// We should only warn once per block for missing comments,
|
||||
// but always complain about malformed comments.
|
||||
|
||||
WhosYourDaddy = "another_missing_one"
|
||||
|
||||
// Something
|
||||
// MATCH /WhatDoesHeDo.*form/
|
||||
WhatDoesHeDo = "it's not a tumor!"
|
||||
)
|
||||
|
||||
// These shouldn't need doc comments.
|
||||
const (
|
||||
Alpha = "a"
|
||||
Beta = "b"
|
||||
Gamma = "g"
|
||||
)
|
||||
|
||||
// The comment on the previous const block shouldn't flow through to here.
|
||||
|
||||
const UndocAgain = 6 // MATCH /UndocAgain.*should have comment/
|
||||
|
||||
const (
|
||||
SomeUndocumented = 7 // MATCH /SomeUndocumented.*should have comment.*block/
|
||||
)
|
||||
@@ -0,0 +1,18 @@
|
||||
// Test of return+else warning; should not trigger on multi-branch if/else.
|
||||
// OK
|
||||
|
||||
// Package pkg ...
|
||||
package pkg
|
||||
|
||||
import "log"
|
||||
|
||||
func f(x int) bool {
|
||||
if x == 0 {
|
||||
log.Print("x is zero")
|
||||
} else if x > 0 {
|
||||
return true
|
||||
} else {
|
||||
log.Printf("non-positive x: %d", x)
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Test of return+else warning.
|
||||
|
||||
// Package pkg ...
|
||||
package pkg
|
||||
|
||||
import "log"
|
||||
|
||||
func f(x int) bool {
|
||||
if x > 0 {
|
||||
return true
|
||||
} else { // MATCH /if.*return.*else.*outdent/
|
||||
log.Printf("non-positive x: %d", x)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func g(f func() bool) string {
|
||||
if ok := f(); ok {
|
||||
return "it's okay"
|
||||
} else { // MATCH /if.*return.*else.*outdent.*short.*var.*declaration/
|
||||
return "it's NOT okay!"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Test for returning errors.
|
||||
|
||||
// Package foo ...
|
||||
package foo
|
||||
|
||||
// Returns nothing
|
||||
func f() { // ok
|
||||
}
|
||||
|
||||
// Check for a single error return
|
||||
func g() error { // ok
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check for a single other return type
|
||||
func h() int { // ok
|
||||
return 0
|
||||
}
|
||||
|
||||
// Check for multiple return but error at end.
|
||||
func i() (int, error) { // ok
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// Check for multiple return but error at end with named variables.
|
||||
func j() (x int, err error) { // ok
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// Check for error in the wrong location on 2 types
|
||||
func k() (error, int) { // MATCH /error should be the last type/
|
||||
return nil, 0
|
||||
}
|
||||
|
||||
// Check for error in the wrong location for > 2 types
|
||||
func l() (int, error, int) { // MATCH /error should be the last type/
|
||||
return 0, nil, 0
|
||||
}
|
||||
|
||||
// Check for error in the wrong location with named variables.
|
||||
func m() (x int, err error, y int) { // MATCH /error should be the last type/
|
||||
return 0, nil, 0
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Test for not using fmt.Errorf.
|
||||
|
||||
// Package foo ...
|
||||
package foo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func f(x int) error {
|
||||
if x > 10 {
|
||||
return errors.New(fmt.Sprintf("something %d", x)) // MATCH /should replace.*errors\.New\(fmt\.Sprintf\(\.\.\.\)\).*fmt\.Errorf\(\.\.\.\)/
|
||||
}
|
||||
if x > 5 {
|
||||
return errors.New(g("blah")) // ok
|
||||
}
|
||||
if x > 4 {
|
||||
return errors.New("something else") // ok
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func g(s string) string { return "prefix: " + s }
|
||||
@@ -0,0 +1,36 @@
|
||||
// Test for naming errors.
|
||||
|
||||
// Package foo ...
|
||||
package foo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var unexp = errors.New("some unexported error") // MATCH /error var.*unexp.*errFoo/
|
||||
|
||||
// Exp ...
|
||||
var Exp = errors.New("some exported error") // MATCH /error var.*Exp.*ErrFoo/
|
||||
|
||||
var (
|
||||
e1 = fmt.Errorf("blah %d", 4) // MATCH /error var.*e1.*errFoo/
|
||||
// E2 ...
|
||||
E2 = fmt.Errorf("blah %d", 5) // MATCH /error var.*E2.*ErrFoo/
|
||||
)
|
||||
|
||||
func f() {
|
||||
var whatever = errors.New("ok") // ok
|
||||
_ = whatever
|
||||
}
|
||||
|
||||
// Check for the error strings themselves.
|
||||
|
||||
func g(x int) error {
|
||||
if x < 1 {
|
||||
return fmt.Errorf("This %d is too low", x) // MATCH /error strings.*not be capitalized/
|
||||
} else if x == 0 {
|
||||
return fmt.Errorf("XML time") // ok
|
||||
}
|
||||
return errors.New(`too much stuff.`) // MATCH /error strings.*not end with punctuation/
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// Test that dot imports are flagged.
|
||||
|
||||
// Package pkg ...
|
||||
package pkg
|
||||
|
||||
import . "fmt" // MATCH /dot import/
|
||||
|
||||
var _ Stringer // from "fmt"
|
||||
@@ -0,0 +1,14 @@
|
||||
// Test for use of x++ and x--.
|
||||
|
||||
// Package pkg ...
|
||||
package pkg
|
||||
|
||||
func addOne(x int) int {
|
||||
x += 1 // MATCH /x\+\+/
|
||||
return x
|
||||
}
|
||||
|
||||
func subOneInLoop(y int) {
|
||||
for ; y > 0; y -= 1 { // MATCH /y--/
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Test for pointless make() calls.
|
||||
|
||||
// Package pkg ...
|
||||
package pkg
|
||||
|
||||
import "net/http"
|
||||
|
||||
// T is a test type.
|
||||
type T int
|
||||
|
||||
var z []T
|
||||
|
||||
func f() {
|
||||
x := make([]T, 0) // MATCH /var x \[\]T/
|
||||
y := make([]http.Request, 0) // MATCH /var y \[\]http\.Request/
|
||||
z = make([]T, 0) // ok, because we don't know where z is declared
|
||||
|
||||
_, _ = x, y
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// Test for name linting.
|
||||
|
||||
// Package pkg_with_underscores ...
|
||||
package pkg_with_underscores // MATCH /underscore.*package name/
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
net_http "net/http" // renamed deliberately
|
||||
"net/url"
|
||||
)
|
||||
|
||||
var var_name int // MATCH /underscore.*var.*var_name/
|
||||
|
||||
type t_wow struct { // MATCH /underscore.*type.*t_wow/
|
||||
x_damn int // MATCH /underscore.*field.*x_damn/
|
||||
Url *url.URL // MATCH /struct field.*Url.*URL/
|
||||
}
|
||||
|
||||
const fooId = "blah" // MATCH /fooId.*fooID/
|
||||
|
||||
func f_it() { // MATCH /underscore.*func.*f_it/
|
||||
more_underscore := 4 // MATCH /underscore.*var.*more_underscore/
|
||||
_ = more_underscore
|
||||
var err error
|
||||
if isEof := (err == io.EOF); isEof { // MATCH /var.*isEof.*isEOF/
|
||||
more_underscore = 7 // should be okay
|
||||
}
|
||||
|
||||
x := net_http.Request{} // should be okay
|
||||
_ = x
|
||||
|
||||
var ips []net.IP
|
||||
for _, theIp := range ips { // MATCH /range var.*theIp.*theIP/
|
||||
_ = theIp
|
||||
}
|
||||
|
||||
switch myJson := g(); { // MATCH /var.*myJson.*myJSON/
|
||||
default:
|
||||
_ = myJson
|
||||
}
|
||||
var y net_http.ResponseWriter // an interface
|
||||
switch tApi := y.(type) { // MATCH /var.*tApi.*tAPI/
|
||||
default:
|
||||
_ = tApi
|
||||
}
|
||||
|
||||
var c chan int
|
||||
select {
|
||||
case qId := <-c: // MATCH /var.*qId.*qID/
|
||||
_ = qId
|
||||
}
|
||||
}
|
||||
|
||||
// Common styles in other languages that don't belong in Go.
|
||||
const (
|
||||
CPP_CONST = 1 // MATCH /ALL_CAPS.*CamelCase/
|
||||
kLeadingKay = 2 // MATCH /k.*leadingKay/
|
||||
|
||||
HTML = 3 // okay; no underscore
|
||||
X509B = 4 // ditto
|
||||
)
|
||||
|
||||
func f(bad_name int) {} // MATCH /underscore.*func parameter.*bad_name/
|
||||
func g() (no_way int) { return 0 } // MATCH /underscore.*func result.*no_way/
|
||||
func (t *t_wow) f(more_under string) {} // MATCH /underscore.*method parameter.*more_under/
|
||||
func (t *t_wow) g() (still_more string) { return "" } // MATCH /underscore.*method result.*still_more/
|
||||
|
||||
type i interface {
|
||||
CheckHtml() string // okay; interface method names are often constrained by the concrete types' method names
|
||||
|
||||
F(foo_bar int) // MATCH /foo_bar.*fooBar/
|
||||
}
|
||||
|
||||
// All okay; underscore between digits
|
||||
const case1_1 = 1
|
||||
|
||||
type case2_1 struct {
|
||||
case2_2 int
|
||||
}
|
||||
|
||||
func case3_1(case3_2 int) (case3_3 string) {
|
||||
case3_4 := 4
|
||||
_ = case3_4
|
||||
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// Test of missing package comment.
|
||||
|
||||
package foo // MATCH /should.*package comment.*unless/
|
||||
@@ -0,0 +1,5 @@
|
||||
// Test of package comment in an incorrect form.
|
||||
|
||||
// Some random package doc that isn't in the right form.
|
||||
// MATCH /package comment should.*form.*"Package testdata .*"/
|
||||
package testdata
|
||||
@@ -0,0 +1,7 @@
|
||||
// Test of block package comment.
|
||||
// OK
|
||||
|
||||
/*
|
||||
Package foo is pretty sweet.
|
||||
*/
|
||||
package foo
|
||||
@@ -0,0 +1,7 @@
|
||||
// Test of block package comment with leading space.
|
||||
|
||||
/*
|
||||
Package foo is pretty sweet.
|
||||
MATCH /package comment.*leading space/
|
||||
*/
|
||||
package foo
|
||||
@@ -0,0 +1,9 @@
|
||||
// Test of detached package comment.
|
||||
|
||||
/*
|
||||
Package foo is pretty sweet.
|
||||
*/
|
||||
|
||||
package foo
|
||||
|
||||
// MATCH:6 /package comment.*detached/
|
||||
@@ -0,0 +1,5 @@
|
||||
// Test of package comment for package main.
|
||||
// OK
|
||||
|
||||
// This binary does something awesome.
|
||||
package main
|
||||
@@ -0,0 +1,37 @@
|
||||
// Test for range construction.
|
||||
|
||||
// Package foo ...
|
||||
package foo
|
||||
|
||||
func f() {
|
||||
var m map[string]int
|
||||
|
||||
// with :=
|
||||
for x, _ := range m { // MATCH /should omit 2nd value.*range.*equivalent.*for x := range/
|
||||
_ = x
|
||||
}
|
||||
// with =
|
||||
var y string
|
||||
_ = y
|
||||
for y, _ = range m { // MATCH /should omit 2nd value.*range.*equivalent.*for y = range/
|
||||
}
|
||||
|
||||
// all OK:
|
||||
for x := range m {
|
||||
_ = x
|
||||
}
|
||||
for x, y := range m {
|
||||
_, _ = x, y
|
||||
}
|
||||
for _, y := range m {
|
||||
_ = y
|
||||
}
|
||||
var x int
|
||||
_ = x
|
||||
for y = range m {
|
||||
}
|
||||
for y, x = range m {
|
||||
}
|
||||
for _, x = range m {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Test for bad receiver names.
|
||||
|
||||
// Package foo ...
|
||||
package foo
|
||||
|
||||
type foo struct{}
|
||||
|
||||
func (this foo) f1() { // MATCH /should be a reflection of its identity/
|
||||
}
|
||||
|
||||
func (self foo) f2() { // MATCH /should be a reflection of its identity/
|
||||
}
|
||||
|
||||
func (f foo) f3() {
|
||||
}
|
||||
|
||||
func (foo) f4() {
|
||||
}
|
||||
|
||||
type bar struct{}
|
||||
|
||||
func (b bar) f1() {
|
||||
}
|
||||
|
||||
func (b bar) f2() {
|
||||
}
|
||||
|
||||
func (a bar) f3() { // MATCH /receiver name a should be consistent with previous receiver name b for bar/
|
||||
}
|
||||
|
||||
func (a *bar) f4() { // MATCH /receiver name a should be consistent with previous receiver name b for bar/
|
||||
}
|
||||
|
||||
func (b *bar) f5() {
|
||||
}
|
||||
|
||||
func (bar) f6() {
|
||||
}
|
||||
|
||||
func (_ *bar) f7() { // MATCH /receiver name should not be an underscore/
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Test that we don't ask for comments on sort.Interface methods.
|
||||
|
||||
// Package pkg ...
|
||||
package pkg
|
||||
|
||||
// T is ...
|
||||
type T []int
|
||||
|
||||
// Len by itself should get documented.
|
||||
|
||||
func (t T) Len() int { return len(t) } // MATCH /exported method T\.Len.*should.*comment/
|
||||
|
||||
// U is ...
|
||||
type U []int
|
||||
|
||||
func (u U) Len() int { return len(u) }
|
||||
func (u U) Less(i, j int) bool { return u[i] < u[j] }
|
||||
func (u U) Swap(i, j int) { u[i], u[j] = u[j], u[i] }
|
||||
|
||||
func (u U) Other() {} // MATCH /exported method U\.Other.*should.*comment/
|
||||
@@ -0,0 +1,25 @@
|
||||
// Test of stuttery names.
|
||||
|
||||
// Package donut ...
|
||||
package donut
|
||||
|
||||
// DonutMaker makes donuts.
|
||||
type DonutMaker struct{} // MATCH /donut\.DonutMaker.*stutter/
|
||||
|
||||
// DonutRank computes the ranking of a donut.
|
||||
func DonutRank(d Donut) int { // MATCH /donut\.DonutRank.*stutter/
|
||||
return 0
|
||||
}
|
||||
|
||||
// Donut is a delicious treat.
|
||||
type Donut struct{} // ok because it is the whole name
|
||||
|
||||
// Donuts are great, aren't they?
|
||||
type Donuts []Donut // ok because it didn't start a new word
|
||||
|
||||
type donutGlaze int // ok because it is unexported
|
||||
|
||||
// DonutMass reports the mass of a donut.
|
||||
func (d *Donut) DonutMass() (grams int) { // okay because it is a method
|
||||
return 38
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Test of time suffixes.
|
||||
|
||||
// Package foo ...
|
||||
package foo
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"time"
|
||||
)
|
||||
|
||||
var rpcTimeoutMsec = flag.Duration("rpc_timeout", 100*time.Millisecond, "some flag") // MATCH /Msec.*\*time.Duration/
|
||||
|
||||
var timeoutSecs = 5 * time.Second // MATCH /Secs.*time.Duration/
|
||||
@@ -0,0 +1,46 @@
|
||||
// Test for unexported return types.
|
||||
|
||||
// Package foo ...
|
||||
package foo
|
||||
|
||||
import ()
|
||||
|
||||
type hidden struct{}
|
||||
|
||||
// Exported returns a hidden type, which is annoying.
|
||||
func Exported() hidden { // MATCH /Exported.*unexported.*hidden/
|
||||
return hidden{}
|
||||
}
|
||||
|
||||
// ExpErr returns a builtin type.
|
||||
func ExpErr() error { // ok
|
||||
}
|
||||
|
||||
func (hidden) ExpOnHidden() hidden { // ok
|
||||
}
|
||||
|
||||
// T is another test type.
|
||||
type T struct{}
|
||||
|
||||
// MethodOnT returns a hidden type, which is annoying.
|
||||
func (T) MethodOnT() hidden { // MATCH /method MethodOnT.*unexported.*hidden/
|
||||
return hidden{}
|
||||
}
|
||||
|
||||
// ExpT returns a T.
|
||||
func ExpT() T { // ok
|
||||
return T{}
|
||||
}
|
||||
|
||||
func unexp() hidden { // ok
|
||||
return hidden{}
|
||||
}
|
||||
|
||||
// This is slightly sneaky: we shadow the builtin "int" type.
|
||||
|
||||
type int struct{}
|
||||
|
||||
// ExportedIntReturner returns an unexported type from this package.
|
||||
func ExportedIntReturner() int { // MATCH /ExportedIntReturner.*unexported.*int/
|
||||
return int{}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// Test for redundant type declaration.
|
||||
|
||||
// Package foo ...
|
||||
package foo
|
||||
|
||||
import "fmt"
|
||||
import "net/http"
|
||||
|
||||
// Q is a test type.
|
||||
type Q bool
|
||||
|
||||
var myInt int = 7 // MATCH /should.*int.*myInt.*inferred/
|
||||
var mux *http.ServeMux = http.NewServeMux() // MATCH /should.*\*http\.ServeMux.*inferred/
|
||||
|
||||
var myZeroInt int = 0 // MATCH /should.*= 0.*myZeroInt.*zero value/
|
||||
var myZeroFlt float32 = 0. // MATCH /should.*= 0\..*myZeroFlt.*zero value/
|
||||
var myZeroF64 float64 = 0.0 // MATCH /should.*= 0\..*myZeroF64.*zero value/
|
||||
var myZeroImg complex64 = 0i // MATCH /should.*= 0i.*myZeroImg.*zero value/
|
||||
var myZeroStr string = "" // MATCH /should.*= "".*myZeroStr.*zero value/
|
||||
var myZeroRaw string = `` // MATCH /should.*= ``.*myZeroRaw.*zero value/
|
||||
var myZeroPtr *Q = nil // MATCH /should.*= nil.*myZeroPtr.*zero value/
|
||||
var myZeroRune rune = '\x00' // MATCH /should.*= '\\x00'.*myZeroRune.*zero value/
|
||||
var myZeroRune2 rune = '\000' // MATCH /should.*= '\\000'.*myZeroRune2.*zero value/
|
||||
|
||||
// No warning because there's no type on the LHS
|
||||
var x = 0
|
||||
|
||||
// This shouldn't get a warning because there's no initial values.
|
||||
var str fmt.Stringer
|
||||
|
||||
// No warning because this is a const.
|
||||
const k uint64 = 7
|
||||
|
||||
const num = 123
|
||||
|
||||
// No warning because the var's RHS is known to be an untyped const.
|
||||
var flags uint32 = num
|
||||
|
||||
// No warnings because the RHS is an ideal int, and the LHS is a different int type.
|
||||
var userID int64 = 1235
|
||||
var negID int64 = -1
|
||||
var parenID int64 = (17)
|
||||
var crazyID int64 = -(-(-(-9)))
|
||||
|
||||
// Same, but for strings and floats.
|
||||
type stringT string
|
||||
type floatT float64
|
||||
|
||||
var stringV stringT = "abc"
|
||||
var floatV floatT = 123.45
|
||||
|
||||
// No warning because the LHS names an interface type.
|
||||
var data interface{} = googleIPs
|
||||
var googleIPs []int
|
||||
|
||||
// No warning because it's a common idiom for interface satisfaction.
|
||||
var _ Server = (*serverImpl)(nil)
|
||||
|
||||
// Server is a test type.
|
||||
type Server interface{}
|
||||
type serverImpl struct{}
|
||||
|
||||
// LHS is a different type than the RHS.
|
||||
var myStringer fmt.Stringer = q(0)
|
||||
|
||||
var y string = q(1).String() // MATCH /should.*string/
|
||||
|
||||
type q int
|
||||
|
||||
func (q) String() string { return "I'm a q" }
|
||||
@@ -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,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 @@
|
||||
Copyright (c) 2009 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,22 @@
|
||||
Additional IP Rights Grant (Patents)
|
||||
|
||||
"This implementation" means the copyrightable works distributed by
|
||||
Google as part of the Go project.
|
||||
|
||||
Google hereby grants to You a perpetual, worldwide, non-exclusive,
|
||||
no-charge, royalty-free, irrevocable (except as stated in this section)
|
||||
patent license to make, have made, use, offer to sell, sell, import,
|
||||
transfer and otherwise run, modify and propagate the contents of this
|
||||
implementation of Go, where such license applies only to those patent
|
||||
claims, both currently owned or controlled by Google and acquired in
|
||||
the future, licensable by Google that are necessarily infringed by this
|
||||
implementation of Go. This grant does not include claims that would be
|
||||
infringed only as a consequence of further modification of this
|
||||
implementation. If you or your agent or exclusive licensee institute or
|
||||
order or agree to the institution of patent litigation against any
|
||||
entity (including a cross-claim or counterclaim in a lawsuit) alleging
|
||||
that this implementation of Go or any code incorporated within this
|
||||
implementation of Go constitutes direct or contributory patent
|
||||
infringement, or inducement of patent infringement, then any patent
|
||||
rights granted to you under this License for this implementation of Go
|
||||
shall terminate as of the date such litigation is filed.
|
||||
@@ -0,0 +1,10 @@
|
||||
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 oracle 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.
|
||||
|
||||
To submit changes to this repository, see http://golang.org/doc/contribute.html.
|
||||
@@ -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,57 @@
|
||||
// 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"`
|
||||
Href string `xml:"href,attr"`
|
||||
}
|
||||
|
||||
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,424 @@
|
||||
// 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
|
||||
|
||||
root := filepath.Join(cfg.TemplatePath, "root.tmpl")
|
||||
parse := func(name string) (*template.Template, error) {
|
||||
t := template.New("").Funcs(funcMap)
|
||||
return t.ParseFiles(root, filepath.Join(cfg.TemplatePath, name))
|
||||
}
|
||||
|
||||
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) }
|
||||
@@ -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,334 @@
|
||||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// callgraph: a tool for reporting the call graph of a Go program.
|
||||
// See Usage for details, or run with -help.
|
||||
package main // import "golang.org/x/tools/cmd/callgraph"
|
||||
|
||||
// TODO(adonovan):
|
||||
//
|
||||
// Features:
|
||||
// - restrict graph to a single package
|
||||
// - output
|
||||
// - functions reachable from root (use digraph tool?)
|
||||
// - unreachable functions (use digraph tool?)
|
||||
// - dynamic (runtime) types
|
||||
// - indexed output (numbered nodes)
|
||||
// - JSON output
|
||||
// - additional template fields:
|
||||
// callee file/line/col
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/build"
|
||||
"go/token"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"text/template"
|
||||
|
||||
"golang.org/x/tools/go/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"
|
||||
)
|
||||
|
||||
var algoFlag = flag.String("algo", "rta",
|
||||
`Call graph construction algorithm, one of "rta" or "pta"`)
|
||||
|
||||
var testFlag = flag.Bool("test", false,
|
||||
"Loads test code (*_test.go) for imported packages")
|
||||
|
||||
var formatFlag = flag.String("format",
|
||||
"{{.Caller}}\t--{{.Dynamic}}-{{.Line}}:{{.Column}}-->\t{{.Callee}}",
|
||||
"A template expression specifying how to format an edge")
|
||||
|
||||
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.Object.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.Object.Path}} -> {{.Callee.Pkg.Object.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,
|
||||
SourceImports: true,
|
||||
}
|
||||
|
||||
if len(args) == 0 {
|
||||
fmt.Fprintln(os.Stderr, Usage)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Use the initial packages from the command line.
|
||||
args, 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 := ssa.Create(iprog, 0)
|
||||
prog.BuildAll()
|
||||
|
||||
// -- call graph construction ------------------------------------------
|
||||
|
||||
var cg *callgraph.Graph
|
||||
|
||||
switch algo {
|
||||
case "static":
|
||||
cg = static.CallGraph(prog)
|
||||
|
||||
case "cha":
|
||||
cg = cha.CallGraph(prog)
|
||||
|
||||
case "pta":
|
||||
main, err := mainPackage(prog, tests)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config := &pointer.Config{
|
||||
Mains: []*ssa.Package{main},
|
||||
BuildCallGraph: true,
|
||||
}
|
||||
ptares, err := pointer.Analyze(config)
|
||||
if err != nil {
|
||||
return err // internal error in pointer analysis
|
||||
}
|
||||
cg = ptares.CallGraph
|
||||
|
||||
case "rta":
|
||||
main, err := mainPackage(prog, tests)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
roots := []*ssa.Function{
|
||||
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
|
||||
}
|
||||
|
||||
// mainPackage returns the main package to analyze.
|
||||
// The resulting package has a main() function.
|
||||
func mainPackage(prog *ssa.Program, tests bool) (*ssa.Package, error) {
|
||||
pkgs := prog.AllPackages()
|
||||
|
||||
// TODO(adonovan): allow independent control over tests, mains and libraries.
|
||||
// TODO(adonovan): put this logic in a library; we keep reinventing it.
|
||||
|
||||
if tests {
|
||||
// If -test, use all packages' tests.
|
||||
if len(pkgs) > 0 {
|
||||
if main := prog.CreateTestMainPackage(pkgs...); main != nil {
|
||||
return main, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("no tests")
|
||||
}
|
||||
|
||||
// Otherwise, use the first package named main.
|
||||
for _, pkg := range pkgs {
|
||||
if pkg.Object.Name() == "main" {
|
||||
if pkg.Func("main") == nil {
|
||||
return nil, fmt.Errorf("no func main() in main package")
|
||||
}
|
||||
return pkg, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no main package")
|
||||
}
|
||||
|
||||
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,73 @@
|
||||
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.Example --> (pkg.C).f`,
|
||||
`test$main.init --> pkg.init`,
|
||||
}},
|
||||
{"pta", format, true, []string{
|
||||
`<root> --> pkg.Example`,
|
||||
`<root> --> test$main.init`,
|
||||
`pkg.Example --> (pkg.C).f`,
|
||||
`test$main.init --> pkg.init`,
|
||||
}},
|
||||
} {
|
||||
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
|
||||
}
|
||||
@@ -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,652 @@
|
||||
// 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"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
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, 0)
|
||||
if err != nil {
|
||||
log.Fatalf("cover: %s: %s", name, err)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
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:
|
||||
return s.Body.Lbrace
|
||||
case *ast.ForStmt:
|
||||
return s.Body.Lbrace
|
||||
case *ast.LabeledStmt:
|
||||
return f.statementBoundary(s.Stmt)
|
||||
case *ast.RangeStmt:
|
||||
// Ranges might loop over things with function literals.: for _ = range []func(){ ... } {.
|
||||
// TODO: There are a few other such possibilities, but they're extremely unlikely.
|
||||
found, pos := hasFuncLiteral(s.X)
|
||||
if found {
|
||||
return pos
|
||||
}
|
||||
return s.Body.Lbrace
|
||||
case *ast.SwitchStmt:
|
||||
return s.Body.Lbrace
|
||||
case *ast.SelectStmt:
|
||||
return s.Body.Lbrace
|
||||
case *ast.TypeSwitchStmt:
|
||||
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
|
||||
}
|
||||
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,87 @@
|
||||
// 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 (
|
||||
"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)
|
||||
|
||||
// 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,21 @@
|
||||
// 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
|
||||
*/
|
||||
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,281 @@
|
||||
// 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)
|
||||
}
|
||||
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,93 @@
|
||||
// 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
|
||||
}
|
||||
}
|
||||
if !PASS {
|
||||
fmt.Fprintf(os.Stderr, "FAIL\n")
|
||||
os.Exit(2)
|
||||
}
|
||||
}
|
||||
|
||||
// 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,177 @@
|
||||
// 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()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testFor() {
|
||||
for i := 0; i < 10; i++ {
|
||||
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; i++ {
|
||||
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 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,140 @@
|
||||
// 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/parser"
|
||||
"go/printer"
|
||||
"go/token"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"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")
|
||||
)
|
||||
|
||||
const usage = `eg: an example-based refactoring tool.
|
||||
|
||||
Usage: eg -t template.go [-w] [-transitive] <args>...
|
||||
-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.
|
||||
` + 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 *templateFlag == "" {
|
||||
return fmt.Errorf("no -t template.go file specified")
|
||||
}
|
||||
|
||||
conf := loader.Config{
|
||||
Fset: token.NewFileSet(),
|
||||
ParserMode: parser.ParseComments,
|
||||
SourceImports: true,
|
||||
}
|
||||
|
||||
// The first Created package is the template.
|
||||
conf.CreateFromFilenames("template", *templateFlag)
|
||||
|
||||
if len(args) == 0 {
|
||||
fmt.Fprint(os.Stderr, usage)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
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, *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 {
|
||||
printer.Fprint(os.Stdout, iprog.Fset, file)
|
||||
}
|
||||
}
|
||||
}
|
||||
if hadErrors {
|
||||
os.Exit(1)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -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,15 @@
|
||||
// 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 (
|
||||
"golang.org/x/tools/go/gcimporter"
|
||||
)
|
||||
|
||||
func init() {
|
||||
register("gc", gcimporter.Import)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// 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 (
|
||||
"golang.org/x/tools/go/gccgoimporter"
|
||||
"golang.org/x/tools/go/types"
|
||||
)
|
||||
|
||||
var (
|
||||
initmap = make(map[*types.Package]gccgoimporter.InitData)
|
||||
)
|
||||
|
||||
func init() {
|
||||
incpaths := []string{"/"}
|
||||
|
||||
// importer for default gccgo
|
||||
var inst gccgoimporter.GccgoInstallation
|
||||
inst.InitFromDriver("gccgo")
|
||||
register("gccgo", inst.GetImporter(incpaths, initmap))
|
||||
}
|
||||
|
||||
// Print the extra gccgo compiler data for this package, if it exists.
|
||||
func (p *printer) printGccgoExtra(pkg *types.Package) {
|
||||
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,207 @@
|
||||
// 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"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/tools/go/types"
|
||||
)
|
||||
|
||||
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")
|
||||
)
|
||||
|
||||
// map of imported packages
|
||||
var packages = make(map[string]*types.Package)
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
imp := tryImports
|
||||
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(packages, 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(packages map[string]*types.Package, 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(packages, actual)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
logf("\t=> importing %q failed: %s\n", actual, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// tryImports is an importer that tries all registered importers
|
||||
// successively until one of them succeeds or all of them failed.
|
||||
func tryImports(packages map[string]*types.Package, path string) (pkg *types.Package, err error) {
|
||||
for i, imp := range importers {
|
||||
logf("\t\ttrying %s import\n", sources[i])
|
||||
pkg, err = imp(packages, path)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
logf("\t\t=> %s import failed: %s\n", sources[i], err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// protect protects an importer imp from panics and returns the protected importer.
|
||||
func protect(imp types.Importer) types.Importer {
|
||||
return func(packages map[string]*types.Package, path string) (pkg *types.Package, err error) {
|
||||
defer func() {
|
||||
if recover() != nil {
|
||||
pkg = nil
|
||||
err = importFailed
|
||||
}
|
||||
}()
|
||||
return imp(packages, path)
|
||||
}
|
||||
}
|
||||
|
||||
// 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,368 @@
|
||||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/token"
|
||||
"io"
|
||||
"math/big"
|
||||
|
||||
"golang.org/x/tools/go/exact"
|
||||
"golang.org/x/tools/go/types"
|
||||
)
|
||||
|
||||
// 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())
|
||||
p.writeType(p.pkg, obj.Type().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 (
|
||||
"golang.org/x/tools/go/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
register("source", sourceImporter)
|
||||
}
|
||||
|
||||
func sourceImporter(packages map[string]*types.Package, 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 "golang.org/x/tools/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,66 @@
|
||||
// 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"
|
||||
"path"
|
||||
"regexp"
|
||||
|
||||
"golang.org/x/tools/godoc"
|
||||
"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"
|
||||
)
|
||||
|
||||
func init() {
|
||||
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)
|
||||
}
|
||||
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)
|
||||
registerHandlers(pres)
|
||||
|
||||
log.Println("godoc initialization complete")
|
||||
}
|
||||
@@ -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,14 @@
|
||||
// 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 "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("http://golang.org/dl/", http.StatusFound))
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
// 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.
|
||||
|
||||
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 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,362 @@
|
||||
// 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"
|
||||
)
|
||||
|
||||
var godocTests = []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{
|
||||
`no such file or directory|does not exist|cannot find the file`,
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"fmt", "NonexistentSymbol"},
|
||||
matches: []string{
|
||||
`No match found\.`,
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"-src", "syscall", "Open"},
|
||||
matches: []string{
|
||||
`func Open\(`,
|
||||
},
|
||||
dontmatch: []string{
|
||||
`No match found\.`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// 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()) {
|
||||
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) }
|
||||
}
|
||||
|
||||
// Basic regression test for godoc command-line tool.
|
||||
func TestCLI(t *testing.T) {
|
||||
bin, cleanup := buildGodoc(t)
|
||||
defer cleanup()
|
||||
for _, test := range godocTests {
|
||||
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 waitForServer(t *testing.T, address string) {
|
||||
// Poll every 50ms for a total of 5s.
|
||||
for i := 0; i < 100; i++ {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
conn, err := net.Dial("tcp", address)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
t.Fatalf("Server %q failed to respond in 5 seconds", address)
|
||||
}
|
||||
|
||||
func killAndWait(cmd *exec.Cmd) {
|
||||
cmd.Process.Kill()
|
||||
cmd.Wait()
|
||||
}
|
||||
|
||||
// Basic integration test for godoc HTTP interface.
|
||||
func TestWeb(t *testing.T) {
|
||||
bin, cleanup := buildGodoc(t)
|
||||
defer cleanup()
|
||||
addr := serverAddress(t)
|
||||
cmd := exec.Command(bin, fmt.Sprintf("-http=%s", addr))
|
||||
cmd.Stdout = os.Stderr
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Args[0] = "godoc"
|
||||
if err := cmd.Start(); err != nil {
|
||||
t.Fatalf("failed to start godoc: %s", err)
|
||||
}
|
||||
defer killAndWait(cmd)
|
||||
waitForServer(t, addr)
|
||||
tests := []struct {
|
||||
path string
|
||||
match []string
|
||||
dontmatch []string
|
||||
}{
|
||||
{
|
||||
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",
|
||||
},
|
||||
dontmatch: []string{
|
||||
"cmd/gc",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
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) {
|
||||
// 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)
|
||||
waitForServer(t, addr)
|
||||
|
||||
// Wait for type analysis to complete.
|
||||
reader := bufio.NewReader(stderr)
|
||||
for {
|
||||
s, err := reader.ReadString('\n')
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// 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 (
|
||||
"log"
|
||||
"net/http"
|
||||
"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{}
|
||||
)
|
||||
|
||||
func registerHandlers(pres *godoc.Presentation) {
|
||||
if pres == nil {
|
||||
panic("nil Presentation")
|
||||
}
|
||||
http.HandleFunc("/doc/codewalk/", codewalk)
|
||||
http.Handle("/doc/play/", pres.FileServer())
|
||||
http.Handle("/robots.txt", pres.FileServer())
|
||||
http.Handle("/", pres)
|
||||
http.Handle("/pkg/C/", redirect.Handler("/cmd/cgo/"))
|
||||
redirect.Register(nil)
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
// 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
|
||||
toolsPath = "golang.org/x/tools/cmd/"
|
||||
)
|
||||
|
||||
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", "", "directory containing alternate template files")
|
||||
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")
|
||||
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 main() {
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
|
||||
playEnabled = *showPlayground
|
||||
|
||||
// Check usage: either server and no args, command line and args, or index creation mode
|
||||
if (*httpAddr != "" || *urlFlag != "") != (flag.NArg() == 0) && !*writeIndex {
|
||||
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.IndexThrottle = *indexThrottle
|
||||
if *writeIndex {
|
||||
corpus.IndexThrottle = 1.0
|
||||
corpus.IndexEnabled = true
|
||||
}
|
||||
if *writeIndex || httpMode || *urlFlag != "" {
|
||||
if err := corpus.Init(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// Start 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// 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.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"go/format"
|
||||
"net/http"
|
||||
|
||||
// This package registers "/compile" and "/share" handlers
|
||||
// that redirect to the golang.org playground.
|
||||
_ "golang.org/x/tools/playground"
|
||||
)
|
||||
|
||||
func init() {
|
||||
http.HandleFunc("/fmt", fmtHandler)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
|
||||
// disabledHandler serves a 501 "Not Implemented" response.
|
||||
func disabledHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotImplemented)
|
||||
fmt.Fprint(w, "This functionality is not available via local godoc.")
|
||||
}
|
||||
@@ -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) -type f -depth 1) $APPDIR/$GODOC/
|
||||
}
|
||||
|
||||
makeAppYaml() {
|
||||
echo "*** make $APPDIR/app.yaml"
|
||||
cat > $APPDIR/app.yaml <<EOF
|
||||
application: godoc
|
||||
version: 1
|
||||
runtime: go
|
||||
api_version: go1.4beta
|
||||
|
||||
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,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.
|
||||
|
||||
// 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"},
|
||||
|
||||
"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"},
|
||||
"mobile": {"https://go.googlesource.com/mobile", "git"},
|
||||
"net": {"https://go.googlesource.com/net", "git"},
|
||||
"oauth2": {"https://go.googlesource.com/oauth2", "git"},
|
||||
"playground": {"https://go.googlesource.com/playground", "git"},
|
||||
"review": {"https://go.googlesource.com/review", "git"},
|
||||
"sys": {"https://go.googlesource.com/sys", "git"},
|
||||
"talks": {"https://go.googlesource.com/talks", "git"},
|
||||
"text": {"https://go.googlesource.com/text", "git"},
|
||||
"tools": {"https://go.googlesource.com/tools", "git"},
|
||||
"tour": {"https://go.googlesource.com/tour", "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:]
|
||||
}
|
||||
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,33 @@
|
||||
/*
|
||||
|
||||
Command goimports updates your Go import lines,
|
||||
adding missing ones and removing unreferenced ones.
|
||||
|
||||
$ go get golang.org/x/tools/cmd/goimports
|
||||
|
||||
It's a drop-in replacement for your editor's gofmt-on-save hook.
|
||||
It has the same command-line interface as gofmt and formats
|
||||
your code in the same way.
|
||||
|
||||
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-load)
|
||||
(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.
|
||||
|
||||
Happy hacking!
|
||||
|
||||
*/
|
||||
package main // import "golang.org/x/tools/cmd/goimports"
|
||||
@@ -0,0 +1,195 @@
|
||||
// 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/scanner"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"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")
|
||||
|
||||
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)")
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
func processFile(filename string, in io.Reader, out io.Writer, stdin bool) error {
|
||||
opt := options
|
||||
if stdin {
|
||||
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
|
||||
}
|
||||
|
||||
res, err := imports.Process(filename, src, opt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !bytes.Equal(src, res) {
|
||||
// formatting has changed
|
||||
if *list {
|
||||
fmt.Fprintln(out, filename)
|
||||
}
|
||||
if *write {
|
||||
err = ioutil.WriteFile(filename, res, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if *doDiff {
|
||||
data, err := diff(src, res)
|
||||
if err != nil {
|
||||
return fmt.Errorf("computing diff: %s", err)
|
||||
}
|
||||
fmt.Printf("diff %s gofmt/%s\n", filename, 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, false)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
func gofmtMain() {
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
|
||||
if options.TabWidth < 0 {
|
||||
fmt.Fprintf(os.Stderr, "negative tabwidth %d\n", options.TabWidth)
|
||||
exitCode = 2
|
||||
return
|
||||
}
|
||||
|
||||
if flag.NArg() == 0 {
|
||||
if err := processFile("<standard input>", os.Stdin, os.Stdout, true); err != nil {
|
||||
report(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for i := 0; i < flag.NArg(); i++ {
|
||||
path := flag.Arg(i)
|
||||
switch dir, err := os.Stat(path); {
|
||||
case err != nil:
|
||||
report(err)
|
||||
case dir.IsDir():
|
||||
walkDir(path)
|
||||
default:
|
||||
if err := processFile(path, nil, os.Stdout, false); err != nil {
|
||||
report(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func diff(b1, b2 []byte) (data []byte, err error) {
|
||||
f1, err := ioutil.TempFile("", "gofmt")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer os.Remove(f1.Name())
|
||||
defer f1.Close()
|
||||
|
||||
f2, err := ioutil.TempFile("", "gofmt")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer os.Remove(f2.Name())
|
||||
defer f2.Close()
|
||||
|
||||
f1.Write(b1)
|
||||
f2.Write(b2)
|
||||
|
||||
data, err = exec.Command("diff", "-u", f1.Name(), f2.Name()).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.
|
||||
err = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// licence that can be found in the LICENSE file.
|
||||
|
||||
// The gomvpkg command moves go packages, updating import declarations.
|
||||
// See the -help message or Usage constant for details.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/build"
|
||||
"os"
|
||||
|
||||
"golang.org/x/tools/refactor/rename"
|
||||
)
|
||||
|
||||
var (
|
||||
fromFlag = flag.String("from", "", "Import path of package to be moved")
|
||||
toFlag = flag.String("to", "", "Destination import path for package")
|
||||
vcsMvCmdFlag = flag.String("vcs_mv_cmd", "", `A template for the version control system's "move directory" command, e.g. "git mv {{.Src}} {{.Dst}}`)
|
||||
helpFlag = flag.Bool("help", false, "show usage message")
|
||||
)
|
||||
|
||||
const Usage = `gomvpkg: moves a package, updating import declarations
|
||||
|
||||
Usage:
|
||||
|
||||
gomvpkg -from <path> -to <path> [-vcs_mv_cmd <template>]
|
||||
|
||||
Flags:
|
||||
|
||||
-from specifies the import path of the package to be moved
|
||||
|
||||
-to specifies the destination import path
|
||||
|
||||
-vcs_mv_cmd specifies a shell command to inform the version control system of a
|
||||
directory move. The argument is a template using the syntax of the
|
||||
text/template package. It has two fields: Src and Dst, the absolute
|
||||
paths of the directories.
|
||||
|
||||
For example: "git mv {{.Src}} {{.Dst}}"
|
||||
|
||||
gomvpkg determines the set of packages that might be affected, including all
|
||||
packages importing the 'from' package and any of its subpackages. It will move
|
||||
the 'from' package and all its subpackages to the destination path and update all
|
||||
imports of those packages to point to its new import path.
|
||||
|
||||
gomvpkg rejects moves in which a package already exists at the destination import
|
||||
path, or in which a directory already exists at the location the package would be
|
||||
moved to.
|
||||
|
||||
gomvpkg will not always be able to rename imports when a package's name is changed.
|
||||
Import statements may want further cleanup.
|
||||
|
||||
gomvpkg's behavior is not defined if any of the packages to be moved are
|
||||
imported using dot imports.
|
||||
|
||||
Examples:
|
||||
|
||||
% gomvpkg -from myproject/foo -to myproject/bar
|
||||
|
||||
Move the package with import path "myproject/foo" to the new path
|
||||
"myproject/bar".
|
||||
|
||||
% gomvpkg -from myproject/foo -to myproject/bar -vcs_mv_cmd "git mv {{.Src}} {{.Dst}}"
|
||||
|
||||
Move the package with import path "myproject/foo" to the new path
|
||||
"myproject/bar" using "git mv" to execute the directory move.
|
||||
`
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
if len(flag.Args()) > 0 {
|
||||
fmt.Fprintln(os.Stderr, "gomvpkg: surplus arguments.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if *helpFlag || *fromFlag == "" || *toFlag == "" {
|
||||
fmt.Println(Usage)
|
||||
return
|
||||
}
|
||||
|
||||
if err := rename.Move(&build.Default, *fromFlag, *toFlag, *vcsMvCmdFlag); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "gomvpkg: %s.\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// The gorename command performs precise type-safe renaming of
|
||||
// identifiers in Go source code.
|
||||
//
|
||||
// Run with -help for usage information, or view the Usage constant in
|
||||
// package golang.org/x/tools/refactor/rename, which contains most of
|
||||
// the implementation.
|
||||
//
|
||||
package main // import "golang.org/x/tools/cmd/gorename"
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/build"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"golang.org/x/tools/refactor/rename"
|
||||
)
|
||||
|
||||
var (
|
||||
offsetFlag = flag.String("offset", "", "file and byte offset of identifier to be renamed, e.g. 'file.go:#123'. For use by editors.")
|
||||
fromFlag = flag.String("from", "", "identifier to be renamed; see -help for formats")
|
||||
toFlag = flag.String("to", "", "new name for identifier")
|
||||
helpFlag = flag.Bool("help", false, "show usage message")
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&rename.Force, "force", false, "proceed, even if conflicts were reported")
|
||||
flag.BoolVar(&rename.DryRun, "dryrun", false, "show the change, but do not apply it")
|
||||
flag.BoolVar(&rename.Verbose, "v", false, "print verbose information")
|
||||
|
||||
// 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 len(flag.Args()) > 0 {
|
||||
fmt.Fprintln(os.Stderr, "gorename: surplus arguments.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if *helpFlag || (*offsetFlag == "" && *fromFlag == "" && *toFlag == "") {
|
||||
fmt.Println(rename.Usage)
|
||||
return
|
||||
}
|
||||
|
||||
if err := rename.Main(&build.Default, *offsetFlag, *fromFlag, *toFlag); err != nil {
|
||||
if err != rename.ConflictError {
|
||||
fmt.Fprintf(os.Stderr, "gorename: %s\n", err)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// 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.
|
||||
|
||||
/*
|
||||
The gotype command does syntactic and semantic analysis of Go files
|
||||
and packages like the front-end of a Go compiler. Errors are reported
|
||||
if the analysis fails; otherwise gotype is quiet (unless -v is set).
|
||||
|
||||
Without a list of paths, gotype reads from standard input, which
|
||||
must provide a single Go source file defining a complete package.
|
||||
|
||||
If a single path is specified that is a directory, gotype checks
|
||||
the Go files in that directory; they must all belong to the same
|
||||
package.
|
||||
|
||||
Otherwise, each path must be the filename of Go file belonging to
|
||||
the same package.
|
||||
|
||||
Usage:
|
||||
gotype [flags] [path...]
|
||||
|
||||
The flags are:
|
||||
-a
|
||||
use all (incl. _test.go) files when processing a directory
|
||||
-e
|
||||
report all errors (not just the first 10)
|
||||
-v
|
||||
verbose mode
|
||||
-gccgo
|
||||
use gccimporter instead of gcimporter
|
||||
|
||||
Debugging flags:
|
||||
-seq
|
||||
parse sequentially, rather than in parallel
|
||||
-ast
|
||||
print AST (forces -seq)
|
||||
-trace
|
||||
print parse trace (forces -seq)
|
||||
-comments
|
||||
parse comments (ignored unless -ast or -trace is provided)
|
||||
|
||||
Examples:
|
||||
|
||||
To check the files a.go, b.go, and c.go:
|
||||
|
||||
gotype a.go b.go c.go
|
||||
|
||||
To check an entire package in the directory dir and print the processed files:
|
||||
|
||||
gotype -v dir
|
||||
|
||||
To check an entire package including tests in the local directory:
|
||||
|
||||
gotype -a .
|
||||
|
||||
To verify the output of a pipe:
|
||||
|
||||
echo "package foo" | gotype
|
||||
|
||||
*/
|
||||
package main // import "golang.org/x/tools/cmd/gotype"
|
||||
@@ -0,0 +1,262 @@
|
||||
// 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.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/build"
|
||||
"go/parser"
|
||||
"go/scanner"
|
||||
"go/token"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"golang.org/x/tools/go/gccgoimporter"
|
||||
_ "golang.org/x/tools/go/gcimporter"
|
||||
"golang.org/x/tools/go/types"
|
||||
)
|
||||
|
||||
var (
|
||||
// main operation modes
|
||||
allFiles = flag.Bool("a", false, "use all (incl. _test.go) files when processing a directory")
|
||||
allErrors = flag.Bool("e", false, "report all errors (not just the first 10)")
|
||||
verbose = flag.Bool("v", false, "verbose mode")
|
||||
gccgo = flag.Bool("gccgo", false, "use gccgoimporter instead of gcimporter")
|
||||
|
||||
// debugging support
|
||||
sequential = flag.Bool("seq", false, "parse sequentially, rather than in parallel")
|
||||
printAST = flag.Bool("ast", false, "print AST (forces -seq)")
|
||||
printTrace = flag.Bool("trace", false, "print parse trace (forces -seq)")
|
||||
parseComments = flag.Bool("comments", false, "parse comments (ignored unless -ast or -trace is provided)")
|
||||
)
|
||||
|
||||
var (
|
||||
fset = token.NewFileSet()
|
||||
errorCount = 0
|
||||
parserMode parser.Mode
|
||||
sizes types.Sizes
|
||||
)
|
||||
|
||||
func initParserMode() {
|
||||
if *allErrors {
|
||||
parserMode |= parser.AllErrors
|
||||
}
|
||||
if *printTrace {
|
||||
parserMode |= parser.Trace
|
||||
}
|
||||
if *parseComments && (*printAST || *printTrace) {
|
||||
parserMode |= parser.ParseComments
|
||||
}
|
||||
}
|
||||
|
||||
func initSizes() {
|
||||
wordSize := 8
|
||||
maxAlign := 8
|
||||
switch build.Default.GOARCH {
|
||||
case "386", "arm":
|
||||
wordSize = 4
|
||||
maxAlign = 4
|
||||
// add more cases as needed
|
||||
}
|
||||
sizes = &types.StdSizes{WordSize: int64(wordSize), MaxAlign: int64(maxAlign)}
|
||||
}
|
||||
|
||||
func usage() {
|
||||
fmt.Fprintln(os.Stderr, "usage: gotype [flags] [path ...]")
|
||||
flag.PrintDefaults()
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
func report(err error) {
|
||||
scanner.PrintError(os.Stderr, err)
|
||||
if list, ok := err.(scanner.ErrorList); ok {
|
||||
errorCount += len(list)
|
||||
return
|
||||
}
|
||||
errorCount++
|
||||
}
|
||||
|
||||
// parse may be called concurrently
|
||||
func parse(filename string, src interface{}) (*ast.File, error) {
|
||||
if *verbose {
|
||||
fmt.Println(filename)
|
||||
}
|
||||
file, err := parser.ParseFile(fset, filename, src, parserMode) // ok to access fset concurrently
|
||||
if *printAST {
|
||||
ast.Print(fset, file)
|
||||
}
|
||||
return file, err
|
||||
}
|
||||
|
||||
func parseStdin() (*ast.File, error) {
|
||||
src, err := ioutil.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parse("<standard input>", src)
|
||||
}
|
||||
|
||||
func parseFiles(filenames []string) ([]*ast.File, error) {
|
||||
files := make([]*ast.File, len(filenames))
|
||||
|
||||
if *sequential {
|
||||
for i, filename := range filenames {
|
||||
var err error
|
||||
files[i], err = parse(filename, nil)
|
||||
if err != nil {
|
||||
return nil, err // leave unfinished goroutines hanging
|
||||
}
|
||||
}
|
||||
} else {
|
||||
type parseResult struct {
|
||||
file *ast.File
|
||||
err error
|
||||
}
|
||||
|
||||
out := make(chan parseResult)
|
||||
for _, filename := range filenames {
|
||||
go func(filename string) {
|
||||
file, err := parse(filename, nil)
|
||||
out <- parseResult{file, err}
|
||||
}(filename)
|
||||
}
|
||||
|
||||
for i := range filenames {
|
||||
res := <-out
|
||||
if res.err != nil {
|
||||
return nil, res.err // leave unfinished goroutines hanging
|
||||
}
|
||||
files[i] = res.file
|
||||
}
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func parseDir(dirname string) ([]*ast.File, error) {
|
||||
ctxt := build.Default
|
||||
pkginfo, err := ctxt.ImportDir(dirname, 0)
|
||||
if _, nogo := err.(*build.NoGoError); err != nil && !nogo {
|
||||
return nil, err
|
||||
}
|
||||
filenames := append(pkginfo.GoFiles, pkginfo.CgoFiles...)
|
||||
if *allFiles {
|
||||
filenames = append(filenames, pkginfo.TestGoFiles...)
|
||||
}
|
||||
|
||||
// complete file names
|
||||
for i, filename := range filenames {
|
||||
filenames[i] = filepath.Join(dirname, filename)
|
||||
}
|
||||
|
||||
return parseFiles(filenames)
|
||||
}
|
||||
|
||||
func getPkgFiles(args []string) ([]*ast.File, error) {
|
||||
if len(args) == 0 {
|
||||
// stdin
|
||||
file, err := parseStdin()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []*ast.File{file}, nil
|
||||
}
|
||||
|
||||
if len(args) == 1 {
|
||||
// possibly a directory
|
||||
path := args[0]
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if info.IsDir() {
|
||||
return parseDir(path)
|
||||
}
|
||||
}
|
||||
|
||||
// list of files
|
||||
return parseFiles(args)
|
||||
}
|
||||
|
||||
func checkPkgFiles(files []*ast.File) {
|
||||
type bailout struct{}
|
||||
conf := types.Config{
|
||||
FakeImportC: true,
|
||||
Error: func(err error) {
|
||||
if !*allErrors && errorCount >= 10 {
|
||||
panic(bailout{})
|
||||
}
|
||||
report(err)
|
||||
},
|
||||
Sizes: sizes,
|
||||
}
|
||||
if *gccgo {
|
||||
var inst gccgoimporter.GccgoInstallation
|
||||
inst.InitFromDriver("gccgo")
|
||||
conf.Import = inst.GetImporter(nil, nil)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
switch p := recover().(type) {
|
||||
case nil, bailout:
|
||||
// normal return or early exit
|
||||
default:
|
||||
// re-panic
|
||||
panic(p)
|
||||
}
|
||||
}()
|
||||
|
||||
const path = "pkg" // any non-empty string will do for now
|
||||
conf.Check(path, fset, files, nil)
|
||||
}
|
||||
|
||||
func printStats(d time.Duration) {
|
||||
fileCount := 0
|
||||
lineCount := 0
|
||||
fset.Iterate(func(f *token.File) bool {
|
||||
fileCount++
|
||||
lineCount += f.LineCount()
|
||||
return true
|
||||
})
|
||||
|
||||
fmt.Printf(
|
||||
"%s (%d files, %d lines, %d lines/s)\n",
|
||||
d, fileCount, lineCount, int64(float64(lineCount)/d.Seconds()),
|
||||
)
|
||||
}
|
||||
|
||||
func main() {
|
||||
runtime.GOMAXPROCS(runtime.NumCPU()) // remove this once runtime is smarter
|
||||
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
if *printAST || *printTrace {
|
||||
*sequential = true
|
||||
}
|
||||
initParserMode()
|
||||
initSizes()
|
||||
|
||||
start := time.Now()
|
||||
|
||||
files, err := getPkgFiles(flag.Args())
|
||||
if err != nil {
|
||||
report(err)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
checkPkgFiles(files)
|
||||
if errorCount > 0 {
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
if *verbose {
|
||||
printStats(time.Since(start))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
// 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 takes an HTML file and outputs a corresponding article file in
|
||||
// present format. See: golang.org/x/tools/present
|
||||
package main // import "golang.org/x/tools/cmd/html2article"
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
"golang.org/x/net/html/atom"
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
err := convert(os.Stdout, os.Stdin)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func convert(w io.Writer, r io.Reader) error {
|
||||
root, err := html.Parse(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
style := find(root, isTag(atom.Style))
|
||||
parseStyles(style)
|
||||
|
||||
body := find(root, isTag(atom.Body))
|
||||
if body == nil {
|
||||
return errors.New("couldn't find body")
|
||||
}
|
||||
article := limitNewlineRuns(makeHeadings(strings.TrimSpace(text(body))))
|
||||
_, err = fmt.Fprintf(w, "Title\n\n%s", article)
|
||||
return err
|
||||
}
|
||||
|
||||
type Style string
|
||||
|
||||
const (
|
||||
Bold Style = "*"
|
||||
Italic Style = "_"
|
||||
Code Style = "`"
|
||||
)
|
||||
|
||||
var cssRules = make(map[string]Style)
|
||||
|
||||
func parseStyles(style *html.Node) {
|
||||
if style == nil || style.FirstChild == nil {
|
||||
log.Println("couldn't find styles")
|
||||
return
|
||||
}
|
||||
s := bufio.NewScanner(strings.NewReader(style.FirstChild.Data))
|
||||
|
||||
findRule := func(b []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||
if i := bytes.Index(b, []byte("{")); i >= 0 {
|
||||
token = bytes.TrimSpace(b[:i])
|
||||
advance = i
|
||||
}
|
||||
return
|
||||
}
|
||||
findBody := func(b []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||
if len(b) == 0 {
|
||||
return
|
||||
}
|
||||
if b[0] != '{' {
|
||||
err = fmt.Errorf("expected {, got %c", b[0])
|
||||
return
|
||||
}
|
||||
if i := bytes.Index(b, []byte("}")); i < 0 {
|
||||
err = fmt.Errorf("can't find closing }")
|
||||
return
|
||||
} else {
|
||||
token = b[1:i]
|
||||
advance = i + 1
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
s.Split(findRule)
|
||||
for s.Scan() {
|
||||
rule := s.Text()
|
||||
s.Split(findBody)
|
||||
if !s.Scan() {
|
||||
break
|
||||
}
|
||||
b := strings.ToLower(s.Text())
|
||||
switch {
|
||||
case strings.Contains(b, "italic"):
|
||||
cssRules[rule] = Italic
|
||||
case strings.Contains(b, "bold"):
|
||||
cssRules[rule] = Bold
|
||||
case strings.Contains(b, "Consolas") || strings.Contains(b, "Courier New"):
|
||||
cssRules[rule] = Code
|
||||
}
|
||||
s.Split(findRule)
|
||||
}
|
||||
if err := s.Err(); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
var newlineRun = regexp.MustCompile(`\n\n+`)
|
||||
|
||||
func limitNewlineRuns(s string) string {
|
||||
return newlineRun.ReplaceAllString(s, "\n\n")
|
||||
}
|
||||
|
||||
func makeHeadings(body string) string {
|
||||
buf := new(bytes.Buffer)
|
||||
lines := strings.Split(body, "\n")
|
||||
for i, s := range lines {
|
||||
if i == 0 && !isBoldTitle(s) {
|
||||
buf.WriteString("* Introduction\n\n")
|
||||
}
|
||||
if isBoldTitle(s) {
|
||||
s = strings.TrimSpace(strings.Replace(s, "*", " ", -1))
|
||||
s = "* " + s
|
||||
}
|
||||
buf.WriteString(s)
|
||||
buf.WriteByte('\n')
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func isBoldTitle(s string) bool {
|
||||
return !strings.Contains(s, " ") &&
|
||||
strings.HasPrefix(s, "*") &&
|
||||
strings.HasSuffix(s, "*")
|
||||
}
|
||||
|
||||
func indent(buf *bytes.Buffer, s string) {
|
||||
for _, l := range strings.Split(s, "\n") {
|
||||
if l != "" {
|
||||
buf.WriteByte('\t')
|
||||
buf.WriteString(l)
|
||||
}
|
||||
buf.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
|
||||
func unwrap(buf *bytes.Buffer, s string) {
|
||||
var cont bool
|
||||
for _, l := range strings.Split(s, "\n") {
|
||||
l = strings.TrimSpace(l)
|
||||
if len(l) == 0 {
|
||||
if cont {
|
||||
buf.WriteByte('\n')
|
||||
buf.WriteByte('\n')
|
||||
}
|
||||
cont = false
|
||||
} else {
|
||||
if cont {
|
||||
buf.WriteByte(' ')
|
||||
}
|
||||
buf.WriteString(l)
|
||||
cont = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func text(n *html.Node) string {
|
||||
var buf bytes.Buffer
|
||||
walk(n, func(n *html.Node) bool {
|
||||
switch n.Type {
|
||||
case html.TextNode:
|
||||
buf.WriteString(n.Data)
|
||||
return false
|
||||
case html.ElementNode:
|
||||
// no-op
|
||||
default:
|
||||
return true
|
||||
}
|
||||
a := n.DataAtom
|
||||
if a == atom.Span {
|
||||
switch {
|
||||
case hasStyle(Code)(n):
|
||||
a = atom.Code
|
||||
case hasStyle(Bold)(n):
|
||||
a = atom.B
|
||||
case hasStyle(Italic)(n):
|
||||
a = atom.I
|
||||
}
|
||||
}
|
||||
switch a {
|
||||
case atom.Br:
|
||||
buf.WriteByte('\n')
|
||||
case atom.P:
|
||||
unwrap(&buf, childText(n))
|
||||
buf.WriteString("\n\n")
|
||||
case atom.Li:
|
||||
buf.WriteString("- ")
|
||||
unwrap(&buf, childText(n))
|
||||
buf.WriteByte('\n')
|
||||
case atom.Pre:
|
||||
indent(&buf, childText(n))
|
||||
buf.WriteByte('\n')
|
||||
case atom.A:
|
||||
href, text := attr(n, "href"), childText(n)
|
||||
// Skip links with no text.
|
||||
if strings.TrimSpace(text) == "" {
|
||||
break
|
||||
}
|
||||
// Don't emit empty links.
|
||||
if strings.TrimSpace(href) == "" {
|
||||
buf.WriteString(text)
|
||||
break
|
||||
}
|
||||
// Use original url for Google Docs redirections.
|
||||
if u, err := url.Parse(href); err != nil {
|
||||
log.Printf("parsing url %q: %v", href, err)
|
||||
} else if u.Host == "www.google.com" && u.Path == "/url" {
|
||||
href = u.Query().Get("q")
|
||||
}
|
||||
fmt.Fprintf(&buf, "[[%s][%s]]", href, text)
|
||||
case atom.Code:
|
||||
buf.WriteString(highlight(n, "`"))
|
||||
case atom.B:
|
||||
buf.WriteString(highlight(n, "*"))
|
||||
case atom.I:
|
||||
buf.WriteString(highlight(n, "_"))
|
||||
case atom.Img:
|
||||
src := attr(n, "src")
|
||||
fmt.Fprintf(&buf, ".image %s\n", src)
|
||||
case atom.Iframe:
|
||||
src, w, h := attr(n, "src"), attr(n, "width"), attr(n, "height")
|
||||
fmt.Fprintf(&buf, "\n.iframe %s %s %s\n", src, h, w)
|
||||
case atom.Param:
|
||||
if attr(n, "name") == "movie" {
|
||||
// Old style YouTube embed.
|
||||
u := attr(n, "value")
|
||||
u = strings.Replace(u, "/v/", "/embed/", 1)
|
||||
if i := strings.Index(u, "&"); i >= 0 {
|
||||
u = u[:i]
|
||||
}
|
||||
fmt.Fprintf(&buf, "\n.iframe %s 540 304\n", u)
|
||||
}
|
||||
case atom.Title:
|
||||
default:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func childText(node *html.Node) string {
|
||||
var buf bytes.Buffer
|
||||
for n := node.FirstChild; n != nil; n = n.NextSibling {
|
||||
fmt.Fprint(&buf, text(n))
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func highlight(node *html.Node, char string) string {
|
||||
t := strings.Replace(childText(node), " ", char, -1)
|
||||
return fmt.Sprintf("%s%s%s", char, t, char)
|
||||
}
|
||||
|
||||
type selector func(*html.Node) bool
|
||||
|
||||
func isTag(a atom.Atom) selector {
|
||||
return func(n *html.Node) bool {
|
||||
return n.DataAtom == a
|
||||
}
|
||||
}
|
||||
|
||||
func hasClass(name string) selector {
|
||||
return func(n *html.Node) bool {
|
||||
for _, a := range n.Attr {
|
||||
if a.Key == "class" {
|
||||
for _, c := range strings.Fields(a.Val) {
|
||||
if c == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func hasStyle(s Style) selector {
|
||||
return func(n *html.Node) bool {
|
||||
for rule, s2 := range cssRules {
|
||||
if s2 != s {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(rule, ".") && hasClass(rule[1:])(n) {
|
||||
return true
|
||||
}
|
||||
if n.DataAtom.String() == rule {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func hasAttr(key, val string) selector {
|
||||
return func(n *html.Node) bool {
|
||||
for _, a := range n.Attr {
|
||||
if a.Key == key && a.Val == val {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func attr(node *html.Node, key string) (value string) {
|
||||
for _, attr := range node.Attr {
|
||||
if attr.Key == key {
|
||||
return attr.Val
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func findAll(node *html.Node, fn selector) (nodes []*html.Node) {
|
||||
walk(node, func(n *html.Node) bool {
|
||||
if fn(n) {
|
||||
nodes = append(nodes, n)
|
||||
}
|
||||
return true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func find(n *html.Node, fn selector) *html.Node {
|
||||
var result *html.Node
|
||||
walk(n, func(n *html.Node) bool {
|
||||
if result != nil {
|
||||
return false
|
||||
}
|
||||
if fn(n) {
|
||||
result = n
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
func walk(n *html.Node, fn selector) {
|
||||
if fn(n) {
|
||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||
walk(c, fn)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Simple test of Go oracle/Emacs integration.
|
||||
# Requires that GOROOT and GOPATH are set.
|
||||
# Side effect: builds and installs oracle in $GOROOT.
|
||||
|
||||
set -eu
|
||||
|
||||
[ -z "$GOROOT" ] && { echo "Error: GOROOT is unset." >&2; exit 1; }
|
||||
[ -z "$GOPATH" ] && { echo "Error: GOPATH is unset." >&2; exit 1; }
|
||||
|
||||
log=/tmp/$(basename $0)-$$.log
|
||||
thisdir=$(dirname $0)
|
||||
|
||||
function die() {
|
||||
echo "Error: $@."
|
||||
cat $log
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
trap "rm -f $log" EXIT
|
||||
|
||||
# Build and install oracle.
|
||||
go get golang.org/x/tools/cmd/oracle || die "'go get' failed"
|
||||
mv -f $GOPATH/bin/oracle $GOROOT/bin/
|
||||
$GOROOT/bin/oracle >$log 2>&1 || true # (prints usage and exits 1)
|
||||
grep -q "Run.*help" $log || die "$GOROOT/bin/oracle not installed"
|
||||
|
||||
|
||||
# Run Emacs, set the scope to the oracle tool itself,
|
||||
# load ./main.go, and describe the "fmt" import.
|
||||
emacs --batch --no-splash --no-window-system --no-init \
|
||||
--load $GOROOT/misc/emacs/go-mode.el \
|
||||
--load $thisdir/oracle.el \
|
||||
--eval '
|
||||
(progn
|
||||
(setq go-oracle-scope "golang.org/x/tools/cmd/oracle")
|
||||
(find-file "'$thisdir'/main.go")
|
||||
(search-forward "\"fmt\"")
|
||||
(backward-char)
|
||||
(go-oracle-describe)
|
||||
(princ (with-current-buffer "*go-oracle*"
|
||||
(buffer-substring-no-properties (point-min) (point-max))))
|
||||
(kill-emacs 0))
|
||||
' main.go >$log 2>&1 || die "emacs command failed"
|
||||
|
||||
# Check that Println is mentioned.
|
||||
grep -q "fmt/print.go.*func Println" $log || die "didn't find expected lines in log; got:"
|
||||
|
||||
echo "PASS"
|
||||
@@ -0,0 +1,193 @@
|
||||
// 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.
|
||||
|
||||
// oracle: a tool for answering questions about Go source code.
|
||||
// http://golang.org/s/oracle-design
|
||||
// http://golang.org/s/oracle-user-manual
|
||||
//
|
||||
// Run with -help flag or help subcommand for usage information.
|
||||
//
|
||||
package main // import "golang.org/x/tools/cmd/oracle"
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/build"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
|
||||
"golang.org/x/tools/go/loader"
|
||||
"golang.org/x/tools/oracle"
|
||||
)
|
||||
|
||||
var posFlag = flag.String("pos", "",
|
||||
"Filename and byte offset or extent of a syntax element about which to query, "+
|
||||
"e.g. foo.go:#123,#456, bar.go:#123.")
|
||||
|
||||
var ptalogFlag = flag.String("ptalog", "",
|
||||
"Location of the points-to analysis log file, or empty to disable logging.")
|
||||
|
||||
var formatFlag = flag.String("format", "plain", "Output format. One of {plain,json,xml}.")
|
||||
|
||||
// TODO(adonovan): flip this flag after PTA presolver is implemented.
|
||||
var reflectFlag = flag.Bool("reflect", false, "Analyze reflection soundly (slow).")
|
||||
|
||||
const useHelp = "Run 'oracle -help' for more information.\n"
|
||||
|
||||
const helpMessage = `Go source code oracle.
|
||||
Usage: oracle [<flag> ...] <mode> <args> ...
|
||||
|
||||
The -format flag controls the output format:
|
||||
plain an editor-friendly format in which every line of output
|
||||
is of the form "pos: text", where pos is "-" if unknown.
|
||||
json structured data in JSON syntax.
|
||||
xml structured data in XML syntax.
|
||||
|
||||
The -pos flag is required in all modes except 'callgraph'.
|
||||
|
||||
The mode argument determines the query to perform:
|
||||
|
||||
callees show possible targets of selected function call
|
||||
callers show possible callers of selected function
|
||||
callgraph show complete callgraph of program
|
||||
callstack show path from callgraph root to selected function
|
||||
describe describe selected syntax: definition, methods, etc
|
||||
freevars show free variables of selection
|
||||
implements show 'implements' relation for selected type
|
||||
peers show send/receive corresponding to selected channel op
|
||||
referrers show all refs to entity denoted by selected identifier
|
||||
what show basic information about the selected syntax node
|
||||
|
||||
The user manual is available here: http://golang.org/s/oracle-user-manual
|
||||
|
||||
Examples:
|
||||
|
||||
Describe the syntax at offset 530 in this file (an import spec):
|
||||
% oracle -pos=src/golang.org/x/tools/cmd/oracle/main.go:#530 describe \
|
||||
golang.org/x/tools/cmd/oracle
|
||||
|
||||
Print the callgraph of the trivial web-server in JSON format:
|
||||
% oracle -format=json $GOROOT/src/net/http/triv.go callgraph
|
||||
` + loader.FromArgsUsage
|
||||
|
||||
var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file")
|
||||
|
||||
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 printHelp() {
|
||||
fmt.Fprintln(os.Stderr, helpMessage)
|
||||
fmt.Fprintln(os.Stderr, "Flags:")
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Don't print full help unless -help was requested.
|
||||
// Just gently remind users that it's there.
|
||||
flag.Usage = func() { fmt.Fprint(os.Stderr, useHelp) }
|
||||
flag.CommandLine.Init(os.Args[0], flag.ContinueOnError) // hack
|
||||
if err := flag.CommandLine.Parse(os.Args[1:]); err != nil {
|
||||
// (err has already been printed)
|
||||
if err == flag.ErrHelp {
|
||||
printHelp()
|
||||
}
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
args := flag.Args()
|
||||
if len(args) == 0 || args[0] == "" {
|
||||
fmt.Fprint(os.Stderr, "oracle: a mode argument is required.\n"+useHelp)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
mode := args[0]
|
||||
args = args[1:]
|
||||
if mode == "help" {
|
||||
printHelp()
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
if len(args) == 0 && mode != "what" {
|
||||
fmt.Fprint(os.Stderr, "oracle: no package arguments.\n"+useHelp)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
// 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() {
|
||||
buf.Flush()
|
||||
f.Close()
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// Profiling support.
|
||||
if *cpuprofile != "" {
|
||||
f, err := os.Create(*cpuprofile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
pprof.StartCPUProfile(f)
|
||||
defer pprof.StopCPUProfile()
|
||||
}
|
||||
|
||||
// -format flag
|
||||
switch *formatFlag {
|
||||
case "json", "plain", "xml":
|
||||
// ok
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "oracle: illegal -format value: %q.\n"+useHelp, *formatFlag)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
// Ask the oracle.
|
||||
res, err := oracle.Query(args, mode, *posFlag, ptalog, &build.Default, *reflectFlag)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "oracle: %s.\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Print the result.
|
||||
switch *formatFlag {
|
||||
case "json":
|
||||
b, err := json.MarshalIndent(res.Serial(), "", "\t")
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "oracle: JSON error: %s.\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Stdout.Write(b)
|
||||
|
||||
case "xml":
|
||||
b, err := xml.MarshalIndent(res.Serial(), "", "\t")
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "oracle: XML error: %s.\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Stdout.Write(b)
|
||||
|
||||
case "plain":
|
||||
res.WriteTo(os.Stdout)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
;;;
|
||||
;;; Integration of the Go 'oracle' analysis tool into Emacs.
|
||||
;;;
|
||||
;;; To install the Go oracle, run:
|
||||
;;; % export GOROOT=... GOPATH=...
|
||||
;;; % go get golang.org/x/tools/cmd/oracle
|
||||
;;; % mv $GOPATH/bin/oracle $GOROOT/bin/
|
||||
;;;
|
||||
;;; Load this file into Emacs and set go-oracle-scope to your
|
||||
;;; configuration. Then, find a file of Go source code, enable
|
||||
;;; go-oracle-mode, select an expression of interest, and press `C-c C-o d'
|
||||
;;; (for "describe") or run one of the other go-oracle-xxx commands.
|
||||
;;;
|
||||
;;; TODO(adonovan): simplify installation and configuration by making
|
||||
;;; oracle a subcommand of 'go tool'.
|
||||
|
||||
(require 'compile)
|
||||
(require 'go-mode)
|
||||
(require 'cl)
|
||||
|
||||
(defgroup go-oracle nil
|
||||
"Options specific to the Go oracle."
|
||||
:group 'go)
|
||||
|
||||
(defcustom go-oracle-command (concat (car (go-root-and-paths)) "/bin/oracle")
|
||||
"The Go oracle command; the default is $GOROOT/bin/oracle."
|
||||
:type 'string
|
||||
:group 'go-oracle)
|
||||
|
||||
(defcustom go-oracle-scope ""
|
||||
"The scope of the analysis. See `go-oracle-set-scope'."
|
||||
:type 'string
|
||||
:group 'go-oracle)
|
||||
|
||||
(defvar go-oracle--scope-history
|
||||
nil
|
||||
"History of values supplied to `go-oracle-set-scope'.")
|
||||
|
||||
;; TODO(adonovan): I'd like to get rid of this separate mode since it
|
||||
;; makes it harder to use the oracle.
|
||||
(defvar go-oracle-mode-map
|
||||
(let ((m (make-sparse-keymap)))
|
||||
(define-key m (kbd "C-c C-o t") #'go-oracle-describe) ; t for type
|
||||
(define-key m (kbd "C-c C-o f") #'go-oracle-freevars)
|
||||
(define-key m (kbd "C-c C-o g") #'go-oracle-callgraph)
|
||||
(define-key m (kbd "C-c C-o i") #'go-oracle-implements)
|
||||
(define-key m (kbd "C-c C-o c") #'go-oracle-peers) ; c for channel
|
||||
(define-key m (kbd "C-c C-o r") #'go-oracle-referrers)
|
||||
(define-key m (kbd "C-c C-o d") #'go-oracle-definition)
|
||||
(define-key m (kbd "C-c C-o p") #'go-oracle-pointsto)
|
||||
(define-key m (kbd "C-c C-o s") #'go-oracle-callstack)
|
||||
(define-key m (kbd "C-c C-o <") #'go-oracle-callers)
|
||||
(define-key m (kbd "C-c C-o >") #'go-oracle-callees)
|
||||
m))
|
||||
|
||||
;; TODO(dominikh): Rethink set-scope some. Setting it to a file is
|
||||
;; painful because it doesn't use find-file, and variables/~ aren't
|
||||
;; expanded. Setting it to an import path is somewhat painful because
|
||||
;; it doesn't make use of go-mode's import path completion. One option
|
||||
;; would be having two different functions, but then we can't
|
||||
;; automatically call it when no scope has been set. Also it wouldn't
|
||||
;; easily allow specifying more than one file/package.
|
||||
(defun go-oracle-set-scope ()
|
||||
"Set the scope for the Go oracle, prompting the user to edit the
|
||||
previous scope.
|
||||
|
||||
The scope specifies a set of arguments, separated by spaces.
|
||||
It may be:
|
||||
1) a set of packages whose main() functions will be analyzed.
|
||||
2) a list of *.go filenames; they will treated like as a single
|
||||
package (see #3).
|
||||
3) a single package whose main() function and/or Test* functions
|
||||
will be analyzed.
|
||||
|
||||
In the common case, this is similar to the argument(s) you would
|
||||
specify to 'go build'."
|
||||
(interactive)
|
||||
(let ((scope (read-from-minibuffer "Go oracle scope: "
|
||||
go-oracle-scope
|
||||
nil
|
||||
nil
|
||||
'go-oracle--scope-history)))
|
||||
(if (string-equal "" scope)
|
||||
(error "You must specify a non-empty scope for the Go oracle"))
|
||||
(setq go-oracle-scope scope)))
|
||||
|
||||
(defun go-oracle--run (mode)
|
||||
"Run the Go oracle in the specified MODE, passing it the
|
||||
selected region of the current buffer. Process the output to
|
||||
replace each file name with a small hyperlink. Display the
|
||||
result."
|
||||
(if (not buffer-file-name)
|
||||
(error "Cannot use oracle on a buffer without a file name"))
|
||||
;; It's not sufficient to save a modified buffer since if
|
||||
;; gofmt-before-save is on the before-save-hook, saving will
|
||||
;; disturb the selected region.
|
||||
(if (buffer-modified-p)
|
||||
(error "Please save the buffer before invoking go-oracle"))
|
||||
(if (string-equal "" go-oracle-scope)
|
||||
(go-oracle-set-scope))
|
||||
(let* ((filename (file-truename buffer-file-name))
|
||||
(posflag (if (use-region-p)
|
||||
(format "-pos=%s:#%d,#%d"
|
||||
filename
|
||||
(1- (go--position-bytes (region-beginning)))
|
||||
(1- (go--position-bytes (region-end))))
|
||||
(format "-pos=%s:#%d"
|
||||
filename
|
||||
(1- (position-bytes (point))))))
|
||||
;; This would be simpler if we could just run 'go tool oracle'.
|
||||
(env-vars (go-root-and-paths))
|
||||
(goroot-env (concat "GOROOT=" (car env-vars)))
|
||||
(gopath-env (concat "GOPATH=" (mapconcat #'identity (cdr env-vars) ":"))))
|
||||
(with-current-buffer (get-buffer-create "*go-oracle*")
|
||||
(setq buffer-read-only nil)
|
||||
(erase-buffer)
|
||||
(insert "Go Oracle\n")
|
||||
(let ((args (append (list go-oracle-command nil t nil posflag mode)
|
||||
(split-string go-oracle-scope " " t))))
|
||||
;; Log the command to *Messages*, for debugging.
|
||||
(message "Command: %s:" args)
|
||||
(message nil) ; clears/shrinks minibuffer
|
||||
|
||||
(message "Running oracle...")
|
||||
;; Use dynamic binding to modify/restore the environment
|
||||
(let ((process-environment (list* goroot-env gopath-env process-environment)))
|
||||
(apply #'call-process args)))
|
||||
(insert "\n")
|
||||
(compilation-mode)
|
||||
(setq compilation-error-screen-columns nil)
|
||||
|
||||
;; Hide the file/line info to save space.
|
||||
;; Replace each with a little widget.
|
||||
;; compilation-mode + this loop = slooow.
|
||||
;; TODO(adonovan): have oracle give us JSON
|
||||
;; and we'll do the markup directly.
|
||||
(let ((buffer-read-only nil)
|
||||
(p 1))
|
||||
(while (not (null p))
|
||||
(let ((np (compilation-next-single-property-change p 'compilation-message)))
|
||||
;; TODO(adonovan): this can be verbose in the *Messages* buffer.
|
||||
;; (message "Post-processing link (%d%%)" (/ (* p 100) (point-max)))
|
||||
(if np
|
||||
(when (equal (line-number-at-pos p) (line-number-at-pos np))
|
||||
;; np is (typically) the space following ":"; consume it too.
|
||||
(put-text-property p np 'display "▶")
|
||||
(goto-char np)
|
||||
(insert " ")))
|
||||
(setq p np)))
|
||||
(message nil))
|
||||
|
||||
(let ((w (display-buffer (current-buffer))))
|
||||
(balance-windows)
|
||||
(shrink-window-if-larger-than-buffer w)
|
||||
(set-window-point w (point-min))))))
|
||||
|
||||
(defun go-oracle-callees ()
|
||||
"Show possible callees of the function call at the current point."
|
||||
(interactive)
|
||||
(go-oracle--run "callees"))
|
||||
|
||||
(defun go-oracle-callers ()
|
||||
"Show the set of callers of the function containing the current point."
|
||||
(interactive)
|
||||
(go-oracle--run "callers"))
|
||||
|
||||
(defun go-oracle-callgraph ()
|
||||
"Show the callgraph of the current program."
|
||||
(interactive)
|
||||
(go-oracle--run "callgraph"))
|
||||
|
||||
(defun go-oracle-callstack ()
|
||||
"Show an arbitrary path from a root of the call graph to the
|
||||
function containing the current point."
|
||||
(interactive)
|
||||
(go-oracle--run "callstack"))
|
||||
|
||||
(defun go-oracle-definition ()
|
||||
"Show the definition of the selected identifier."
|
||||
(interactive)
|
||||
(go-oracle--run "definition"))
|
||||
|
||||
(defun go-oracle-describe ()
|
||||
"Describe the selected syntax, its kind, type and methods."
|
||||
(interactive)
|
||||
(go-oracle--run "describe"))
|
||||
|
||||
(defun go-oracle-pointsto ()
|
||||
"Show what the selected expression points to."
|
||||
(interactive)
|
||||
(go-oracle--run "pointsto"))
|
||||
|
||||
(defun go-oracle-implements ()
|
||||
"Describe the 'implements' relation for types in the package
|
||||
containing the current point."
|
||||
(interactive)
|
||||
(go-oracle--run "implements"))
|
||||
|
||||
(defun go-oracle-freevars ()
|
||||
"Enumerate the free variables of the current selection."
|
||||
(interactive)
|
||||
(go-oracle--run "freevars"))
|
||||
|
||||
(defun go-oracle-peers ()
|
||||
"Enumerate the set of possible corresponding sends/receives for
|
||||
this channel receive/send operation."
|
||||
(interactive)
|
||||
(go-oracle--run "peers"))
|
||||
|
||||
(defun go-oracle-referrers ()
|
||||
"Enumerate all references to the object denoted by the selected
|
||||
identifier."
|
||||
(interactive)
|
||||
(go-oracle--run "referrers"))
|
||||
|
||||
(defun go-oracle-whicherrs ()
|
||||
"Show globals, constants and types to which the selected
|
||||
expression (of type 'error') may refer."
|
||||
(interactive)
|
||||
(go-oracle--run "whicherrs"))
|
||||
|
||||
;; TODO(dominikh): better docstring
|
||||
(define-minor-mode go-oracle-mode "Oracle minor mode for go-mode
|
||||
|
||||
Keys specific to go-oracle-mode:
|
||||
\\{go-oracle-mode-map}"
|
||||
nil " oracle" go-oracle-mode-map)
|
||||
|
||||
(provide 'go-oracle)
|
||||
@@ -0,0 +1,107 @@
|
||||
" -*- text -*-
|
||||
" oracle.vim -- Vim integration for the Go oracle.
|
||||
"
|
||||
" Load with (e.g.) :source oracle.vim
|
||||
" Call with (e.g.) :GoOracleDescribe
|
||||
" while cursor or selection is over syntax of interest.
|
||||
" Run :copen to show the quick-fix file.
|
||||
"
|
||||
" This is an absolutely rudimentary integration of the Go Oracle into
|
||||
" Vim's quickfix mechanism and it needs a number of usability
|
||||
" improvements before it can be practically useful to Vim users.
|
||||
" Voluntary contributions welcomed!
|
||||
"
|
||||
" TODO(adonovan):
|
||||
" - reject buffers with no filename.
|
||||
" - hide all filenames in quickfix buffer.
|
||||
|
||||
" Get the path to the Go oracle executable.
|
||||
func! s:go_oracle_bin()
|
||||
let [ext, sep] = (has('win32') || has('win64') ? ['.exe', ';'] : ['', ':'])
|
||||
let go_oracle = globpath(join(split($GOPATH, sep), ','), '/bin/oracle' . ext)
|
||||
if go_oracle == ''
|
||||
let go_oracle = globpath($GOROOT, '/bin/oracle' . ext)
|
||||
endif
|
||||
return go_oracle
|
||||
endfunction
|
||||
|
||||
let s:go_oracle = s:go_oracle_bin()
|
||||
|
||||
func! s:qflist(output)
|
||||
let qflist = []
|
||||
" Parse GNU-style 'file:line.col-line.col: message' format.
|
||||
let mx = '^\(\a:[\\/][^:]\+\|[^:]\+\):\(\d\+\):\(\d\+\):\(.*\)$'
|
||||
for line in split(a:output, "\n")
|
||||
let ml = matchlist(line, mx)
|
||||
" Ignore non-match lines or warnings
|
||||
if ml == [] || ml[4] =~ '^ warning:'
|
||||
continue
|
||||
endif
|
||||
let item = {
|
||||
\ 'filename': ml[1],
|
||||
\ 'text': ml[4],
|
||||
\ 'lnum': ml[2],
|
||||
\ 'col': ml[3],
|
||||
\}
|
||||
let bnr = bufnr(fnameescape(ml[1]))
|
||||
if bnr != -1
|
||||
let item['bufnr'] = bnr
|
||||
endif
|
||||
call add(qflist, item)
|
||||
endfor
|
||||
call setqflist(qflist)
|
||||
cwindow
|
||||
endfun
|
||||
|
||||
func! s:getpos(l, c)
|
||||
if &encoding != 'utf-8'
|
||||
let buf = a:l == 1 ? '' : (join(getline(1, a:l-1), "\n") . "\n")
|
||||
let buf .= a:c == 1 ? '' : getline('.')[:a:c-2]
|
||||
return len(iconv(buf, &encoding, 'utf-8'))
|
||||
endif
|
||||
return line2byte(a:l) + (a:c-2)
|
||||
endfun
|
||||
|
||||
func! s:RunOracle(mode, selected) range abort
|
||||
let fname = expand('%:p')
|
||||
let sname = get(g:, 'go_oracle_scope_file', fname)
|
||||
if a:selected != -1
|
||||
let pos1 = s:getpos(line("'<"), col("'<"))
|
||||
let pos2 = s:getpos(line("'>"), col("'>"))
|
||||
let cmd = printf('%s -pos=%s:#%d,#%d %s %s',
|
||||
\ s:go_oracle,
|
||||
\ shellescape(fname), pos1, pos2, a:mode, shellescape(sname))
|
||||
else
|
||||
let pos = s:getpos(line('.'), col('.'))
|
||||
let cmd = printf('%s -pos=%s:#%d %s %s',
|
||||
\ s:go_oracle,
|
||||
\ shellescape(fname), pos, a:mode, shellescape(sname))
|
||||
endif
|
||||
call s:qflist(system(cmd))
|
||||
endfun
|
||||
|
||||
" Describe the expression at the current point.
|
||||
command! -range=% GoOracleDescribe
|
||||
\ call s:RunOracle('describe', <count>)
|
||||
|
||||
" Show possible callees of the function call at the current point.
|
||||
command! -range=% GoOracleCallees
|
||||
\ call s:RunOracle('callees', <count>)
|
||||
|
||||
" Show the set of callers of the function containing the current point.
|
||||
command! -range=% GoOracleCallers
|
||||
\ call s:RunOracle('callers', <count>)
|
||||
|
||||
" Show the callgraph of the current program.
|
||||
command! -range=% GoOracleCallgraph
|
||||
\ call s:RunOracle('callgraph', <count>)
|
||||
|
||||
" Describe the 'implements' relation for types in the
|
||||
" package containing the current point.
|
||||
command! -range=% GoOracleImplements
|
||||
\ call s:RunOracle('implements', <count>)
|
||||
|
||||
" Enumerate the set of possible corresponding sends/receives for
|
||||
" this channel receive/send operation.
|
||||
command! -range=% GoOracleChannelPeers
|
||||
\ call s:RunOracle('peers', <count>)
|
||||
Loaded 100 of 660 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user