internal/lsp/cmd: add the definition mode

Change-Id: Ib171016fb1bb063a6424677458b554a08144465c
Reviewed-on: https://go-review.googlesource.com/c/159438
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
This commit is contained in:
Ian Cottrell 2018-12-14 15:46:12 -05:00
Родитель 8dbcc66f33
Коммит 067a2f313b
9 изменённых файлов: 705 добавлений и 15 удалений

Просмотреть файл

@ -11,39 +11,45 @@ import (
"context"
"flag"
"fmt"
"go/token"
"golang.org/x/tools/go/packages"
"golang.org/x/tools/internal/tool"
)
// Application is the main application as passed to tool.Main
// It handles the main command line parsing and dispatch to the sub commands.
type Application struct {
// Core application flags
// Embed the basic profiling flags supported by the tool package
tool.Profile
// we also include the server directly for now, so the flags work even without
// the verb. We should remove this when we stop allowing the server verb by
// default
// We include the server directly for now, so the flags work even without the verb.
// TODO: Remove this when we stop allowing the server verb by default.
Server Server
// An initial, common go/packages configuration
Config packages.Config
}
// Name implements tool.Application returning the binary name.
func (app *Application) Name() string { return "gopls" }
// Usage implements tool.Application returning empty extra argument usage.
func (app *Application) Usage() string { return "<mode> [mode-flags] [mode-args]" }
func (app *Application) Usage() string { return "<command> [command-flags] [command-args]" }
// ShortHelp implements tool.Application returning the main binary help.
func (app *Application) ShortHelp() string {
return "The Go Language Smartness Provider."
return "The Go Language source tools."
}
// DetailedHelp implements tool.Application returning the main binary help.
// This includes the short help for all the sub commands.
func (app *Application) DetailedHelp(f *flag.FlagSet) {
fmt.Fprint(f.Output(), `
Available modes are:
Available commands are:
`)
for _, c := range app.modes() {
for _, c := range app.commands() {
fmt.Fprintf(f.Output(), " %s : %v\n", c.Name(), c.ShortHelp())
}
fmt.Fprint(f.Output(), `
@ -61,21 +67,27 @@ func (app *Application) Run(ctx context.Context, args ...string) error {
tool.Main(ctx, &app.Server, args)
return nil
}
mode, args := args[0], args[1:]
for _, m := range app.modes() {
if m.Name() == mode {
tool.Main(ctx, m, args)
app.Config.Mode = packages.LoadSyntax
app.Config.Tests = true
if app.Config.Fset == nil {
app.Config.Fset = token.NewFileSet()
}
command, args := args[0], args[1:]
for _, c := range app.commands() {
if c.Name() == command {
tool.Main(ctx, c, args)
return nil
}
}
return tool.CommandLineErrorf("Unknown mode %v", mode)
return tool.CommandLineErrorf("Unknown command %v", command)
}
// modes returns the set of command modes supported by the gopls tool on the
// commands returns the set of commands supported by the gopls tool on the
// command line.
// The mode is specified by the first non flag argument.
func (app *Application) modes() []tool.Application {
// The command is specified by the first non flag argument.
func (app *Application) commands() []tool.Application {
return []tool.Application{
&app.Server,
&query{app: app},
}
}

Просмотреть файл

@ -0,0 +1,179 @@
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cmd
import (
"bytes"
"context"
"encoding/json"
"flag"
"fmt"
"go/types"
"os"
guru "golang.org/x/tools/cmd/guru/serial"
"golang.org/x/tools/internal/lsp/cache"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/tool"
)
// A Definition is the result of a 'definition' query.
type Definition struct {
Location Location `json:"location"` // location of the definition
Description string `json:"description"` // description of the denoted object
}
// This constant is printed in the help, and then used in a test to verify the
// help is still valid.
// It should be the byte offset in this file of the "Set" in "flag.FlagSet" from
// the DetailedHelp method below.
const exampleOffset = 1277
// definition implements the definition noun for the query command.
type definition struct {
query *query
}
func (d *definition) Name() string { return "definition" }
func (d *definition) Usage() string { return "<position>" }
func (d *definition) ShortHelp() string { return "show declaration of selected identifier" }
func (d *definition) DetailedHelp(f *flag.FlagSet) {
fmt.Fprintf(f.Output(), `
Example: show the definition of the identifier at syntax at offset %[1]v in this file (flag.FlagSet):
$ gopls definition internal/lsp/cmd/definition.go:#%[1]v
gopls definition flags are:
`, exampleOffset)
f.PrintDefaults()
}
// Run performs the definition query as specified by args and prints the
// results to stdout.
func (d *definition) Run(ctx context.Context, args ...string) error {
if len(args) != 1 {
return tool.CommandLineErrorf("definition expects 1 argument")
}
view := cache.NewView(&d.query.app.Config)
from, err := parseLocation(args[0])
if err != nil {
return err
}
f, err := view.GetFile(ctx, source.ToURI(from.Filename))
if err != nil {
return err
}
tok, err := f.GetToken()
if err != nil {
return err
}
pos := tok.Pos(from.Start.Offset)
ident, err := source.Identifier(ctx, view, f, pos)
if err != nil {
return err
}
if ident == nil {
return fmt.Errorf("not an identifier")
}
var result interface{}
switch d.query.Emulate {
case "":
result, err = buildDefinition(view, ident)
case emulateGuru:
result, err = buildGuruDefinition(view, ident)
default:
return fmt.Errorf("unknown emulation for definition: %s", d.query.Emulate)
}
if err != nil {
return err
}
if d.query.JSON {
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", "\t")
return enc.Encode(result)
}
switch d := result.(type) {
case *Definition:
fmt.Printf("%v: defined here as %s", d.Location, d.Description)
case *guru.Definition:
fmt.Printf("%s: defined here as %s", d.ObjPos, d.Desc)
default:
return fmt.Errorf("no printer for type %T", result)
}
return nil
}
func buildDefinition(view source.View, ident *source.IdentifierInfo) (*Definition, error) {
content, err := ident.Hover(nil)
if err != nil {
return nil, err
}
return &Definition{
Location: newLocation(view.FileSet(), ident.Declaration.Range),
Description: content,
}, nil
}
func buildGuruDefinition(view source.View, ident *source.IdentifierInfo) (*guru.Definition, error) {
loc := newLocation(view.FileSet(), ident.Declaration.Range)
pkg, err := ident.File.GetPackage()
if err != nil {
return nil, err
}
// guru does not support ranges
loc.End = loc.Start
// Behavior that attempts to match the expected output for guru. For an example
// of the format, see the associated definition tests.
buf := &bytes.Buffer{}
q := types.RelativeTo(pkg.Types)
qualifyName := ident.Declaration.Object.Pkg() != pkg.Types
name := ident.Name
var suffix interface{}
switch obj := ident.Declaration.Object.(type) {
case *types.TypeName:
fmt.Fprint(buf, "type")
case *types.Var:
if obj.IsField() {
qualifyName = false
fmt.Fprint(buf, "field")
suffix = obj.Type()
} else {
fmt.Fprint(buf, "var")
}
case *types.Func:
fmt.Fprint(buf, "func")
typ := obj.Type()
if obj.Type() != nil {
if sig, ok := typ.(*types.Signature); ok {
buf := &bytes.Buffer{}
if recv := sig.Recv(); recv != nil {
if named, ok := recv.Type().(*types.Named); ok {
fmt.Fprintf(buf, "(%s).%s", named.Obj().Name(), name)
}
}
if buf.Len() == 0 {
buf.WriteString(name)
}
types.WriteSignature(buf, sig, q)
name = buf.String()
}
}
default:
fmt.Fprintf(buf, "unknown [%T]", obj)
}
fmt.Fprint(buf, " ")
if qualifyName {
fmt.Fprintf(buf, "%s.", ident.Declaration.Object.Pkg().Path())
}
fmt.Fprint(buf, name)
if suffix != nil {
fmt.Fprint(buf, " ")
fmt.Fprint(buf, suffix)
}
return &guru.Definition{
ObjPos: fmt.Sprint(loc),
Desc: buf.String(),
}, nil
}

Просмотреть файл

@ -0,0 +1,163 @@
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cmd_test
import (
"context"
"flag"
"fmt"
"go/token"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"testing"
"golang.org/x/tools/go/packages/packagestest"
"golang.org/x/tools/internal/lsp/cmd"
"golang.org/x/tools/internal/tool"
)
var verifyGuru = flag.Bool("verify-guru", false, "Check that the guru compatability matches")
func TestDefinitionHelpExample(t *testing.T) {
dir, err := os.Getwd()
if err != nil {
t.Errorf("could not get wd: %v", err)
return
}
thisFile := filepath.Join(dir, "definition.go")
args := []string{"query", "definition", fmt.Sprintf("%v:#%v", thisFile, cmd.ExampleOffset)}
expect := regexp.MustCompile(`^[\w/\\:_]+flag[/\\]flag.go:\d+:\d+,\d+:\d+: defined here as type flag.FlagSet struct{.*}$`)
got := captureStdOut(t, func() {
tool.Main(context.Background(), &cmd.Application{}, args)
})
if !expect.MatchString(got) {
t.Errorf("test with %v\nexpected:\n%s\ngot:\n%s", args, expect, got)
}
}
func TestDefinition(t *testing.T) {
exported := packagestest.Export(t, packagestest.GOPATH, []packagestest.Module{{
Name: "golang.org/fake",
Files: packagestest.MustCopyFileTree("testdata"),
}})
defer exported.Cleanup()
count := 0
if err := exported.Expect(map[string]interface{}{
"definition": func(fset *token.FileSet, src token.Pos, flags string, def packagestest.Range, match string) {
count++
args := []string{"query"}
if flags != "" {
args = append(args, strings.Split(flags, " ")...)
}
args = append(args, "definition")
f := fset.File(src)
loc := cmd.Location{
Filename: f.Name(),
Start: cmd.Position{
Offset: f.Offset(src),
},
}
loc.End = loc.Start
args = append(args, fmt.Sprint(loc))
app := &cmd.Application{}
app.Config = *exported.Config
got := captureStdOut(t, func() {
tool.Main(context.Background(), app, args)
})
start := fset.Position(def.Start)
end := fset.Position(def.End)
expect := os.Expand(match, func(name string) string {
switch name {
case "file":
return start.Filename
case "efile":
qfile := strconv.Quote(start.Filename)
return qfile[1 : len(qfile)-1]
case "line":
return fmt.Sprint(start.Line)
case "col":
return fmt.Sprint(start.Column)
case "offset":
return fmt.Sprint(start.Offset)
case "eline":
return fmt.Sprint(end.Line)
case "ecol":
return fmt.Sprint(end.Column)
case "eoffset":
return fmt.Sprint(end.Offset)
default:
return name
}
})
if *verifyGuru {
var guruArgs []string
runGuru := false
for _, arg := range args {
switch {
case arg == "query":
// just ignore this one
case arg == "-json":
guruArgs = append(guruArgs, arg)
case arg == "-emulate=guru":
// if we don't see this one we should not run guru
runGuru = true
case strings.HasPrefix(arg, "-"):
// unknown flag, ignore it
break
default:
guruArgs = append(guruArgs, arg)
}
}
if runGuru {
cmd := exec.Command("guru", guruArgs...)
cmd.Env = exported.Config.Env
out, err := cmd.CombinedOutput()
if err != nil {
t.Errorf("Could not run guru %v: %v\n%s", guruArgs, err, out)
} else {
guru := strings.TrimSpace(string(out))
if !strings.HasPrefix(expect, guru) {
t.Errorf("definition %v\nexpected:\n%s\nguru gave:\n%s", args, expect, guru)
}
}
}
}
if expect != got {
t.Errorf("definition %v\nexpected:\n%s\ngot:\n%s", args, expect, got)
}
},
}); err != nil {
t.Fatal(err)
}
if count == 0 {
t.Fatalf("No tests were run")
}
}
func captureStdOut(t testing.TB, f func()) string {
r, out, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
old := os.Stdout
defer func() {
os.Stdout = old
out.Close()
r.Close()
}()
os.Stdout = out
f()
out.Close()
data, err := ioutil.ReadAll(r)
if err != nil {
t.Fatal(err)
}
return strings.TrimSpace(string(data))
}

Просмотреть файл

@ -0,0 +1,7 @@
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cmd
const ExampleOffset = exampleOffset

Просмотреть файл

@ -0,0 +1,161 @@
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cmd
import (
"fmt"
"go/token"
"regexp"
"strconv"
"golang.org/x/tools/internal/lsp/source"
)
type Location struct {
Filename string `json:"file"`
Start Position `json:"start"`
End Position `json:"end"`
}
type Position struct {
Line int `json:"line"`
Column int `json:"column"`
Offset int `json:"offset"`
}
func newLocation(fset *token.FileSet, r source.Range) Location {
start := fset.Position(r.Start)
end := fset.Position(r.End)
// it should not be possible the following line to fail
filename, _ := source.ToURI(start.Filename).Filename()
return Location{
Filename: filename,
Start: Position{
Line: start.Line,
Column: start.Column,
Offset: fset.File(r.Start).Offset(r.Start),
},
End: Position{
Line: end.Line,
Column: end.Column,
Offset: fset.File(r.End).Offset(r.End),
},
}
}
var posRe = regexp.MustCompile(
`(?P<file>.*):(?P<start>(?P<sline>\d+):(?P<scol>\d)+|#(?P<soff>\d+))(?P<end>:(?P<eline>\d+):(?P<ecol>\d+)|#(?P<eoff>\d+))?$`)
const (
posReAll = iota
posReFile
posReStart
posReSLine
posReSCol
posReSOff
posReEnd
posReELine
posReECol
posReEOff
)
func init() {
names := posRe.SubexpNames()
// verify all our submatch offsets are correct
for name, index := range map[string]int{
"file": posReFile,
"start": posReStart,
"sline": posReSLine,
"scol": posReSCol,
"soff": posReSOff,
"end": posReEnd,
"eline": posReELine,
"ecol": posReECol,
"eoff": posReEOff,
} {
if names[index] == name {
continue
}
// try to find it
for test := range names {
if names[test] == name {
panic(fmt.Errorf("Index for %s incorrect, wanted %v have %v", name, index, test))
}
}
panic(fmt.Errorf("Subexp %s does not exist", name))
}
}
// parseLocation parses a string of the form "file:pos" or
// file:start,end" where pos, start, end match either a byte offset in the
// form #%d or a line and column in the form %d,%d.
func parseLocation(value string) (Location, error) {
var loc Location
m := posRe.FindStringSubmatch(value)
if m == nil {
return loc, fmt.Errorf("bad location syntax %q", value)
}
loc.Filename = m[posReFile]
if m[posReSLine] != "" {
if v, err := strconv.ParseInt(m[posReSLine], 10, 32); err != nil {
return loc, err
} else {
loc.Start.Line = int(v)
}
if v, err := strconv.ParseInt(m[posReSCol], 10, 32); err != nil {
return loc, err
} else {
loc.Start.Column = int(v)
}
} else {
if v, err := strconv.ParseInt(m[posReSOff], 10, 32); err != nil {
return loc, err
} else {
loc.Start.Offset = int(v)
}
}
if m[posReEnd] == "" {
loc.End = loc.Start
} else {
if m[posReELine] != "" {
if v, err := strconv.ParseInt(m[posReELine], 10, 32); err != nil {
return loc, err
} else {
loc.End.Line = int(v)
}
if v, err := strconv.ParseInt(m[posReECol], 10, 32); err != nil {
return loc, err
} else {
loc.End.Column = int(v)
}
} else {
if v, err := strconv.ParseInt(m[posReEOff], 10, 32); err != nil {
return loc, err
} else {
loc.End.Offset = int(v)
}
}
}
return loc, nil
}
func (l Location) Format(f fmt.State, c rune) {
// we should always have a filename
fmt.Fprint(f, l.Filename)
// are we in line:column format or #offset format
fmt.Fprintf(f, ":%v", l.Start)
if l.End != l.Start {
fmt.Fprintf(f, ",%v", l.End)
}
}
func (p Position) Format(f fmt.State, c rune) {
// are we in line:column format or #offset format
if p.Line > 0 {
fmt.Fprintf(f, "%d:%d", p.Line, p.Column)
return
}
fmt.Fprintf(f, "#%d", p.Offset)
}

71
internal/lsp/cmd/query.go Normal file
Просмотреть файл

@ -0,0 +1,71 @@
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cmd
import (
"context"
"flag"
"fmt"
"golang.org/x/tools/internal/tool"
)
const (
// The set of possible options that can be passed through the -emulate flag,
// which causes query to adjust its output to match that of the binary being
// emulated.
// emulateGuru tells query to emulate the output format of the guru tool.
emulateGuru = "guru"
)
// query implements the query command.
type query struct {
JSON bool `flag:"json" help:"emit output in JSON format"`
Emulate string `flag:"emulate" help:"compatability mode, causes gopls to emulate another tool.\nvalues depend on the operation being performed"`
app *Application
}
func (q *query) Name() string { return "query" }
func (q *query) Usage() string { return "query [flags] <mode> <mode args>" }
func (q *query) ShortHelp() string {
return "answer queries about go source code"
}
func (q *query) DetailedHelp(f *flag.FlagSet) {
fmt.Fprint(f.Output(), `
The mode argument determines the query to perform:
`)
for _, m := range q.modes() {
fmt.Fprintf(f.Output(), " %s : %v\n", m.Name(), m.ShortHelp())
}
fmt.Fprint(f.Output(), `
query flags are:
`)
f.PrintDefaults()
}
// Run takes the args after command flag processing, and invokes the correct
// query mode as specified by the first argument.
func (q *query) Run(ctx context.Context, args ...string) error {
if len(args) == 0 {
return tool.CommandLineErrorf("query must be supplied a mode")
}
mode, args := args[0], args[1:]
for _, m := range q.modes() {
if m.Name() == mode {
tool.Main(ctx, m, args)
return nil
}
}
return tool.CommandLineErrorf("unknown command %v", mode)
}
// modes returns the set of modes supported by the query command.
func (q *query) modes() []tool.Application {
return []tool.Application{
&definition{query: q},
}
}

Просмотреть файл

@ -38,7 +38,10 @@ func (s *Server) DetailedHelp(f *flag.FlagSet) {
fmt.Fprint(f.Output(), `
The server communicates using JSONRPC2 on stdin and stdout, and is intended to be run directly as
a child of an editor process.
gopls server flags are:
`)
f.PrintDefaults()
}
// Run configures a server based on the flags, and then runs it.

68
internal/lsp/cmd/testdata/a/a.go поставляемый Normal file
Просмотреть файл

@ -0,0 +1,68 @@
package a
type Thing struct { //@Thing
Member string //@Member
}
var Other Thing //@Other
func Things(val []string) []Thing { //@Things
return nil
}
func (t Thing) Method(i int) string { //@Method
return t.Member
}
func useThings() {
t := Thing{} //@mark(aStructType, "ing")
fmt.Print(t.Member) //@mark(aMember, "ember")
fmt.Print(Other) //@mark(aVar, "ther")
Things() //@mark(aFunc, "ings")
t.Method() //@mark(aMethod, "eth")
}
/*@
definition(aStructType, "", Thing, "$file:$line:$col,$eline:$ecol: defined here as type Thing struct{Member string}")
definition(aStructType, "-emulate=guru", Thing, "$file:$line:$col: defined here as type Thing")
definition(aMember, "", Member, "$file:$line:$col,$eline:$ecol: defined here as field Member string")
definition(aMember, "-emulate=guru", Member, "$file:$line:$col: defined here as field Member string")
definition(aVar, "", Other, "$file:$line:$col,$eline:$ecol: defined here as var Other Thing")
definition(aVar, "-emulate=guru", Other, "$file:$line:$col: defined here as var Other")
definition(aFunc, "", Things, "$file:$line:$col,$eline:$ecol: defined here as func Things(val []string) []Thing")
definition(aFunc, "-emulate=guru", Things, "$file:$line:$col: defined here as func Things(val []string) []Thing")
definition(aMethod, "", Method, "$file:$line:$col,$eline:$ecol: defined here as func (Thing).Method(i int) string")
definition(aMethod, "-emulate=guru", Method, "$file:$line:$col: defined here as func (Thing).Method(i int) string")
//param
//package name
//const
//anon field
// JSON tests
definition(aStructType, "-json", Thing, `{
"location": {
"file": "$efile",
"start": {
"line": $line,
"column": $col,
"offset": $offset
},
"end": {
"line": $eline,
"column": $ecol,
"offset": $eoffset
}
},
"description": "type Thing struct{Member string}"
}`)
definition(aStructType, "-json -emulate=guru", Thing, `{
"objpos": "$efile:$line:$col",
"desc": "type Thing"
}`)
*/

26
internal/lsp/cmd/testdata/b/b.go поставляемый Normal file
Просмотреть файл

@ -0,0 +1,26 @@
package b
import (
"golang.org/fake/a"
)
func useThings() {
t := a.Thing{} //@mark(bStructType, "ing")
fmt.Print(t.Member) //@mark(bMember, "ember")
fmt.Print(a.Other) //@mark(bVar, "ther")
a.Things() //@mark(bFunc, "ings")
}
/*@
definition(bStructType, "", Thing, "$file:$line:$col,$eline:$ecol: defined here as type a.Thing struct{Member string}")
definition(bStructType, "-emulate=guru", Thing, "$file:$line:$col: defined here as type golang.org/fake/a.Thing")
definition(bMember, "", Member, "$file:$line:$col,$eline:$ecol: defined here as field Member string")
definition(bMember, "-emulate=guru", Member, "$file:$line:$col: defined here as field Member string")
definition(bVar, "", Other, "$file:$line:$col,$eline:$ecol: defined here as var a.Other a.Thing")
definition(bVar, "-emulate=guru", Other, "$file:$line:$col: defined here as var golang.org/fake/a.Other")
definition(bFunc, "", Things, "$file:$line:$col,$eline:$ecol: defined here as func a.Things(val []string) []a.Thing")
definition(bFunc, "-emulate=guru", Things, "$file:$line:$col: defined here as func golang.org/fake/a.Things(val []string) []golang.org/fake/a.Thing")
*/