1
0
mirror of https://github.com/golang/go synced 2024-11-18 10:14:45 -07:00

cmd/guru: add support for loading modified files

The -modified flag causes guru to read a simple archive file from stdin.
This archive specifies alternative contents for one or more file names.
The build.Context checks this table before delegating to the usual
behavior.

This will not work for files that import "C" since cgo accesses the
file system directly.

Added end-to-end test via Emacs.

Simplify findQueryPos (now: fileOffsetToPos)

Credit: Daniel Morsing, for the prototype of this feature.

Change-Id: I5ae818ed5e8bb81001781893dded2d085e9cf8d6
Reviewed-on: https://go-review.googlesource.com/19498
Reviewed-by: Daniel Morsing <daniel.morsing@gmail.com>
This commit is contained in:
Alan Donovan 2016-02-14 22:14:31 -05:00 committed by Alan Donovan
parent 5a22c00969
commit e08a7ae6bc
6 changed files with 183 additions and 44 deletions

View File

@ -35,6 +35,7 @@ emacs --batch --no-splash --no-window-system --no-init \
(progn (progn
(princ (emacs-version)) ; requires Emacs v23 (princ (emacs-version)) ; requires Emacs v23
(find-file "'$thisdir'/main.go") (find-file "'$thisdir'/main.go")
(insert "// modify but do not save the editor buffer\n")
(search-forward "\"fmt\"") (search-forward "\"fmt\"")
(backward-char) (backward-char)
(go-guru-describe) (go-guru-describe)

View File

@ -86,11 +86,6 @@ a scope if not already set. Process the output to replace each
file name with a small hyperlink. Display the result." file name with a small hyperlink. Display the result."
(if (not buffer-file-name) (if (not buffer-file-name)
(error "Cannot use guru on a buffer without a file name")) (error "Cannot use guru 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-guru"))
(and need-scope (and need-scope
(string-equal "" go-guru-scope) (string-equal "" go-guru-scope)
(go-guru-set-scope)) (go-guru-set-scope))
@ -105,21 +100,34 @@ file name with a small hyperlink. Display the result."
(1- (position-bytes (point)))))) (1- (position-bytes (point))))))
(env-vars (go-root-and-paths)) (env-vars (go-root-and-paths))
(goroot-env (concat "GOROOT=" (car env-vars))) (goroot-env (concat "GOROOT=" (car env-vars)))
(gopath-env (concat "GOPATH=" (mapconcat #'identity (cdr env-vars) ":")))) (gopath-env (concat "GOPATH=" (mapconcat #'identity (cdr env-vars) ":")))
(with-current-buffer (get-buffer-create "*go-guru*") (output-buffer (get-buffer-create "*go-guru*")))
(with-current-buffer output-buffer
(setq buffer-read-only nil) (setq buffer-read-only nil)
(erase-buffer) (erase-buffer)
(insert "Go Guru\n") (insert "Go Guru\n"))
(let ((args (list go-guru-command nil t nil (with-current-buffer (get-buffer-create "*go-guru-input*")
"-scope" go-guru-scope mode posn))) (setq buffer-read-only nil)
;; Log the command to *Messages*, for debugging. (erase-buffer)
(message "Command: %s:" args) (go-guru--insert-modified-files)
(message nil) ; clears/shrinks minibuffer (let* ((args (list "-modified"
"-scope" go-guru-scope
(message "Running guru...") mode
;; Use dynamic binding to modify/restore the environment posn)))
(let ((process-environment (list* goroot-env gopath-env process-environment))) ;; Log the command to *Messages*, for debugging.
(apply #'call-process args))) (message "Command: %s:" args)
(message nil) ; clears/shrinks minibuffer
(message "Running guru...")
;; Use dynamic binding to modify/restore the environment
(let ((process-environment (list* goroot-env gopath-env process-environment)))
(apply #'call-process-region (append (list (point-min)
(point-max)
go-guru-command
nil ; delete
output-buffer
t)
args)))))
(with-current-buffer output-buffer
(insert "\n") (insert "\n")
(compilation-mode) (compilation-mode)
(setq compilation-error-screen-columns nil) (setq compilation-error-screen-columns nil)
@ -158,6 +166,20 @@ file name with a small hyperlink. Display the result."
(shrink-window-if-larger-than-buffer w) (shrink-window-if-larger-than-buffer w)
(set-window-point w (point-min)))))) (set-window-point w (point-min))))))
(defun go-guru--insert-modified-files ()
"Insert the contents of each modified Go buffer into the
current buffer in the format specified by guru's -modified flag."
(mapc #'(lambda (b)
(and (buffer-modified-p b)
(buffer-file-name b)
(string= (file-name-extension (buffer-file-name b)) "go")
(progn
(insert (format "%s\n%d\n"
(buffer-file-name b)
(buffer-size b)))
(insert-buffer-substring b))))
(buffer-list)))
(defun go-guru-callees () (defun go-guru-callees ()
"Show possible callees of the function call at the current point." "Show possible callees of the function call at the current point."
(interactive) (interactive)

View File

@ -252,7 +252,21 @@ func parseQueryPos(lprog *loader.Program, pos string, needExact bool) (*queryPos
if err != nil { if err != nil {
return nil, err return nil, err
} }
start, end, err := findQueryPos(lprog.Fset, filename, startOffset, endOffset)
// Find the named file among those in the loaded program.
var file *token.File
lprog.Fset.Iterate(func(f *token.File) bool {
if sameFile(filename, f.Name()) {
file = f
return false // done
}
return true // continue
})
if file == nil {
return nil, fmt.Errorf("file %s not found in loaded program", filename)
}
start, end, err := fileOffsetToPos(file, startOffset, endOffset)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -13,15 +13,19 @@ package main // import "golang.org/x/tools/cmd/guru"
import ( import (
"bufio" "bufio"
"bytes"
"encoding/json" "encoding/json"
"encoding/xml" "encoding/xml"
"flag" "flag"
"fmt" "fmt"
"go/build" "go/build"
"io" "io"
"io/ioutil"
"log" "log"
"os" "os"
"path/filepath"
"runtime/pprof" "runtime/pprof"
"strconv"
"strings" "strings"
"golang.org/x/tools/go/buildutil" "golang.org/x/tools/go/buildutil"
@ -29,6 +33,7 @@ import (
// flags // flags
var ( var (
modifiedFlag = flag.Bool("modified", false, "read archive of modified files from standard input")
scopeFlag = flag.String("scope", "", "comma-separated list of `packages` the analysis should be limited to (default=all)") scopeFlag = flag.String("scope", "", "comma-separated list of `packages` the analysis should be limited to (default=all)")
ptalogFlag = flag.String("ptalog", "", "write points-to analysis log to `file`") ptalogFlag = flag.String("ptalog", "", "write points-to analysis log to `file`")
formatFlag = flag.String("format", "plain", "output `format`; one of {plain,json,xml}") formatFlag = flag.String("format", "plain", "output `format`; one of {plain,json,xml}")
@ -72,6 +77,14 @@ The -format flag controls the output format:
json structured data in JSON syntax. json structured data in JSON syntax.
xml structured data in XML syntax. xml structured data in XML syntax.
The -modified flag causes guru to read an archive from standard input.
Files in this archive will be used in preference to those in
the file system. In this way, a text editor may supply guru
with the contents of its unsaved buffers. Each archive entry
consists of the file name, a newline, the decimal file size,
another newline, and the contents of the file.
User manual: http://golang.org/s/oracle-user-manual User manual: http://golang.org/s/oracle-user-manual
Example: describe syntax at offset 530 in this file (an import spec): Example: describe syntax at offset 530 in this file (an import spec):
@ -150,11 +163,31 @@ func main() {
log.Fatalf("illegal -format value: %q.\n"+useHelp, *formatFlag) log.Fatalf("illegal -format value: %q.\n"+useHelp, *formatFlag)
} }
ctxt := &build.Default
// If there were modified files,
// read them from the standard input and
// overlay them on the build context.
if *modifiedFlag {
modified, err := parseArchive(os.Stdin)
if err != nil {
log.Fatal(err)
}
// All I/O done by guru needs to consult the modified map.
// The ReadFile done by referrers does,
// but the loader's cgo preprocessing currently does not.
if len(modified) > 0 {
ctxt = useModifiedFiles(ctxt, modified)
}
}
// Ask the guru. // Ask the guru.
query := Query{ query := Query{
Mode: mode, Mode: mode,
Pos: posn, Pos: posn,
Build: &build.Default, Build: ctxt,
Scope: strings.Split(*scopeFlag, ","), Scope: strings.Split(*scopeFlag, ","),
PTALog: ptalog, PTALog: ptalog,
Reflection: *reflectFlag, Reflection: *reflectFlag,
@ -184,3 +217,68 @@ func main() {
query.WriteTo(os.Stdout) query.WriteTo(os.Stdout)
} }
} }
func parseArchive(archive io.Reader) (map[string][]byte, error) {
modified := make(map[string][]byte)
r := bufio.NewReader(archive)
for {
// Read file name.
filename, err := r.ReadString('\n')
if err != nil {
if err == io.EOF {
break // OK
}
return nil, fmt.Errorf("reading modified file name: %v", err)
}
filename = filepath.Clean(strings.TrimSpace(filename))
// Read file size.
sz, err := r.ReadString('\n')
if err != nil {
return nil, fmt.Errorf("reading size of modified file %s: %v", filename, err)
}
sz = strings.TrimSpace(sz)
size, err := strconv.ParseInt(sz, 10, 32)
if err != nil {
return nil, fmt.Errorf("parsing size of modified file %s: %v", filename, err)
}
// Read file content.
var content bytes.Buffer
content.Grow(int(size))
if _, err := io.CopyN(&content, r, size); err != nil {
return nil, fmt.Errorf("reading modified file %s: %v", filename, err)
}
modified[filename] = content.Bytes()
}
return modified, nil
}
// useModifiedFiles augments the provided build.Context by the
// mapping from file names to alternative contents.
func useModifiedFiles(orig *build.Context, modified map[string][]byte) *build.Context {
rc := func(data []byte) (io.ReadCloser, error) {
return ioutil.NopCloser(bytes.NewBuffer(data)), nil
}
copy := *orig // make a copy
ctxt := &copy
ctxt.OpenFile = func(path string) (io.ReadCloser, error) {
// Fast path: names match exactly.
if content, ok := modified[path]; ok {
return rc(content)
}
// Slow path: check for same file under a different
// alias, perhaps due to a symbolic link.
for filename, content := range modified {
if sameFile(path, filename) {
return rc(content)
}
}
return buildutil.OpenFile(orig, path)
}
return ctxt
}

View File

@ -66,26 +66,11 @@ func parsePos(pos string) (filename string, startOffset, endOffset int, err erro
return return
} }
// findQueryPos searches fset for filename and translates the // fileOffsetToPos translates the specified file-relative byte offsets
// specified file-relative byte offsets into token.Pos form. It // into token.Pos form. It returns an error if the file was not found
// returns an error if the file was not found or the offsets were out // or the offsets were out of bounds.
// of bounds.
// //
func findQueryPos(fset *token.FileSet, filename string, startOffset, endOffset int) (start, end token.Pos, err error) { func fileOffsetToPos(file *token.File, startOffset, endOffset int) (start, end token.Pos, err error) {
var file *token.File
fset.Iterate(func(f *token.File) bool {
if sameFile(filename, f.Name()) {
// (f.Name() is absolute)
file = f
return false // done
}
return true // continue
})
if file == nil {
err = fmt.Errorf("couldn't find file containing position")
return
}
// Range check [start..end], inclusive of both end-points. // Range check [start..end], inclusive of both end-points.
if 0 <= startOffset && startOffset <= file.Size() { if 0 <= startOffset && startOffset <= file.Size() {
@ -119,8 +104,8 @@ func sameFile(x, y string) bool {
return false return false
} }
// fastQueryPos parses the position string and returns a QueryPos. // fastQueryPos parses the position string and returns a queryPos.
// It parses only a single file, and does not run the type checker. // It parses only a single file and does not run the type checker.
func fastQueryPos(pos string) (*queryPos, error) { func fastQueryPos(pos string) (*queryPos, error) {
filename, startOffset, endOffset, err := parsePos(pos) filename, startOffset, endOffset, err := parsePos(pos)
if err != nil { if err != nil {
@ -133,7 +118,7 @@ func fastQueryPos(pos string) (*queryPos, error) {
return nil, err return nil, err
} }
start, end, err := findQueryPos(fset, filename, startOffset, endOffset) start, end, err := fileOffsetToPos(fset.File(f.Pos()), startOffset, endOffset)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -8,12 +8,14 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"go/ast" "go/ast"
"go/build"
"go/token" "go/token"
"go/types" "go/types"
"io/ioutil" "io"
"sort" "sort"
"golang.org/x/tools/cmd/guru/serial" "golang.org/x/tools/cmd/guru/serial"
"golang.org/x/tools/go/buildutil"
"golang.org/x/tools/go/loader" "golang.org/x/tools/go/loader"
"golang.org/x/tools/refactor/importgraph" "golang.org/x/tools/refactor/importgraph"
) )
@ -108,6 +110,7 @@ func referrers(q *Query) error {
sort.Sort(byNamePos{q.Fset, refs}) sort.Sort(byNamePos{q.Fset, refs})
q.result = &referrersResult{ q.result = &referrersResult{
build: q.Build,
qpos: qpos, qpos: qpos,
query: id, query: id,
obj: obj, obj: obj,
@ -156,6 +159,7 @@ func (p byNamePos) Less(i, j int) bool {
} }
type referrersResult struct { type referrersResult struct {
build *build.Context
qpos *queryPos qpos *queryPos
query *ast.Ident // identifier of query query *ast.Ident // identifier of query
obj types.Object // object it denotes obj types.Object // object it denotes
@ -188,7 +192,7 @@ func (r *referrersResult) display(printf printfFunc) {
// start asynchronous read. // start asynchronous read.
go func() { go func() {
sema <- struct{}{} // acquire token sema <- struct{}{} // acquire token
content, err := ioutil.ReadFile(posn.Filename) content, err := readFile(r.build, posn.Filename)
<-sema // release token <-sema // release token
if err != nil { if err != nil {
fi.data <- err fi.data <- err
@ -224,6 +228,21 @@ func (r *referrersResult) display(printf printfFunc) {
} }
} }
// readFile is like ioutil.ReadFile, but
// it goes through the virtualized build.Context.
func readFile(ctxt *build.Context, filename string) ([]byte, error) {
rc, err := buildutil.OpenFile(ctxt, filename)
if err != nil {
return nil, err
}
defer rc.Close()
var buf bytes.Buffer
if _, err := io.Copy(&buf, rc); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// TODO(adonovan): encode extent, not just Pos info, in Serial form. // TODO(adonovan): encode extent, not just Pos info, in Serial form.
func (r *referrersResult) toSerial(res *serial.Result, fset *token.FileSet) { func (r *referrersResult) toSerial(res *serial.Result, fset *token.FileSet) {