Skip to content
This repository was archived by the owner on Sep 30, 2024. It is now read-only.

Commit 67f73ab

Browse files
authored
Reimplement /go/ links without the Go langserver (#1197)
1 parent 3887ed6 commit 67f73ab

File tree

6 files changed

+503
-19
lines changed

6 files changed

+503
-19
lines changed

cmd/frontend/internal/app/go_symbol_url.go

Lines changed: 169 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,27 @@
11
package app
22

33
import (
4+
"context"
45
"errors"
56
"fmt"
7+
"go/ast"
8+
"go/build"
9+
"go/doc"
10+
"go/token"
611
"net/http"
712
"net/url"
813
"path"
914
"strings"
1015

11-
"github.com/sourcegraph/go-lsp/lspext"
16+
"github.com/sourcegraph/ctxvfs"
17+
"github.com/sourcegraph/sourcegraph/pkg/vfsutil"
18+
"golang.org/x/tools/go/buildutil"
19+
20+
"github.com/sourcegraph/go-lsp"
21+
22+
"github.com/hashicorp/go-multierror"
1223
"github.com/sourcegraph/sourcegraph/cmd/frontend/backend"
24+
"github.com/sourcegraph/sourcegraph/cmd/frontend/internal/app/pkg/golangserverutil"
1325
"github.com/sourcegraph/sourcegraph/pkg/api"
1426
"github.com/sourcegraph/sourcegraph/pkg/errcode"
1527
"github.com/sourcegraph/sourcegraph/pkg/gituri"
@@ -37,7 +49,27 @@ func serveGoSymbolURL(w http.ResponseWriter, r *http.Request) error {
3749
}
3850
}
3951

52+
// def
53+
// vvvvvvvvvvvv
54+
// http://sourcegraph.com/go/github.com/gorilla/mux/-/Router/Match
55+
// ^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^ ^^^^^
56+
// importPath receiver? symbolname
4057
importPath := strings.Split(symbolID, "/-/")[0]
58+
def := strings.Split(symbolID, "/-/")[1]
59+
var symbolName string
60+
var receiver *string
61+
symbolComponents := strings.Split(def, "/")
62+
switch len(symbolComponents) {
63+
case 1:
64+
symbolName = symbolComponents[0]
65+
case 2:
66+
// This is a method call.
67+
receiver = &symbolComponents[0]
68+
symbolName = symbolComponents[1]
69+
default:
70+
return fmt.Errorf("invalid def %s (must have 1 or 2 path components)", def)
71+
}
72+
4173
dir, err := gosrc.ResolveImportPath(httputil.CachingClient, importPath)
4274
if err != nil {
4375
return err
@@ -62,30 +94,150 @@ func serveGoSymbolURL(w http.ResponseWriter, r *http.Request) error {
6294
return err
6395
}
6496

65-
symbols, err := backend.Symbols.List(r.Context(), repo.Name, commitID, mode, lspext.WorkspaceSymbolParams{
66-
Symbol: lspext.SymbolDescriptor{"id": symbolID},
67-
})
97+
vfs, err := repoVFS(r.Context(), repoName, commitID)
6898
if err != nil {
6999
return err
70100
}
71101

72-
if len(symbols) > 0 {
73-
symbol := symbols[0]
74-
uri, err := gituri.Parse(string(symbol.Location.URI))
75-
if err != nil {
76-
return err
102+
location, err := symbolLocation(r.Context(), vfs, commitID, importPath, path.Join("/", dir.RepoPrefix, strings.TrimPrefix(dir.ImportPath, string(dir.ProjectRoot))), receiver, symbolName)
103+
if err != nil {
104+
return err
105+
}
106+
if location == nil {
107+
return &errcode.HTTPErr{
108+
Status: http.StatusNotFound,
109+
Err: errors.New("symbol not found"),
77110
}
78-
filePath := uri.Fragment
79-
dest := &url.URL{
80-
Path: "/" + path.Join(string(repo.Name), "-/blob", filePath),
81-
Fragment: fmt.Sprintf("L%d:%d$references", symbol.Location.Range.Start.Line+1, symbol.Location.Range.Start.Character+1),
111+
}
112+
113+
uri, err := gituri.Parse(string(location.URI))
114+
if err != nil {
115+
return err
116+
}
117+
filePath := uri.Fragment
118+
dest := &url.URL{
119+
Path: "/" + path.Join(string(repo.Name), "-/blob", filePath),
120+
Fragment: fmt.Sprintf("L%d:%d$references", location.Range.Start.Line+1, location.Range.Start.Character+1),
121+
}
122+
http.Redirect(w, r, dest.String(), http.StatusFound)
123+
return nil
124+
}
125+
126+
func symbolLocation(ctx context.Context, vfs ctxvfs.FileSystem, commitID api.CommitID, importPath string, path string, receiver *string, symbol string) (*lsp.Location, error) {
127+
bctx := buildContextFromVFS(ctx, vfs)
128+
129+
fileSet := token.NewFileSet()
130+
pkg, err := parseFiles(fileSet, &bctx, importPath, path)
131+
if err != nil {
132+
return nil, err
133+
}
134+
135+
pos := (func() *token.Pos {
136+
docPackage := doc.New(pkg, importPath, doc.AllDecls)
137+
for _, docConst := range docPackage.Consts {
138+
for _, spec := range docConst.Decl.Specs {
139+
if valueSpec, ok := spec.(*ast.ValueSpec); ok {
140+
for _, ident := range valueSpec.Names {
141+
if ident.Name == symbol {
142+
return &ident.NamePos
143+
}
144+
}
145+
}
146+
}
147+
}
148+
for _, docType := range docPackage.Types {
149+
if receiver != nil && docType.Name == *receiver {
150+
for _, method := range docType.Methods {
151+
if method.Name == symbol {
152+
return &method.Decl.Name.NamePos
153+
}
154+
}
155+
}
156+
for _, fun := range docType.Funcs {
157+
if fun.Name == symbol {
158+
return &fun.Decl.Name.NamePos
159+
}
160+
}
161+
for _, spec := range docType.Decl.Specs {
162+
if typeSpec, ok := spec.(*ast.TypeSpec); ok && typeSpec.Name.Name == symbol {
163+
return &typeSpec.Name.NamePos
164+
}
165+
}
166+
}
167+
for _, docVar := range docPackage.Vars {
168+
for _, spec := range docVar.Decl.Specs {
169+
if valueSpec, ok := spec.(*ast.ValueSpec); ok {
170+
for _, ident := range valueSpec.Names {
171+
if ident.Name == symbol {
172+
return &ident.NamePos
173+
}
174+
}
175+
}
176+
}
177+
}
178+
for _, docFunc := range docPackage.Funcs {
179+
if docFunc.Name == symbol {
180+
return &docFunc.Decl.Name.NamePos
181+
}
82182
}
83-
http.Redirect(w, r, dest.String(), http.StatusFound)
84183
return nil
184+
})()
185+
186+
if pos == nil {
187+
return nil, nil
188+
}
189+
190+
position := fileSet.Position(*pos)
191+
location := lsp.Location{
192+
URI: lsp.DocumentURI("https://" + string(importPath) + "?" + string(commitID) + "#" + position.Filename),
193+
Range: lsp.Range{
194+
Start: lsp.Position{
195+
Line: position.Line - 1,
196+
Character: position.Column - 1,
197+
},
198+
End: lsp.Position{
199+
Line: position.Line - 1,
200+
Character: position.Column - 1,
201+
},
202+
},
203+
}
204+
205+
return &location, nil
206+
}
207+
208+
func buildContextFromVFS(ctx context.Context, vfs ctxvfs.FileSystem) build.Context {
209+
bctx := build.Default
210+
golangserverutil.PrepareContext(&bctx, ctx, vfs)
211+
return bctx
212+
}
213+
214+
func repoVFS(ctx context.Context, name api.RepoName, rev api.CommitID) (ctxvfs.FileSystem, error) {
215+
if strings.HasPrefix(string(name), "github.com/") {
216+
return vfsutil.NewGitHubRepoVFS(string(name), string(rev))
217+
}
218+
219+
// Fall back to a full git clone for non-github.com repos.
220+
return nil, fmt.Errorf("unable to fetch repo %s (only github.com repos are supported)", name)
221+
}
222+
223+
func parseFiles(fset *token.FileSet, bctx *build.Context, importPath, srcDir string) (*ast.Package, error) {
224+
bpkg, err := bctx.ImportDir(srcDir, 0)
225+
if err != nil {
226+
return nil, err
85227
}
86228

87-
return &errcode.HTTPErr{
88-
Status: http.StatusNotFound,
89-
Err: errors.New("symbol not found"),
229+
pkg := &ast.Package{
230+
Files: map[string]*ast.File{},
90231
}
232+
var errs error
233+
for _, file := range append(bpkg.GoFiles, bpkg.TestGoFiles...) {
234+
if src, err := buildutil.ParseFile(fset, bctx, nil, buildutil.JoinPath(bctx, srcDir), file, 0); err == nil {
235+
pkg.Name = src.Name.Name
236+
pkg.Files[file] = src
237+
} else {
238+
errs = multierror.Append(errs, err)
239+
}
240+
}
241+
242+
return pkg, errs
91243
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package app
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/sourcegraph/ctxvfs"
8+
"github.com/sourcegraph/go-langserver/pkg/lsp"
9+
"github.com/sourcegraph/sourcegraph/pkg/api"
10+
)
11+
12+
type symbolLocationArgs struct {
13+
vfs map[string]string
14+
commitID api.CommitID
15+
importPath string
16+
path string
17+
receiver *string
18+
symbol string
19+
}
20+
21+
type test struct {
22+
args symbolLocationArgs
23+
want *lsp.Location
24+
}
25+
26+
func mkLocation(uri string, line int, character int) *lsp.Location {
27+
return &lsp.Location{
28+
URI: "https://github.com/gorilla/mux?deadbeefdeadbeefdeadbeefdeadbeefdeadbeef#/mux.go",
29+
Range: lsp.Range{
30+
Start: lsp.Position{
31+
Line: line,
32+
Character: character,
33+
},
34+
End: lsp.Position{
35+
Line: line,
36+
Character: character,
37+
},
38+
},
39+
}
40+
}
41+
42+
func strptr(s string) *string {
43+
return &s
44+
}
45+
46+
func TestSymbolLocation(t *testing.T) {
47+
vfs := map[string]string{
48+
"mux.go": "package mux\nconst Foo = 5\ntype Bar int\nfunc (b Bar) Quux() {}\nvar Floop = 6",
49+
}
50+
51+
tests := []test{
52+
test{
53+
args: symbolLocationArgs{
54+
vfs: vfs,
55+
commitID: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
56+
importPath: "github.com/gorilla/mux",
57+
path: "/",
58+
receiver: nil,
59+
symbol: "NonexistentSymbol",
60+
},
61+
want: nil,
62+
},
63+
test{
64+
args: symbolLocationArgs{
65+
vfs: vfs,
66+
commitID: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
67+
importPath: "github.com/gorilla/mux",
68+
path: "/",
69+
receiver: nil,
70+
symbol: "Foo",
71+
},
72+
want: mkLocation("https://github.com/gorilla/mux?deadbeefdeadbeefdeadbeefdeadbeefdeadbeef#mux.go", 1, 6),
73+
},
74+
test{
75+
args: symbolLocationArgs{
76+
vfs: vfs,
77+
commitID: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
78+
importPath: "github.com/gorilla/mux",
79+
path: "/",
80+
receiver: nil,
81+
symbol: "Bar",
82+
},
83+
want: mkLocation("https://github.com/gorilla/mux?deadbeefdeadbeefdeadbeefdeadbeefdeadbeef#mux.go", 2, 5),
84+
},
85+
test{
86+
args: symbolLocationArgs{
87+
vfs: vfs,
88+
commitID: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
89+
importPath: "github.com/gorilla/mux",
90+
path: "/",
91+
receiver: strptr("Bar"),
92+
symbol: "Quux",
93+
},
94+
want: mkLocation("https://github.com/gorilla/mux?deadbeefdeadbeefdeadbeefdeadbeefdeadbeef#mux.go", 3, 13),
95+
},
96+
test{
97+
args: symbolLocationArgs{
98+
vfs: vfs,
99+
commitID: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
100+
importPath: "github.com/gorilla/mux",
101+
path: "/",
102+
receiver: nil,
103+
symbol: "Floop",
104+
},
105+
want: mkLocation("https://github.com/gorilla/mux?deadbeefdeadbeefdeadbeefdeadbeefdeadbeef#mux.go", 4, 4),
106+
},
107+
}
108+
for i, test := range tests {
109+
got, _ := symbolLocation(context.Background(), mapFS(test.args.vfs), test.args.commitID, test.args.importPath, test.args.path, test.args.receiver, test.args.symbol)
110+
if got != test.want && (got == nil || test.want == nil || *got != *test.want) {
111+
t.Errorf("Test #%d:\ngot %#v\nwant %#v", i, got, test.want)
112+
}
113+
}
114+
}
115+
116+
// mapFS lets us easily instantiate a VFS with a map[string]string
117+
// (which is less noisy than map[string][]byte in test fixtures).
118+
func mapFS(m map[string]string) *stringMapFS {
119+
m2 := make(map[string][]byte, len(m))
120+
filenames := make([]string, 0, len(m))
121+
for k, v := range m {
122+
m2[k] = []byte(v)
123+
filenames = append(filenames, k)
124+
}
125+
return &stringMapFS{
126+
FileSystem: ctxvfs.Map(m2),
127+
filenames: filenames,
128+
}
129+
}
130+
131+
type stringMapFS struct {
132+
ctxvfs.FileSystem
133+
filenames []string
134+
}
135+
136+
func (fs *stringMapFS) ListAllFiles(ctx context.Context) ([]string, error) {
137+
return fs.filenames, nil
138+
}

0 commit comments

Comments
 (0)