Skip to content
This repository was archived by the owner on Oct 12, 2022. It is now read-only.

Commit 0f37f10

Browse files
author
Stephen Gutekanst
authored
Merge pull request #195 from sourcegraph/sg/perf2
enable godef-based textDocument/definition implementation
2 parents fe8fc70 + fcaddf9 commit 0f37f10

16 files changed

+4691
-23
lines changed

langserver/definition.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,27 @@ import (
55
"errors"
66
"fmt"
77
"go/ast"
8+
"go/build"
9+
"go/token"
10+
"io/ioutil"
811
"log"
12+
"path/filepath"
913

14+
"github.com/sourcegraph/go-langserver/langserver/internal/godef"
1015
"github.com/sourcegraph/go-langserver/langserver/internal/refs"
1116
"github.com/sourcegraph/go-langserver/pkg/lsp"
1217
"github.com/sourcegraph/jsonrpc2"
1318
)
1419

20+
// UseBinaryPkgCache controls whether or not $GOPATH/pkg binary .a files should
21+
// be used.
22+
var UseBinaryPkgCache = false
23+
1524
func (h *LangHandler) handleDefinition(ctx context.Context, conn jsonrpc2.JSONRPC2, req *jsonrpc2.Request, params lsp.TextDocumentPositionParams) ([]lsp.Location, error) {
25+
if UseBinaryPkgCache {
26+
return h.handleDefinitionGodef(ctx, conn, req, params)
27+
}
28+
1629
res, err := h.handleXDefinition(ctx, conn, req, params)
1730
if err != nil {
1831
return nil, err
@@ -24,6 +37,41 @@ func (h *LangHandler) handleDefinition(ctx context.Context, conn jsonrpc2.JSONRP
2437
return locs, nil
2538
}
2639

40+
func (h *LangHandler) handleDefinitionGodef(ctx context.Context, conn jsonrpc2.JSONRPC2, req *jsonrpc2.Request, params lsp.TextDocumentPositionParams) ([]lsp.Location, error) {
41+
// Read file contents and calculate byte offset.
42+
filename := h.FilePath(params.TextDocument.URI)
43+
contents, err := ioutil.ReadFile(filename)
44+
if err != nil {
45+
return nil, err
46+
}
47+
offset, valid, why := offsetForPosition(contents, params.Position)
48+
if !valid {
49+
return nil, fmt.Errorf("invalid position: %s:%d:%d (%s)", filename, params.Position.Line, params.Position.Character, why)
50+
}
51+
52+
// Invoke godef to determine the position of the definition.
53+
fset := token.NewFileSet()
54+
res, err := godef.Godef(fset, offset, filename, contents)
55+
if err != nil {
56+
return nil, err
57+
}
58+
if res.Package != nil {
59+
// TODO: return directory location. This right now at least matches our
60+
// other implementation.
61+
return []lsp.Location{}, nil
62+
}
63+
loc := goRangeToLSPLocation(fset, res.Start, res.End)
64+
65+
if loc.URI == "file://" {
66+
// TODO: builtins do not have valid URIs or locations, so we emit a
67+
// phony location here instead. This is better than our other
68+
// implementation.
69+
loc.URI = pathToURI(filepath.Join(build.Default.GOROOT, "/src/builtin/builtin.go"))
70+
loc.Range = lsp.Range{}
71+
}
72+
return []lsp.Location{loc}, nil
73+
}
74+
2775
func (h *LangHandler) handleXDefinition(ctx context.Context, conn jsonrpc2.JSONRPC2, req *jsonrpc2.Request, params lsp.TextDocumentPositionParams) ([]symbolLocationInformation, error) {
2876
if !isFileURI(params.TextDocument.URI) {
2977
return nil, &jsonrpc2.Error{

langserver/handler.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,11 @@ func (h *LangHandler) Handle(ctx context.Context, conn jsonrpc2.JSONRPC2, req *j
354354
}
355355
if uri != "" {
356356
// a user is viewing this path, hint to add it to the cache
357-
go h.typecheck(ctx, conn, uri, lsp.Position{})
357+
// (unless we're primarily using binary package cache .a
358+
// files).
359+
if !UseBinaryPkgCache {
360+
go h.typecheck(ctx, conn, uri, lsp.Position{})
361+
}
358362
}
359363
return nil, err
360364
}

langserver/integration_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ func TestIntegration_FileSystem(t *testing.T) {
7171
"p2/c.go:1:40": "func A()",
7272
},
7373
}
74-
lspTests(t, ctx, conn, rootPath, cases)
74+
lspTests(t, ctx, nil, conn, rootPath, cases)
7575

7676
// Now mimic what happens when a file is edited but not yet
7777
// saved. It should re-typecheck using the unsaved file contents.
@@ -90,5 +90,5 @@ func TestIntegration_FileSystem(t *testing.T) {
9090
"p2/c.go:1:40": "func A() int",
9191
},
9292
}
93-
lspTests(t, ctx, conn, rootPath, cases)
93+
lspTests(t, ctx, nil, conn, rootPath, cases)
9494
}

langserver/internal/godef/LICENSE

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
Copyright © 2014, Roger Peppe
2+
All rights reserved.
3+
4+
Redistribution and use in source and binary forms, with or without modification,
5+
are permitted provided that the following conditions are met:
6+
7+
* Redistributions of source code must retain the above copyright notice,
8+
this list of conditions and the following disclaimer.
9+
* Redistributions in binary form must reproduce the above copyright notice,
10+
this list of conditions and the following disclaimer in the documentation
11+
and/or other materials provided with the distribution.
12+
* Neither the name of this project nor the names of its contributors
13+
may be used to endorse or promote products derived from this software
14+
without specific prior written permission.
15+
16+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19+
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20+
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
22+
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
23+
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
24+
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
25+
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
26+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
// Copyright 2009 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// This file contains the exported entry points for invoking the parser.
6+
7+
package parser
8+
9+
import (
10+
"bytes"
11+
"errors"
12+
"io"
13+
"io/ioutil"
14+
"os"
15+
"path/filepath"
16+
17+
"go/ast"
18+
19+
"go/token"
20+
)
21+
22+
// ImportPathToName is the type of the function that's used
23+
// to find the package name for an imported package path.
24+
// The fromDir argument holds the directory that contains the
25+
// import statement, which may be empty.
26+
type ImportPathToName func(path string, fromDir string) (string, error)
27+
28+
// If src != nil, readSource converts src to a []byte if possible;
29+
// otherwise it returns an error. If src == nil, readSource returns
30+
// the result of reading the file specified by filename.
31+
//
32+
func readSource(filename string, src interface{}) ([]byte, error) {
33+
if src != nil {
34+
switch s := src.(type) {
35+
case string:
36+
return []byte(s), nil
37+
case []byte:
38+
return s, nil
39+
case *bytes.Buffer:
40+
// is io.Reader, but src is already available in []byte form
41+
if s != nil {
42+
return s.Bytes(), nil
43+
}
44+
case io.Reader:
45+
var buf bytes.Buffer
46+
_, err := io.Copy(&buf, s)
47+
if err != nil {
48+
return nil, err
49+
}
50+
return buf.Bytes(), nil
51+
default:
52+
return nil, errors.New("invalid source")
53+
}
54+
}
55+
56+
return ioutil.ReadFile(filename)
57+
}
58+
59+
func (p *parser) parseEOF() error {
60+
p.expect(token.EOF)
61+
p.ErrorList.Sort()
62+
return p.ErrorList.Err()
63+
}
64+
65+
// ParseExpr parses a Go expression and returns the corresponding
66+
// AST node. The fset, filename, and src arguments have the same interpretation
67+
// as for ParseFile. If there is an error, the result expression
68+
// may be nil or contain a partial AST.
69+
//
70+
// if scope is non-nil, it will be used as the scope for the expression.
71+
//
72+
func ParseExpr(fset *token.FileSet, filename string, src interface{}, scope *ast.Scope, pathToName ImportPathToName) (ast.Expr, error) {
73+
data, err := readSource(filename, src)
74+
if err != nil {
75+
return nil, err
76+
}
77+
78+
var p parser
79+
p.init(fset, filename, data, 0, scope, pathToName)
80+
x := p.parseExpr()
81+
if p.tok == token.SEMICOLON {
82+
p.next() // consume automatically inserted semicolon, if any
83+
}
84+
return x, p.parseEOF()
85+
}
86+
87+
// ParseFile parses the source code of a single Go source file and returns
88+
// the corresponding ast.File node. The source code may be provided via
89+
// the filename of the source file, or via the src parameter.
90+
//
91+
// If src != nil, ParseFile parses the source from src and the filename is
92+
// only used when recording position information. The type of the argument
93+
// for the src parameter must be string, []byte, or io.Reader.
94+
//
95+
// If src == nil, ParseFile parses the file specified by filename.
96+
//
97+
// The mode parameter controls the amount of source text parsed and other
98+
// optional parser functionality. Position information is recorded in the
99+
// file set fset.
100+
//
101+
// If the source couldn't be read, the returned AST is nil and the error
102+
// indicates the specific failure. If the source was read but syntax
103+
// errors were found, the result is a partial AST (with ast.BadX nodes
104+
// representing the fragments of erroneous source code). Multiple errors
105+
// are returned via a scanner.ErrorList which is sorted by file position.
106+
//
107+
func ParseFile(fset *token.FileSet, filename string, src interface{}, mode uint, pkgScope *ast.Scope, pathToName ImportPathToName) (*ast.File, error) {
108+
data, err := readSource(filename, src)
109+
if err != nil {
110+
return nil, err
111+
}
112+
113+
var p parser
114+
p.init(fset, filename, data, mode, pkgScope, pathToName)
115+
p.pkgScope = p.topScope
116+
p.openScope()
117+
p.fileScope = p.topScope
118+
p.ErrorList.RemoveMultiples()
119+
return p.parseFile(), p.ErrorList.Err() // parseFile() reads to EOF
120+
}
121+
122+
func parseFileInPkg(fset *token.FileSet, pkgs map[string]*ast.Package, filename string, mode uint, pathToName ImportPathToName) (err error) {
123+
data, err := readSource(filename, nil)
124+
if err != nil {
125+
return err
126+
}
127+
// first find package name, so we can use the correct package
128+
// scope when parsing the file.
129+
src, err := ParseFile(fset, filename, data, PackageClauseOnly, nil, pathToName)
130+
if err != nil {
131+
return
132+
}
133+
name := src.Name.Name
134+
pkg := pkgs[name]
135+
if pkg == nil {
136+
pkg = &ast.Package{name, ast.NewScope(Universe), nil, make(map[string]*ast.File)}
137+
pkgs[name] = pkg
138+
}
139+
src, err = ParseFile(fset, filename, data, mode, pkg.Scope, pathToName)
140+
if err != nil {
141+
return
142+
}
143+
pkg.Files[filename] = src
144+
return
145+
}
146+
147+
// ParseFiles calls ParseFile for each file in the filenames list and returns
148+
// a map of package name -> package AST with all the packages found. The mode
149+
// bits are passed to ParseFile unchanged. Position information is recorded
150+
// in the file set fset.
151+
//
152+
// Files with parse errors are ignored. In this case the map of packages may
153+
// be incomplete (missing packages and/or incomplete packages) and the first
154+
// error encountered is returned.
155+
//
156+
func ParseFiles(fset *token.FileSet, filenames []string, mode uint, pathToName ImportPathToName) (pkgs map[string]*ast.Package, first error) {
157+
pkgs = make(map[string]*ast.Package)
158+
for _, filename := range filenames {
159+
if err := parseFileInPkg(fset, pkgs, filename, mode, pathToName); err != nil && first == nil {
160+
first = err
161+
}
162+
}
163+
return
164+
}
165+
166+
// ParseDir calls ParseFile for the files in the directory specified by path and
167+
// returns a map of package name -> package AST with all the packages found. If
168+
// filter != nil, only the files with os.FileInfo entries passing through the filter
169+
// are considered. The mode bits are passed to ParseFile unchanged. Position
170+
// information is recorded in the file set fset.
171+
//
172+
// If the directory couldn't be read, a nil map and the respective error are
173+
// returned. If a parse error occurred, a non-nil but incomplete map and the
174+
// error are returned.
175+
//
176+
func ParseDir(fset *token.FileSet, path string, filter func(os.FileInfo) bool, mode uint, pathToName ImportPathToName) (map[string]*ast.Package, error) {
177+
fd, err := os.Open(path)
178+
if err != nil {
179+
return nil, err
180+
}
181+
defer fd.Close()
182+
183+
list, err := fd.Readdir(-1)
184+
if err != nil {
185+
return nil, err
186+
}
187+
188+
filenames := make([]string, len(list))
189+
n := 0
190+
for i := 0; i < len(list); i++ {
191+
d := list[i]
192+
if filter == nil || filter(d) {
193+
filenames[n] = filepath.Join(path, d.Name())
194+
n++
195+
}
196+
}
197+
filenames = filenames[0:n]
198+
199+
return ParseFiles(fset, filenames, mode, pathToName)
200+
}

0 commit comments

Comments
 (0)