From 067a2f313b693f3c2d3c0eb328434c91c7fc9b6b Mon Sep 17 00:00:00 2001 From: Ian Cottrell Date: Fri, 14 Dec 2018 15:46:12 -0500 Subject: [PATCH] internal/lsp/cmd: add the definition mode Change-Id: Ib171016fb1bb063a6424677458b554a08144465c Reviewed-on: https://go-review.googlesource.com/c/159438 Reviewed-by: Rebecca Stambler --- internal/lsp/cmd/cmd.go | 42 ++++--- internal/lsp/cmd/definition.go | 179 ++++++++++++++++++++++++++++ internal/lsp/cmd/definition_test.go | 163 +++++++++++++++++++++++++ internal/lsp/cmd/export_test.go | 7 ++ internal/lsp/cmd/location.go | 161 +++++++++++++++++++++++++ internal/lsp/cmd/query.go | 71 +++++++++++ internal/lsp/cmd/server.go | 3 + internal/lsp/cmd/testdata/a/a.go | 68 +++++++++++ internal/lsp/cmd/testdata/b/b.go | 26 ++++ 9 files changed, 705 insertions(+), 15 deletions(-) create mode 100644 internal/lsp/cmd/definition.go create mode 100644 internal/lsp/cmd/definition_test.go create mode 100644 internal/lsp/cmd/export_test.go create mode 100644 internal/lsp/cmd/location.go create mode 100644 internal/lsp/cmd/query.go create mode 100644 internal/lsp/cmd/testdata/a/a.go create mode 100644 internal/lsp/cmd/testdata/b/b.go diff --git a/internal/lsp/cmd/cmd.go b/internal/lsp/cmd/cmd.go index 9bf80dd3..b054dcf2 100644 --- a/internal/lsp/cmd/cmd.go +++ b/internal/lsp/cmd/cmd.go @@ -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-flags] [mode-args]" } +func (app *Application) Usage() string { return " [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}, } } diff --git a/internal/lsp/cmd/definition.go b/internal/lsp/cmd/definition.go new file mode 100644 index 00000000..861de838 --- /dev/null +++ b/internal/lsp/cmd/definition.go @@ -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 "" } +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 +} diff --git a/internal/lsp/cmd/definition_test.go b/internal/lsp/cmd/definition_test.go new file mode 100644 index 00000000..d02fab2c --- /dev/null +++ b/internal/lsp/cmd/definition_test.go @@ -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)) +} diff --git a/internal/lsp/cmd/export_test.go b/internal/lsp/cmd/export_test.go new file mode 100644 index 00000000..4377c8d0 --- /dev/null +++ b/internal/lsp/cmd/export_test.go @@ -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 diff --git a/internal/lsp/cmd/location.go b/internal/lsp/cmd/location.go new file mode 100644 index 00000000..35e25806 --- /dev/null +++ b/internal/lsp/cmd/location.go @@ -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.*):(?P(?P\d+):(?P\d)+|#(?P\d+))(?P:(?P\d+):(?P\d+)|#(?P\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) +} diff --git a/internal/lsp/cmd/query.go b/internal/lsp/cmd/query.go new file mode 100644 index 00000000..3d0bf30f --- /dev/null +++ b/internal/lsp/cmd/query.go @@ -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] " } +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}, + } +} diff --git a/internal/lsp/cmd/server.go b/internal/lsp/cmd/server.go index dc9a2c4e..e205052f 100644 --- a/internal/lsp/cmd/server.go +++ b/internal/lsp/cmd/server.go @@ -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. diff --git a/internal/lsp/cmd/testdata/a/a.go b/internal/lsp/cmd/testdata/a/a.go new file mode 100644 index 00000000..23364341 --- /dev/null +++ b/internal/lsp/cmd/testdata/a/a.go @@ -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" +}`) +*/ diff --git a/internal/lsp/cmd/testdata/b/b.go b/internal/lsp/cmd/testdata/b/b.go new file mode 100644 index 00000000..bd197de6 --- /dev/null +++ b/internal/lsp/cmd/testdata/b/b.go @@ -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") +*/