1
0
mirror of https://github.com/golang/go synced 2024-11-18 19:24:39 -07:00

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
parent 8dbcc66f33
commit 067a2f313b
9 changed files with 705 additions and 15 deletions

View File

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

View File

@ -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
}

View File

@ -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))
}

View File

@ -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

View File

@ -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
View 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},
}
}

View File

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

68
internal/lsp/cmd/testdata/a/a.go vendored Normal file
View 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 vendored Normal file
View 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")
*/