├── langserver ├── internal │ ├── refs │ │ └── testdata │ │ │ ├── empty.go │ │ │ ├── imports.go │ │ │ ├── http-request-headers.go │ │ │ ├── vars.go │ │ │ ├── convoluted.go │ │ │ └── defs.go │ ├── gocode │ │ ├── .gitignore │ │ ├── gbimporter │ │ │ ├── samepath_unix.go │ │ │ ├── samepath_windows.go │ │ │ ├── context.go │ │ │ └── gbimporter.go │ │ ├── LICENSE │ │ ├── gocode.go │ │ ├── suggest │ │ │ ├── formatters.go │ │ │ ├── candidate.go │ │ │ └── suggest.go │ │ └── lookdot │ │ │ └── lookdot.go │ └── godef │ │ ├── go │ │ ├── parser │ │ │ ├── parser_go18.go │ │ │ ├── parser_go19.go │ │ │ ├── universe.go │ │ │ ├── parser_test.go │ │ │ └── interface.go │ │ └── types │ │ │ ├── goodarch.go │ │ │ └── objpos.go │ │ └── LICENSE ├── doc.go ├── types.go ├── isAlias19.go ├── isAlias18.go ├── completion_test.go ├── langserver_go19_test.go ├── cancel.go ├── rename.go ├── xcache.go ├── build_context_test.go ├── cancel_test.go ├── util │ ├── context.go │ └── util.go ├── format_test.go ├── partial.go ├── handler_common.go ├── vendor │ └── go │ │ └── doc │ │ ├── synopsis.go │ │ ├── filter.go │ │ ├── headscan.go │ │ └── doc.go ├── lsp.go ├── lspx.go ├── fs_test.go ├── signature.go ├── build_context.go ├── handler_shared.go ├── config.go ├── tracing.go ├── format.go ├── cache.go ├── completion.go ├── handler_shared_test.go ├── symbol_test.go ├── lint.go ├── diagnostics.go └── lint_test.go ├── .gitattributes ├── pkg ├── tools │ ├── doc.go │ ├── importgraph.go │ └── buildutil.go ├── lspext │ ├── partial.go │ ├── implementation.go │ ├── cache.go │ ├── filesext.go │ └── lspext.go └── lsp │ ├── doc.go │ ├── jsonrpc2.go │ └── structures.go ├── .github └── workflows │ └── lsif.yml ├── appveyor.yml ├── Dockerfile ├── deploy.sh ├── .travis.yml ├── diskcache ├── url_mutex.go └── cache_test.go ├── vfsutil ├── url_mutex.go ├── github_archive_test.go ├── github_archive.go ├── vfs_test.go ├── cache.go ├── git_test.go ├── zip.go ├── archive.go └── git.go ├── brew ├── README.md ├── generate.sh ├── go-langserver.rb.template └── go-langserver.rb ├── LICENSE ├── debugserver ├── expvar.go └── debug.go ├── gosrc └── NOTICE ├── NOTICE ├── go.mod ├── tracer └── tracer.go └── gituri ├── uri.go └── uri_test.go /langserver/internal/refs/testdata/empty.go: -------------------------------------------------------------------------------- 1 | package main // no external refs 2 | -------------------------------------------------------------------------------- /langserver/internal/refs/testdata/imports.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import _ "net/http" // "net/http" 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # We have tests which parse files and fail if file offsets change. So disable 2 | # EOL conversions. 3 | **/testdata/* -text 4 | -------------------------------------------------------------------------------- /langserver/doc.go: -------------------------------------------------------------------------------- 1 | // Package langserver is a language server for Go that adheres to the 2 | // Language Server Protocol (LSP). 3 | package langserver 4 | -------------------------------------------------------------------------------- /pkg/tools/doc.go: -------------------------------------------------------------------------------- 1 | // tools mainly contains variations on code found under golang.org/x/tools, 2 | // but modified to be optimized for our usecase. 3 | package tools 4 | -------------------------------------------------------------------------------- /langserver/internal/gocode/.gitignore: -------------------------------------------------------------------------------- 1 | *.8 2 | *.a 3 | *.out 4 | gocode 5 | gocode.exe 6 | goremote 7 | gocodetest 8 | *.swp 9 | listidents 10 | showcursor 11 | showsmap 12 | rename 13 | -------------------------------------------------------------------------------- /pkg/lspext/partial.go: -------------------------------------------------------------------------------- 1 | package lspext 2 | 3 | import "github.com/sourcegraph/go-lsp/lspext" 4 | 5 | // PartialResultParams is the input for "$/partialResult", a notification. 6 | type PartialResultParams = lspext.PartialResultParams 7 | -------------------------------------------------------------------------------- /pkg/lsp/doc.go: -------------------------------------------------------------------------------- 1 | // Package lsp contains Go types for the messages used in the Language 2 | // Server Protocol. 3 | // 4 | // See https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md 5 | // for more information. 6 | package lsp 7 | -------------------------------------------------------------------------------- /langserver/internal/gocode/gbimporter/samepath_unix.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package gbimporter 4 | 5 | // samePath checks two file paths for their equality based on the current filesystem 6 | func samePath(a, b string) bool { 7 | return a == b 8 | } 9 | -------------------------------------------------------------------------------- /pkg/lsp/jsonrpc2.go: -------------------------------------------------------------------------------- 1 | package lsp 2 | 3 | import ( 4 | "github.com/sourcegraph/go-lsp" 5 | ) 6 | 7 | // ID represents a JSON-RPC 2.0 request ID, which may be either a 8 | // string or number (or null, which is unsupported). 9 | type ID = lsp.ID 10 | -------------------------------------------------------------------------------- /pkg/lspext/implementation.go: -------------------------------------------------------------------------------- 1 | package lspext 2 | 3 | import "github.com/sourcegraph/go-lsp/lspext" 4 | 5 | // ImplementationLocation is a superset of lsp.Location with additional Go-specific information 6 | // about the implementation. 7 | type ImplementationLocation = lspext.ImplementationLocation 8 | -------------------------------------------------------------------------------- /langserver/types.go: -------------------------------------------------------------------------------- 1 | package langserver 2 | 3 | import "go/types" 4 | 5 | // deref returns a pointer's element type; otherwise it returns typ. 6 | func deref(typ types.Type) types.Type { 7 | if p, ok := typ.Underlying().(*types.Pointer); ok { 8 | return p.Elem() 9 | } 10 | return typ 11 | } 12 | -------------------------------------------------------------------------------- /langserver/internal/gocode/gbimporter/samepath_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package gbimporter 4 | 5 | import ( 6 | "strings" 7 | ) 8 | 9 | // samePath checks two file paths for their equality based on the current filesystem 10 | func samePath(a, b string) bool { 11 | return strings.EqualFold(a, b) 12 | } 13 | -------------------------------------------------------------------------------- /langserver/internal/refs/testdata/http-request-headers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" // "net/http" 5 | ) 6 | 7 | func main() { 8 | r := &http.Request{} // "net/http Request" 9 | r.Header = nil // "net/http Request Header" 10 | x := r 11 | x.Header["Content-Encoding"] = []string{"application/json"} // "net/http Request Header" 12 | } 13 | -------------------------------------------------------------------------------- /langserver/internal/refs/testdata/vars.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "net/http" // "net/http" 4 | 5 | func main() { 6 | var x *http.Client // "net/http Client" 7 | var y int 8 | z := http.RoundTripper(nil) // "net/http RoundTripper" 9 | _ = x 10 | _ = y 11 | _ = &http.Client{ // "net/http Client" 12 | Transport: z, // "net/http Client Transport" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/lsif.yml: -------------------------------------------------------------------------------- 1 | name: LSIF 2 | on: 3 | - push 4 | jobs: 5 | lsif-go: 6 | runs-on: ubuntu-latest 7 | container: sourcegraph/lsif-go 8 | steps: 9 | - uses: actions/checkout@v1 10 | - name: Generate LSIF data 11 | run: lsif-go 12 | - name: Upload LSIF data 13 | run: src lsif upload -github-token=${{ secrets.GITHUB_TOKEN }} 14 | -------------------------------------------------------------------------------- /langserver/isAlias19.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 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 | // +build go1.9 6 | 7 | package langserver 8 | 9 | import "go/types" 10 | 11 | func isAlias(obj *types.TypeName) bool { 12 | return obj.IsAlias() 13 | } 14 | 15 | const HasAlias = true 16 | -------------------------------------------------------------------------------- /langserver/isAlias18.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 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 | // +build !go1.9 6 | 7 | package langserver 8 | 9 | import "go/types" 10 | 11 | func isAlias(obj *types.TypeName) bool { 12 | return false // there are no type aliases before Go 1.9 13 | } 14 | 15 | const HasAlias = false 16 | -------------------------------------------------------------------------------- /pkg/lspext/cache.go: -------------------------------------------------------------------------------- 1 | package lspext 2 | 3 | import "github.com/sourcegraph/go-lsp/lspext" 4 | 5 | // See https://github.com/sourcegraph/language-server-protocol/pull/14 6 | 7 | // CacheGetParams is the input for 'cache/get'. The response is any or null. 8 | type CacheGetParams = lspext.CacheGetParams 9 | 10 | // CacheSetParams is the input for the notification 'cache/set'. 11 | type CacheSetParams = lspext.CacheSetParams 12 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: "{build}" 2 | shallow_clone: true 3 | clone_folder: c:\gopath\src\github.com\sourcegraph\go-langserver 4 | environment: 5 | GOPATH: c:\gopath 6 | install: 7 | - set PATH=C:\go112;C:\msys64\mingw64\bin;%PATH%;%GOPATH%\bin 8 | - echo %PATH% 9 | - echo %GOPATH% 10 | - go version 11 | - go env 12 | - go get -d -t ./... 13 | - go test -i ./... 14 | build_script: 15 | - go get golang.org/x/lint/golint 16 | - go test -short -race ./... 17 | -------------------------------------------------------------------------------- /pkg/lspext/filesext.go: -------------------------------------------------------------------------------- 1 | package lspext 2 | 3 | import "github.com/sourcegraph/go-lsp/lspext" 4 | 5 | // See https://github.com/sourcegraph/language-server-protocol/pull/4. 6 | 7 | // ContentParams is the input for 'textDocument/content'. The response is a 8 | // 'TextDocumentItem'. 9 | type ContentParams = lspext.ContentParams 10 | 11 | // FilesParams is the input for 'workspace/xfiles'. The response is '[]TextDocumentIdentifier' 12 | type FilesParams = lspext.FilesParams 13 | -------------------------------------------------------------------------------- /langserver/internal/refs/testdata/convoluted.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import myhttp "net/http" // "net/http" 4 | 5 | type unexpA struct { 6 | *myhttp.Client // net/http Client" 7 | } 8 | 9 | type unexpB struct { 10 | *unexpA 11 | } 12 | 13 | func main() { 14 | var x unexpB 15 | x.Transport.RoundTrip(nil) // "net/http Client Transport", "net/http RoundTripper RoundTrip" 16 | 17 | b := x 18 | c := b 19 | _ = c.unexpA.Transport.RoundTrip // "net/http Client Transport", "net/http RoundTripper RoundTrip" 20 | } 21 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine AS builder 2 | 3 | RUN apk add --no-cache ca-certificates 4 | 5 | ENV CGO_ENABLED=0 GO111MODULE=on 6 | WORKDIR /go/src/github.com/google/zoekt 7 | 8 | # Cache dependencies 9 | COPY go.mod go.sum ./ 10 | RUN go mod download 11 | 12 | COPY . ./ 13 | RUN go install . 14 | 15 | FROM alpine AS go-langserver 16 | 17 | RUN apk add --no-cache openssh git curl ca-certificates bind-tools tini 18 | 19 | COPY --from=builder /go/bin/* /usr/local/bin/ 20 | 21 | ENTRYPOINT ["/sbin/tini", "--"] 22 | CMD ["go-langserver"] 23 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | cd $(dirname "${BASH_SOURCE[0]}") 4 | 5 | # Build image 6 | VERSION=$(printf "%05d" $BUILDKITE_BUILD_NUMBER)_$(date +%Y-%m-%d)_$(git rev-parse --short HEAD) 7 | docker build -t sourcegraph/lang-go:$VERSION . 8 | 9 | # Upload to Docker Hub 10 | docker push sourcegraph/lang-go:$VERSION 11 | docker tag sourcegraph/lang-go:$VERSION sourcegraph/lang-go:latest 12 | docker push sourcegraph/lang-go:latest 13 | docker tag sourcegraph/lang-go:$VERSION sourcegraph/lang-go:insiders 14 | docker push sourcegraph/lang-go:insiders 15 | -------------------------------------------------------------------------------- /langserver/internal/refs/testdata/defs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "net/http" // "net/http" 4 | 5 | type wrapper struct { 6 | c *http.Client // "net/http Client" 7 | x http.RoundTripper // "net/http RoundTripper" 8 | } 9 | 10 | func foobar(c *http.Client) error { // "net/http Client" 11 | w := &wrapper{ 12 | c: &http.Client{}, // "net/http Client" 13 | x: http.RoundTripper(nil), // "net/http RoundTripper" 14 | } 15 | _ = w 16 | return nil 17 | } 18 | 19 | func main() { 20 | c := &http.Client{} // "net/http Client" 21 | foobar(c) 22 | } 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.10.x 5 | - 1.11.x 6 | 7 | go_import_path: github.com/sourcegraph/go-langserver 8 | 9 | os: 10 | - windows 11 | - linux 12 | 13 | branches: 14 | only: 15 | - master 16 | 17 | install: 18 | - go get -d -t ./... 19 | - go get golang.org/x/lint/golint 20 | - go test -i ./... 21 | 22 | script: 23 | - cd $TRAVIS_BUILD_DIR 24 | # Travis windows is changing line endings, breaking tests. Switch to LF. 25 | - find langserver -type f -print0 | xargs -0 sed -i ':a;N;$!ba;s/\r//g' 26 | - go test -short -timeout 5m -race ./... 27 | -------------------------------------------------------------------------------- /diskcache/url_mutex.go: -------------------------------------------------------------------------------- 1 | package diskcache 2 | 3 | import "sync" 4 | 5 | // If we're saving to the local FS, we need to globally synchronize 6 | // writes so we don't corrupt the .zip files with concurrent 7 | // writes. We also needn't bother fetching the same file concurrently, 8 | // since we'll be able to reuse it in the second caller. 9 | 10 | var ( 11 | urlMusMu sync.Mutex 12 | urlMus = map[string]*sync.Mutex{} 13 | ) 14 | 15 | func urlMu(path string) *sync.Mutex { 16 | urlMusMu.Lock() 17 | mu, ok := urlMus[path] 18 | if !ok { 19 | mu = new(sync.Mutex) 20 | urlMus[path] = mu 21 | } 22 | urlMusMu.Unlock() 23 | return mu 24 | } 25 | -------------------------------------------------------------------------------- /vfsutil/url_mutex.go: -------------------------------------------------------------------------------- 1 | package vfsutil 2 | 3 | import "sync" 4 | 5 | // If we're saving to the local FS, we need to globally synchronize 6 | // writes so we don't corrupt the .zip files with concurrent 7 | // writes. We also needn't bother fetching the same file concurrently, 8 | // since we'll be able to reuse it in the second caller. 9 | // 10 | // This URL mutex is shared among multiple VFS implementations in this 11 | // package. 12 | 13 | var ( 14 | urlMusMu sync.Mutex 15 | urlMus = map[string]*sync.Mutex{} 16 | ) 17 | 18 | func urlMu(path string) *sync.Mutex { 19 | urlMusMu.Lock() 20 | mu, ok := urlMus[path] 21 | if !ok { 22 | mu = new(sync.Mutex) 23 | urlMus[path] = mu 24 | } 25 | urlMusMu.Unlock() 26 | return mu 27 | } 28 | -------------------------------------------------------------------------------- /langserver/completion_test.go: -------------------------------------------------------------------------------- 1 | package langserver 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestParseFuncArgs(t *testing.T) { 9 | got := parseFuncArgs("func(a int, b bool, c interface{}, d string...) ([]string, error)") 10 | want := []string{"a int", "b bool", "c interface{}", "d string..."} 11 | if !reflect.DeepEqual(got, want) { 12 | t.Fatalf("Wrong function args parsed. got: %s want: %s", got, want) 13 | } 14 | } 15 | 16 | func TestGenSnippetArgs(t *testing.T) { 17 | got := genSnippetArgs([]string{"a int", "b bool", "c interface{}", "d string..."}) 18 | want := []string{"${1:a int}", "${2:b bool}", "${3:c interface{\\}}", "${4:d string...}"} 19 | if !reflect.DeepEqual(got, want) { 20 | t.Fatalf("Wrong snippet args. got: %s want: %s", got, want) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /brew/README.md: -------------------------------------------------------------------------------- 1 | ![se7en](http://www.flutecrate.com/uploads/1/0/2/0/10200817/5243300_orig.jpg) 2 | 3 | This directory contains scripts needed to generate (or update) Homebrew formula 4 | used to build, install, and test `go-langserver`. Also, you may find here the 5 | latest version of formula generated (`go-langserver.rb`) 6 | 7 | # How to update formula 8 | 9 | * Tag `go-langserver` with the new tag like `vX.Y.Z` 10 | * Run `./generate.sh X.Y.Z`, it will compute all the things needed (SHA256 11 | checksum, Go dependencies and so on) and will update `formula.rb` with the new 12 | data based on `go-langserver.rb.template` template file. 13 | * Copy resulting `go-langserver.rb` to Homebrew tap directory (ensure it has 14 | 644 permissions) and run `brew audit --strict go-langserver` 15 | 16 | Now you can submit PR to homebrew -------------------------------------------------------------------------------- /langserver/internal/godef/go/parser/parser_go18.go: -------------------------------------------------------------------------------- 1 | // +build !go1.9 2 | 3 | package parser 4 | 5 | import "go/ast" 6 | 7 | func parseTypeSpec(p *parser, doc *ast.CommentGroup, decl *ast.GenDecl, _ int) ast.Spec { 8 | if p.trace { 9 | defer un(trace(p, "TypeSpec")) 10 | } 11 | 12 | ident := p.parseIdent() 13 | // Go spec: The scope of a type identifier declared inside a function begins 14 | // at the identifier in the TypeSpec and ends at the end of the innermost 15 | // containing block. 16 | // (Global identifiers are resolved in a separate phase after parsing.) 17 | spec := &ast.TypeSpec{Doc: doc, Name: ident, Comment: p.lineComment} 18 | p.declare(spec, p.topScope, ast.Typ, ident) 19 | spec.Type = p.parseType() 20 | p.expectSemi() // call before accessing p.linecomment 21 | spec.Comment = p.lineComment 22 | 23 | return spec 24 | } 25 | -------------------------------------------------------------------------------- /pkg/lsp/structures.go: -------------------------------------------------------------------------------- 1 | package lsp 2 | 3 | import ( 4 | "github.com/sourcegraph/go-lsp" 5 | ) 6 | 7 | type Position = lsp.Position 8 | 9 | type Range = lsp.Range 10 | 11 | type Location = lsp.Location 12 | 13 | type Diagnostic = lsp.Diagnostic 14 | 15 | type DiagnosticSeverity = lsp.DiagnosticSeverity 16 | 17 | const ( 18 | Error = lsp.Error 19 | Warning = lsp.Warning 20 | Information = lsp.Information 21 | Hint = lsp.Hint 22 | ) 23 | 24 | type Command = lsp.Command 25 | 26 | type TextEdit = lsp.TextEdit 27 | 28 | type WorkspaceEdit = lsp.WorkspaceEdit 29 | 30 | type TextDocumentIdentifier = lsp.TextDocumentIdentifier 31 | 32 | type TextDocumentItem = lsp.TextDocumentItem 33 | 34 | type VersionedTextDocumentIdentifier = lsp.VersionedTextDocumentIdentifier 35 | 36 | type TextDocumentPositionParams = lsp.TextDocumentPositionParams 37 | -------------------------------------------------------------------------------- /langserver/internal/godef/go/parser/parser_go19.go: -------------------------------------------------------------------------------- 1 | // +build go1.9 2 | 3 | package parser 4 | 5 | import ( 6 | "go/ast" 7 | "go/token" 8 | ) 9 | 10 | func parseTypeSpec(p *parser, doc *ast.CommentGroup, decl *ast.GenDecl, _ int) ast.Spec { 11 | if p.trace { 12 | defer un(trace(p, "TypeSpec")) 13 | } 14 | 15 | ident := p.parseIdent() 16 | // Go spec: The scope of a type identifier declared inside a function begins 17 | // at the identifier in the TypeSpec and ends at the end of the innermost 18 | // containing block. 19 | // (Global identifiers are resolved in a separate phase after parsing.) 20 | spec := &ast.TypeSpec{Doc: doc, Name: ident, Comment: p.lineComment} 21 | p.declare(spec, p.topScope, ast.Typ, ident) 22 | if p.tok == token.ASSIGN { 23 | spec.Assign = p.pos 24 | p.next() 25 | } 26 | spec.Type = p.parseType() 27 | p.expectSemi() // call before accessing p.linecomment 28 | spec.Comment = p.lineComment 29 | 30 | return spec 31 | } 32 | -------------------------------------------------------------------------------- /langserver/internal/gocode/gbimporter/context.go: -------------------------------------------------------------------------------- 1 | package gbimporter 2 | 3 | import "go/build" 4 | 5 | // PackedContext is a copy of build.Context without the func fields. 6 | // 7 | // TODO(mdempsky): Not sure this belongs here. 8 | type PackedContext struct { 9 | GOARCH string 10 | GOOS string 11 | GOROOT string 12 | GOPATH string 13 | CgoEnabled bool 14 | UseAllFiles bool 15 | Compiler string 16 | BuildTags []string 17 | ReleaseTags []string 18 | InstallSuffix string 19 | } 20 | 21 | func PackContext(ctx *build.Context) PackedContext { 22 | return PackedContext{ 23 | GOARCH: ctx.GOARCH, 24 | GOOS: ctx.GOOS, 25 | GOROOT: ctx.GOROOT, 26 | GOPATH: ctx.GOPATH, 27 | CgoEnabled: ctx.CgoEnabled, 28 | UseAllFiles: ctx.UseAllFiles, 29 | Compiler: ctx.Compiler, 30 | BuildTags: ctx.BuildTags, 31 | ReleaseTags: ctx.ReleaseTags, 32 | InstallSuffix: ctx.InstallSuffix, 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /langserver/langserver_go19_test.go: -------------------------------------------------------------------------------- 1 | // +build go1.9 2 | 3 | package langserver 4 | 5 | func init() { 6 | serverTestCases["go1.9 type alias"] = serverTestCase{ 7 | rootURI: "file:///src/test/pkg", 8 | fs: map[string]string{ 9 | "a.go": "package p; type A struct{ a int }", 10 | "b.go": "package p; type B = A", 11 | }, 12 | cases: lspTestCases{ 13 | overrideGodefHover: map[string]string{ 14 | "a.go:1:17": "type A struct; struct{ a int }", 15 | "b.go:1:17": "type B A", 16 | "b.go:1:20": "", 17 | "b.go:1:21": "type A struct; struct{ a int }", 18 | }, 19 | wantHover: map[string]string{ 20 | "a.go:1:17": "type A struct; struct {\n a int\n}", 21 | "b.go:1:17": "type B struct; struct {\n a int\n}", 22 | "b.go:1:20": "", 23 | "b.go:1:21": "type A struct; struct {\n a int\n}", 24 | }, 25 | wantDefinition: map[string]string{ 26 | "a.go:1:17": "/src/test/pkg/a.go:1:17-1:18", 27 | "b.go:1:17": "/src/test/pkg/b.go:1:17-1:18", 28 | "b.go:1:20": "", 29 | "b.go:1:21": "/src/test/pkg/a.go:1:17-1:18", 30 | }, 31 | }, 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Sourcegraph 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /langserver/internal/gocode/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2010 nsf 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /langserver/cancel.go: -------------------------------------------------------------------------------- 1 | package langserver 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "github.com/sourcegraph/jsonrpc2" 8 | ) 9 | 10 | // cancel manages $/cancelRequest by keeping track of running commands 11 | type cancel struct { 12 | mu sync.Mutex 13 | m map[jsonrpc2.ID]func() 14 | } 15 | 16 | // WithCancel is like context.WithCancel, except you can also cancel via 17 | // calling c.Cancel with the same id. 18 | func (c *cancel) WithCancel(ctx context.Context, id jsonrpc2.ID) (context.Context, func()) { 19 | ctx, cancel := context.WithCancel(ctx) 20 | c.mu.Lock() 21 | if c.m == nil { 22 | c.m = make(map[jsonrpc2.ID]func()) 23 | } 24 | c.m[id] = cancel 25 | c.mu.Unlock() 26 | return ctx, func() { 27 | c.mu.Lock() 28 | delete(c.m, id) 29 | c.mu.Unlock() 30 | cancel() 31 | } 32 | } 33 | 34 | // Cancel will cancel the request with id. If the request has already been 35 | // cancelled or not been tracked before, Cancel is a noop. 36 | func (c *cancel) Cancel(id jsonrpc2.ID) { 37 | var cancel func() 38 | c.mu.Lock() 39 | if c.m != nil { 40 | cancel = c.m[id] 41 | delete(c.m, id) 42 | } 43 | c.mu.Unlock() 44 | if cancel != nil { 45 | cancel() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /vfsutil/github_archive_test.go: -------------------------------------------------------------------------------- 1 | package vfsutil 2 | 3 | import ( 4 | "context" 5 | "io/ioutil" 6 | "os" 7 | "testing" 8 | ) 9 | 10 | func TestGitHubRepoVFS(t *testing.T) { 11 | // Ensure fetch logic works 12 | cleanup := useEmptyArchiveCacheDir() 13 | defer cleanup() 14 | 15 | // Any public repo will work. 16 | fs, err := NewGitHubRepoVFS(context.Background(), "github.com/gorilla/schema", "0164a00ab4cd01d814d8cd5bf63fd9fcea30e23b") 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | defer fs.Close() 21 | want := map[string]string{ 22 | "/LICENSE": "...", 23 | "/README.md": "schema...", 24 | "/cache.go": "// Copyright...", 25 | "/converter.go": "// Copyright...", 26 | "/decoder.go": "// Copyright...", 27 | "/decoder_test.go": "// Copyright...", 28 | "/doc.go": "// Copyright...", 29 | "/.travis.yml": "...", 30 | } 31 | 32 | testVFS(t, fs, want) 33 | } 34 | 35 | func useEmptyArchiveCacheDir() func() { 36 | d, err := ioutil.TempDir("", "vfsutil_test") 37 | if err != nil { 38 | panic(err) 39 | } 40 | orig := ArchiveCacheDir 41 | ArchiveCacheDir = d 42 | return func() { 43 | os.RemoveAll(d) 44 | ArchiveCacheDir = orig 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /vfsutil/github_archive.go: -------------------------------------------------------------------------------- 1 | package vfsutil 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "regexp" 7 | 8 | "github.com/prometheus/client_golang/prometheus" 9 | ) 10 | 11 | // NewGitHubRepoVFS creates a new VFS backed by a GitHub downloadable 12 | // repository archive. 13 | func NewGitHubRepoVFS(ctx context.Context, repo, rev string) (*ArchiveFS, error) { 14 | if !githubRepoRx.MatchString(repo) { 15 | return nil, fmt.Errorf(`invalid GitHub repo %q: must be "github.com/user/repo"`, repo) 16 | } 17 | 18 | url := fmt.Sprintf("https://codeload.%s/zip/%s", repo, rev) 19 | return NewZipVFS(ctx, url, ghFetch.Inc, ghFetchFailed.Inc, false) 20 | } 21 | 22 | var githubRepoRx = regexp.MustCompile(`^github\.com/[\w.-]{1,100}/[\w.-]{1,100}$`) 23 | 24 | var ghFetch = prometheus.NewCounter(prometheus.CounterOpts{ 25 | Name: "golangserver_vfs_github_fetch_total", 26 | Help: "Total number of fetches by GitHubRepoVFS.", 27 | }) 28 | var ghFetchFailed = prometheus.NewCounter(prometheus.CounterOpts{ 29 | Name: "golangserver_vfs_github_fetch_failed_total", 30 | Help: "Total number of fetches by GitHubRepoVFS that failed.", 31 | }) 32 | 33 | func init() { 34 | prometheus.MustRegister(ghFetch) 35 | prometheus.MustRegister(ghFetchFailed) 36 | } 37 | -------------------------------------------------------------------------------- /langserver/rename.go: -------------------------------------------------------------------------------- 1 | package langserver 2 | 3 | import ( 4 | "context" 5 | 6 | lsp "github.com/sourcegraph/go-lsp" 7 | "github.com/sourcegraph/jsonrpc2" 8 | ) 9 | 10 | func (h *LangHandler) handleRename(ctx context.Context, conn jsonrpc2.JSONRPC2, 11 | req *jsonrpc2.Request, params lsp.RenameParams) (lsp.WorkspaceEdit, error) { 12 | rp := lsp.ReferenceParams{ 13 | TextDocumentPositionParams: lsp.TextDocumentPositionParams{ 14 | TextDocument: params.TextDocument, 15 | Position: params.Position, 16 | }, 17 | Context: lsp.ReferenceContext{ 18 | IncludeDeclaration: true, 19 | XLimit: 0, 20 | }, 21 | } 22 | 23 | references, err := h.handleTextDocumentReferences(ctx, conn, req, rp) 24 | if err != nil { 25 | return lsp.WorkspaceEdit{}, err 26 | } 27 | 28 | result := lsp.WorkspaceEdit{} 29 | if result.Changes == nil { 30 | result.Changes = make(map[string][]lsp.TextEdit) 31 | } 32 | for _, ref := range references { 33 | edit := lsp.TextEdit{ 34 | Range: ref.Range, 35 | NewText: params.NewName, 36 | } 37 | edits := result.Changes[string(ref.URI)] 38 | if edits == nil { 39 | edits = []lsp.TextEdit{} 40 | } 41 | edits = append(edits, edit) 42 | result.Changes[string(ref.URI)] = edits 43 | } 44 | return result, nil 45 | } 46 | -------------------------------------------------------------------------------- /pkg/lspext/lspext.go: -------------------------------------------------------------------------------- 1 | package lspext 2 | 3 | import ( 4 | "github.com/sourcegraph/go-lsp/lspext" 5 | ) 6 | 7 | // WorkspaceSymbolParams is the extension workspace/symbol parameter type. 8 | type WorkspaceSymbolParams = lspext.WorkspaceSymbolParams 9 | 10 | // WorkspaceReferencesParams is parameters for the `workspace/xreferences` extension 11 | // 12 | // See: https://github.com/sourcegraph/language-server-protocol/blob/master/extension-workspace-reference.md 13 | // 14 | type WorkspaceReferencesParams = lspext.WorkspaceReferencesParams 15 | 16 | // ReferenceInformation represents information about a reference to programming 17 | // constructs like variables, classes, interfaces etc. 18 | type ReferenceInformation = lspext.ReferenceInformation 19 | 20 | // SymbolDescriptor represents information about a programming construct like a 21 | // variable, class, interface, etc that has a reference to it. It is up to the 22 | // language server to define the schema of this object. 23 | // 24 | // SymbolDescriptor usually uniquely identifies a symbol, but it is not 25 | // guaranteed to do so. 26 | type SymbolDescriptor = lspext.SymbolDescriptor 27 | 28 | // SymbolLocationInformation is the response type for the `textDocument/xdefinition` extension. 29 | type SymbolLocationInformation = lspext.SymbolLocationInformation 30 | -------------------------------------------------------------------------------- /debugserver/expvar.go: -------------------------------------------------------------------------------- 1 | package debugserver 2 | 3 | import ( 4 | "expvar" 5 | "fmt" 6 | "net/http" 7 | "runtime" 8 | "runtime/debug" 9 | "time" 10 | ) 11 | 12 | // expvarHandler is copied from package expvar and exported so that it 13 | // can be mounted on any ServeMux, not just http.DefaultServeMux. 14 | func expvarHandler(w http.ResponseWriter, r *http.Request) { 15 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 16 | fmt.Fprintf(w, "{\n") 17 | first := true 18 | expvar.Do(func(kv expvar.KeyValue) { 19 | if !first { 20 | fmt.Fprintf(w, ",\n") 21 | } 22 | first = false 23 | fmt.Fprintf(w, "%q: %s", kv.Key, kv.Value) 24 | }) 25 | fmt.Fprintf(w, "\n}\n") 26 | } 27 | 28 | func gcHandler(w http.ResponseWriter, r *http.Request) { 29 | if r.Method != "POST" { 30 | http.Error(w, "only POST is supported", http.StatusMethodNotAllowed) 31 | return 32 | } 33 | 34 | t0 := time.Now() 35 | runtime.GC() 36 | fmt.Fprintf(w, "GC took %s\n", time.Since(t0)) 37 | } 38 | 39 | func freeOSMemoryHandler(w http.ResponseWriter, r *http.Request) { 40 | if r.Method != "POST" { 41 | http.Error(w, "only POST is supported", http.StatusMethodNotAllowed) 42 | return 43 | } 44 | 45 | t0 := time.Now() 46 | debug.FreeOSMemory() 47 | fmt.Fprintf(w, "FreeOSMemory took %s\n", time.Since(t0)) 48 | } 49 | -------------------------------------------------------------------------------- /langserver/xcache.go: -------------------------------------------------------------------------------- 1 | package langserver 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | 8 | "github.com/sourcegraph/go-lsp/lspext" 9 | "github.com/sourcegraph/jsonrpc2" 10 | ) 11 | 12 | // cacheGet will do a xcache/get request for key, and unmarshal the result 13 | // into v on a cache hit. If it is a cache miss, false is returned. If the 14 | // client is not a XCacheProvider, cacheGet will always return false. 15 | func (h *LangHandler) cacheGet(ctx context.Context, conn jsonrpc2.JSONRPC2, key string, v interface{}) bool { 16 | if !h.init.Capabilities.XCacheProvider { 17 | return false 18 | } 19 | 20 | var r json.RawMessage 21 | err := conn.Call(ctx, "xcache/get", lspext.CacheGetParams{Key: key}, &r) 22 | if err != nil { 23 | return false 24 | } 25 | b := []byte(r) 26 | if bytes.Equal(b, []byte("null")) { 27 | return false 28 | } 29 | err = json.Unmarshal(b, &v) 30 | return err == nil 31 | } 32 | 33 | // cacheSet will do a xcache/set request for key and a marshalled value. If 34 | // the client is not a XCacheProvider, cacheSet will do nothing. 35 | func (h *LangHandler) cacheSet(ctx context.Context, conn jsonrpc2.JSONRPC2, key string, v interface{}) { 36 | if !h.init.Capabilities.XCacheProvider { 37 | return 38 | } 39 | 40 | b, _ := json.Marshal(v) 41 | m := json.RawMessage(b) 42 | _ = conn.Notify(ctx, "xcache/set", lspext.CacheSetParams{Key: key, Value: &m}) 43 | } 44 | -------------------------------------------------------------------------------- /langserver/build_context_test.go: -------------------------------------------------------------------------------- 1 | package langserver 2 | 3 | import ( 4 | "testing" 5 | 6 | "golang.org/x/tools/go/buildutil" 7 | ) 8 | 9 | func TestContainingPackageInGOPATH(t *testing.T) { 10 | bctx := buildutil.FakeContext(map[string]map[string]string{ 11 | "p": { 12 | "a.go": "package p", 13 | "a_test.go": "package p_test", 14 | }, 15 | }) 16 | bctx.GOPATH = "/go" 17 | 18 | tests := map[string]string{ 19 | "/go/src/p/a.go": "p", 20 | "/go/src/p/a_test.go": "p_test", 21 | } 22 | for file, wantPkgName := range tests { 23 | pkg, err := ContainingPackage(bctx, file, "") 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | if pkg.Name != wantPkgName { 28 | t.Errorf("%s: got pkg name %q, want %q", file, pkg.Name, wantPkgName) 29 | } 30 | } 31 | } 32 | 33 | func TestContainingPackageOutGOPATH(t *testing.T) { 34 | bctx := buildutil.FakeContext(map[string]map[string]string{ 35 | "/home/me/p": { 36 | "a.go": "package p", 37 | "a_test.go": "package p_test", 38 | }, 39 | }) 40 | bctx.GOPATH = "/go" 41 | 42 | tests := map[string]string{ 43 | "/home/me/p/a.go": "p", 44 | "/home/me/p/a_test.go": "p_test", 45 | } 46 | for file, wantPkgName := range tests { 47 | pkg, err := ContainingPackage(bctx, file, "/home/me/p") 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | if pkg.Name != wantPkgName { 52 | t.Errorf("%s: got pkg name %q, want %q", file, pkg.Name, wantPkgName) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /langserver/internal/gocode/gocode.go: -------------------------------------------------------------------------------- 1 | package gocode 2 | 3 | import ( 4 | "go/importer" 5 | "go/types" 6 | "log" 7 | 8 | "github.com/sourcegraph/go-langserver/langserver/internal/gocode/gbimporter" 9 | "github.com/sourcegraph/go-langserver/langserver/internal/gocode/suggest" 10 | ) 11 | 12 | type AutoCompleteRequest struct { 13 | Filename string 14 | Data []byte 15 | Cursor int 16 | Context gbimporter.PackedContext 17 | Source bool 18 | Builtin bool 19 | } 20 | 21 | type AutoCompleteReply struct { 22 | Candidates []suggest.Candidate 23 | Len int 24 | } 25 | 26 | func AutoComplete(req *AutoCompleteRequest) (res *AutoCompleteReply, err error) { 27 | res = &AutoCompleteReply{} 28 | defer func() { 29 | if err := recover(); err != nil { 30 | log.Printf("gocode panic: %s\n\n", err) 31 | 32 | res.Candidates = []suggest.Candidate{ 33 | {Class: "PANIC", Name: "PANIC", Type: "PANIC"}, 34 | } 35 | } 36 | }() 37 | 38 | var underlying types.ImporterFrom 39 | if req.Source { 40 | underlying = importer.For("source", nil).(types.ImporterFrom) 41 | } else { 42 | underlying = importer.Default().(types.ImporterFrom) 43 | } 44 | cfg := suggest.Config{ 45 | Importer: gbimporter.New(&req.Context, req.Filename, underlying), 46 | Builtin: req.Builtin, 47 | } 48 | 49 | candidates, d, err := cfg.Suggest(req.Filename, req.Data, req.Cursor) 50 | if err != nil { 51 | return nil, err 52 | } 53 | res.Candidates, res.Len = candidates, d 54 | return res, nil 55 | } 56 | -------------------------------------------------------------------------------- /diskcache/cache_test.go: -------------------------------------------------------------------------------- 1 | package diskcache 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "io" 7 | "io/ioutil" 8 | "os" 9 | "testing" 10 | ) 11 | 12 | func TestOpen(t *testing.T) { 13 | dir, err := ioutil.TempDir("", "diskcache_test") 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | defer os.RemoveAll(dir) 18 | 19 | store := &Store{ 20 | Dir: dir, 21 | Component: "test", 22 | } 23 | 24 | do := func() (*File, bool) { 25 | want := "foobar" 26 | calledFetcher := false 27 | f, err := store.Open(context.Background(), "key", func(ctx context.Context) (io.ReadCloser, error) { 28 | calledFetcher = true 29 | return ioutil.NopCloser(bytes.NewReader([]byte(want))), nil 30 | }) 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | got, err := ioutil.ReadAll(f.File) 35 | if err != nil { 36 | t.Fatal(err) 37 | } 38 | f.Close() 39 | if string(got) != want { 40 | t.Fatalf("did not return fetcher output. got %q, want %q", string(got), want) 41 | } 42 | return f, !calledFetcher 43 | } 44 | 45 | // Cache should be empty 46 | _, usedCache := do() 47 | if usedCache { 48 | t.Fatal("Expected fetcher to be called on empty cache") 49 | } 50 | 51 | // Redo, now we should use the cache 52 | f, usedCache := do() 53 | if !usedCache { 54 | t.Fatal("Expected fetcher to not be called when cached") 55 | } 56 | 57 | // Evict, then we should not use the cache 58 | os.Remove(f.Path) 59 | _, usedCache = do() 60 | if usedCache { 61 | t.Fatal("Item was not properly evicted") 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /langserver/cancel_test.go: -------------------------------------------------------------------------------- 1 | package langserver 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/sourcegraph/jsonrpc2" 8 | ) 9 | 10 | func TestCancel(t *testing.T) { 11 | c := &cancel{} 12 | id1 := jsonrpc2.ID{Num: 1} 13 | id2 := jsonrpc2.ID{Num: 2} 14 | id3 := jsonrpc2.ID{Num: 3} 15 | ctx1, cancel1 := c.WithCancel(context.Background(), id1) 16 | ctx2, cancel2 := c.WithCancel(context.Background(), id2) 17 | ctx3, cancel3 := c.WithCancel(context.Background(), id3) 18 | 19 | if ctx1.Err() != nil { 20 | t.Fatal("ctx1 should not be canceled yet") 21 | } 22 | if ctx2.Err() != nil { 23 | t.Fatal("ctx2 should not be canceled yet") 24 | } 25 | if ctx3.Err() != nil { 26 | t.Fatal("ctx3 should not be canceled yet") 27 | } 28 | 29 | cancel1() 30 | if ctx1.Err() == nil { 31 | t.Fatal("ctx1 should be canceled") 32 | } 33 | if ctx2.Err() != nil { 34 | t.Fatal("ctx2 should not be canceled yet") 35 | } 36 | if ctx3.Err() != nil { 37 | t.Fatal("ctx3 should not be canceled yet") 38 | } 39 | 40 | c.Cancel(id2) 41 | if ctx2.Err() == nil { 42 | t.Fatal("ctx2 should be canceled") 43 | } 44 | if ctx3.Err() != nil { 45 | t.Fatal("ctx3 should not be canceled yet") 46 | } 47 | // we always need to call cancel from a WithCancel, even if it is 48 | // already cancelled. Calling to ensure no panic/etc 49 | cancel2() 50 | 51 | cancel3() 52 | if ctx3.Err() == nil { 53 | t.Fatal("ctx3 should be canceled") 54 | } 55 | // If we try to cancel something that has already been cancelled, it 56 | // should just be a noop. 57 | c.Cancel(id3) 58 | } 59 | -------------------------------------------------------------------------------- /langserver/internal/godef/go/types/goodarch.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "runtime" 5 | "strings" 6 | ) 7 | 8 | // Code for determining system-specific files stolen from 9 | // goinstall. We can't automatically generate goosList and 10 | // goarchList if this package is to remain goinstallable. 11 | 12 | const goosList = "darwin freebsd linux plan9 windows " 13 | const goarchList = "386 amd64 arm " 14 | 15 | // goodOSArch returns false if the filename contains a $GOOS or $GOARCH 16 | // suffix which does not match the current system. 17 | // The recognized filename formats are: 18 | // 19 | // name_$(GOOS).* 20 | // name_$(GOARCH).* 21 | // name_$(GOOS)_$(GOARCH).* 22 | // 23 | func goodOSArch(filename string) (ok bool) { 24 | if dot := strings.Index(filename, "."); dot != -1 { 25 | filename = filename[:dot] 26 | } 27 | l := strings.Split(filename, "_") 28 | n := len(l) 29 | if n == 0 { 30 | return true 31 | } 32 | if good, known := goodOS[l[n-1]]; known { 33 | return good 34 | } 35 | if good, known := goodArch[l[n-1]]; known { 36 | if !good || n < 2 { 37 | return false 38 | } 39 | good, known = goodOS[l[n-2]] 40 | return good || !known 41 | } 42 | return true 43 | } 44 | 45 | var goodOS = make(map[string]bool) 46 | var goodArch = make(map[string]bool) 47 | 48 | func init() { 49 | goodOS = make(map[string]bool) 50 | goodArch = make(map[string]bool) 51 | for _, v := range strings.Fields(goosList) { 52 | goodOS[v] = v == runtime.GOOS 53 | } 54 | for _, v := range strings.Fields(goarchList) { 55 | goodArch[v] = v == runtime.GOARCH 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /langserver/internal/godef/LICENSE: -------------------------------------------------------------------------------- 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. 27 | -------------------------------------------------------------------------------- /langserver/util/context.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "go/build" 5 | "io" 6 | "os" 7 | "path" 8 | "path/filepath" 9 | 10 | "github.com/sourcegraph/ctxvfs" 11 | 12 | "golang.org/x/net/context" 13 | ) 14 | 15 | func PrepareContext(bctx *build.Context, ctx context.Context, fs ctxvfs.FileSystem) { 16 | // HACK: in the all Context's methods below we are trying to convert path to virtual one (/foo/bar/..) 17 | // because some code may pass OS-specific arguments. 18 | // See golang.org/x/tools/go/buildutil/allpackages.go which uses `filepath` for example 19 | 20 | bctx.OpenFile = func(path string) (io.ReadCloser, error) { 21 | path = filepath.ToSlash(path) 22 | return fs.Open(ctx, path) 23 | } 24 | bctx.IsDir = func(path string) bool { 25 | path = filepath.ToSlash(path) 26 | fi, err := fs.Stat(ctx, path) 27 | return err == nil && fi.Mode().IsDir() 28 | } 29 | bctx.HasSubdir = func(root, dir string) (rel string, ok bool) { 30 | if !bctx.IsDir(dir) { 31 | return "", false 32 | } 33 | if !PathHasPrefix(dir, root) { 34 | return "", false 35 | } 36 | return PathTrimPrefix(dir, root), true 37 | } 38 | bctx.ReadDir = func(path string) ([]os.FileInfo, error) { 39 | path = filepath.ToSlash(path) 40 | return fs.ReadDir(ctx, path) 41 | } 42 | bctx.IsAbsPath = func(path string) bool { 43 | path = filepath.ToSlash(path) 44 | return IsAbs(path) 45 | } 46 | bctx.JoinPath = func(elem ...string) string { 47 | // convert all backslashes to slashes to avoid 48 | // weird paths like C:\mygopath\/src/github.com/... 49 | for i, el := range elem { 50 | elem[i] = filepath.ToSlash(el) 51 | } 52 | return path.Join(elem...) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /gosrc/NOTICE: -------------------------------------------------------------------------------- 1 | Portions of code are adapted from github.com/golang/gddo: 2 | 3 | Copyright (c) 2013 The Go Authors. All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are 7 | met: 8 | 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following disclaimer 13 | in the documentation and/or other materials provided with the 14 | distribution. 15 | * Neither the name of Google Inc. nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | Contact GitHub API Training Shop Blog About 31 | -------------------------------------------------------------------------------- /langserver/internal/godef/go/types/objpos.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "go/token" 5 | 6 | "go/ast" 7 | ) 8 | 9 | func declPos(name string, decl ast.Node) token.Pos { 10 | switch d := decl.(type) { 11 | case nil: 12 | return token.NoPos 13 | case *ast.AssignStmt: 14 | for _, n := range d.Lhs { 15 | if n, ok := n.(*ast.Ident); ok && n.Name == name { 16 | return n.Pos() 17 | } 18 | } 19 | case *ast.Field: 20 | for _, n := range d.Names { 21 | if n.Name == name { 22 | return n.Pos() 23 | } 24 | } 25 | case *ast.ValueSpec: 26 | for _, n := range d.Names { 27 | if n.Name == name { 28 | return n.Pos() 29 | } 30 | } 31 | case *ast.TypeSpec: 32 | if d.Name.Name == name { 33 | return d.Name.Pos() 34 | } 35 | case *ast.FuncDecl: 36 | if d.Name.Name == name { 37 | return d.Name.Pos() 38 | } 39 | case *ast.LabeledStmt: 40 | if d.Label.Name == name { 41 | return d.Label.Pos() 42 | } 43 | case *ast.GenDecl: 44 | for _, spec := range d.Specs { 45 | if pos := declPos(name, spec); pos.IsValid() { 46 | return pos 47 | } 48 | } 49 | case *ast.TypeSwitchStmt: 50 | return declPos(name, d.Assign) 51 | } 52 | return token.NoPos 53 | } 54 | 55 | // DeclPos computes the source position of the declaration of an object name. 56 | // The result may be an invalid position if it cannot be computed 57 | // (obj.Decl may be nil or not correct). 58 | // This should be called ast.Object.Pos. 59 | func DeclPos(obj *ast.Object) token.Pos { 60 | decl, _ := obj.Decl.(ast.Node) 61 | if decl == nil { 62 | return token.NoPos 63 | } 64 | pos := declPos(obj.Name, decl) 65 | if !pos.IsValid() { 66 | pos = decl.Pos() 67 | } 68 | return pos 69 | } 70 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Portions of code are adapted from go/types, golang.org/x/tools, and github.com/golang/gddo: 2 | 3 | Copyright (c) 2013 The Go Authors. All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are 7 | met: 8 | 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following disclaimer 13 | in the documentation and/or other materials provided with the 14 | distribution. 15 | * Neither the name of Google Inc. nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | Contact GitHub API Training Shop Blog About 31 | -------------------------------------------------------------------------------- /langserver/format_test.go: -------------------------------------------------------------------------------- 1 | package langserver 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/sourcegraph/go-lsp" 8 | ) 9 | 10 | type computeTextEditsTestCase struct { 11 | unformatted string 12 | formatted string 13 | expected []lsp.TextEdit 14 | } 15 | 16 | var computeTextEditsTestCases = map[string]computeTextEditsTestCase{ 17 | "one edit": computeTextEditsTestCase{ 18 | unformatted: "package p\n\n func A() {}\n", 19 | formatted: "package p\n\nfunc A() {}\n", 20 | expected: []lsp.TextEdit{ 21 | toTextEdit(toRange(2, 0, 3, 0), "func A() {}\n"), 22 | }, 23 | }, 24 | "multiple edits": computeTextEditsTestCase{ 25 | unformatted: "package p\n\n func A() {}\n\n func B() {}\n", 26 | formatted: "package p\n\nfunc A() {}\n\nfunc B() {}\n", 27 | expected: []lsp.TextEdit{ 28 | toTextEdit(toRange(2, 0, 3, 0), "func A() {}\n"), 29 | toTextEdit(toRange(4, 0, 5, 0), "func B() {}\n"), 30 | }, 31 | }, 32 | "whole text": computeTextEditsTestCase{ 33 | unformatted: "package p; func A() {}", 34 | formatted: "package langserver\n\nfunc A() {}", 35 | expected: []lsp.TextEdit{ 36 | toTextEdit(toRange(0, 0, 1, 0), "package langserver\n\nfunc A() {}\n"), // TODO: why a new line? 37 | }, 38 | }, 39 | } 40 | 41 | func TestComputeEdits(t *testing.T) { 42 | for label, test := range computeTextEditsTestCases { 43 | t.Run(label, func(t *testing.T) { 44 | edits := ComputeTextEdits(test.unformatted, test.formatted) 45 | if !reflect.DeepEqual(edits, test.expected) { 46 | t.Errorf("Expected %q but got %q", test.expected, edits) 47 | } 48 | }) 49 | } 50 | } 51 | 52 | func toTextEdit(r lsp.Range, t string) lsp.TextEdit { 53 | return lsp.TextEdit{Range: r, NewText: t} 54 | } 55 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sourcegraph/go-langserver 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/HdrHistogram/hdrhistogram-go v1.1.0 // indirect 7 | github.com/die-net/lrucache v0.0.0-20190707192454-883874fe3947 8 | github.com/fhs/go-netrc v1.0.0 9 | github.com/gorilla/websocket v1.4.1 10 | github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 11 | github.com/hashicorp/go-multierror v1.1.0 12 | github.com/hashicorp/golang-lru v0.5.4 13 | github.com/keegancsmith/tmpfriend v0.0.0-20180423180255-86e88902a513 14 | github.com/lightstep/lightstep-tracer-go v0.20.0 15 | github.com/mattn/go-colorable v0.1.6 // indirect 16 | github.com/neelance/parallel v0.0.0-20160708114440-4de9ce63d14c 17 | github.com/opentracing/basictracer-go v1.0.0 18 | github.com/opentracing/opentracing-go v1.2.0 19 | github.com/pelletier/go-toml v1.6.0 20 | github.com/pkg/errors v0.9.1 21 | github.com/pmezard/go-difflib v1.0.0 22 | github.com/prometheus/client_golang v1.11.0 23 | github.com/slimsag/godocmd v0.0.0-20161025000126-a1005ad29fe3 24 | github.com/sourcegraph/ctxvfs v0.0.0-20180418081416-2b65f1b1ea81 25 | github.com/sourcegraph/go-lsp v0.0.0-20200117082640-b19bb38222e2 26 | github.com/sourcegraph/jsonrpc2 v0.0.0-20191222043438-96c4efab7ee2 27 | github.com/sourcegraph/jsonx v0.0.0-20190114210550-ba8cb36a8614 28 | github.com/uber/jaeger-client-go v2.29.1+incompatible 29 | github.com/uber/jaeger-lib v2.4.1+incompatible 30 | go.uber.org/atomic v1.9.0 // indirect 31 | go4.org v0.0.0-20200312051459-7028f7b4a332 32 | golang.org/x/net v0.0.0-20200625001655-4c5254603344 33 | golang.org/x/tools v0.0.0-20200401192744-099440627f01 34 | gopkg.in/inconshreveable/log15.v2 v2.0.0-20200109203555-b30bc20e4fd1 35 | gopkg.in/yaml.v2 v2.3.0 36 | ) 37 | -------------------------------------------------------------------------------- /langserver/partial.go: -------------------------------------------------------------------------------- 1 | package langserver 2 | 3 | import "github.com/sourcegraph/go-lsp" 4 | 5 | // This file contains types and functions related to $/partialResult streaming 6 | 7 | // RewriteURIer is a type that implements RewriteURI. The typical use of 8 | // RewriteURI is a build server which translates workspace URIs into URIs for 9 | // other systems to consume. 10 | type RewriteURIer interface { 11 | // RewriteURI will update all URIs in the type using the rewrite 12 | // function. 13 | RewriteURI(rewrite func(lsp.DocumentURI) lsp.DocumentURI) 14 | } 15 | 16 | // referenceAddOp is a JSON Patch operation used by 17 | // textDocument/references. The only other patch operation is to create the 18 | // empty location list. 19 | type referenceAddOp struct { 20 | // OP should always be "add" 21 | OP string `json:"op"` 22 | Path string `json:"path"` 23 | Value lsp.Location `json:"value"` 24 | } 25 | 26 | type referencePatch []referenceAddOp 27 | 28 | func (p referencePatch) RewriteURI(rewrite func(lsp.DocumentURI) lsp.DocumentURI) { 29 | for i := range p { 30 | p[i].Value.URI = rewrite(p[i].Value.URI) 31 | } 32 | } 33 | 34 | // xreferenceAddOp is a JSON Patch operation used by 35 | // workspace/xreferences. The only other patch operation is to create the 36 | // empty location list. 37 | type xreferenceAddOp struct { 38 | // OP should always be "add" 39 | OP string `json:"op"` 40 | Path string `json:"path"` 41 | Value referenceInformation `json:"value"` 42 | } 43 | 44 | type xreferencePatch []xreferenceAddOp 45 | 46 | func (p xreferencePatch) RewriteURI(rewrite func(lsp.DocumentURI) lsp.DocumentURI) { 47 | for i := range p { 48 | p[i].Value.Reference.URI = rewrite(p[i].Value.Reference.URI) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /vfsutil/vfs_test.go: -------------------------------------------------------------------------------- 1 | package vfsutil 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "sort" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/sourcegraph/ctxvfs" 11 | ) 12 | 13 | func testVFS(t *testing.T, fs ctxvfs.FileSystem, want map[string]string) { 14 | tree, err := ctxvfs.ReadAllFiles(context.Background(), fs, "", nil) 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | if len(tree) != len(want) { 19 | t.Errorf("got %d files, want %d files", len(tree), len(want)) 20 | } 21 | for wantFile, wantContents := range want { 22 | contentsBytes, ok := tree[wantFile] 23 | if !ok { 24 | t.Errorf("missing file %s", wantFile) 25 | continue 26 | } 27 | contents := string(contentsBytes) 28 | 29 | if strings.HasSuffix(wantContents, "...") { 30 | // Allow specifying expected contents with "..." at the 31 | // end for ease of test creation. 32 | if len(contents) >= len(wantContents)+3 { 33 | contents = contents[:len(wantContents)-3] + "..." 34 | } 35 | } 36 | if contents != wantContents { 37 | t.Errorf("%s: got contents %q, want %q", wantFile, contents, wantContents) 38 | } 39 | } 40 | for file := range tree { 41 | if _, ok := want[file]; !ok { 42 | t.Errorf("extra file %s", file) 43 | } 44 | } 45 | if fsLister, ok := fs.(interface { 46 | ListAllFiles(context.Context) ([]string, error) 47 | }); ok { 48 | got, err := fsLister.ListAllFiles(context.Background()) 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | wantList := make([]string, 0, len(want)) 53 | for file := range want { 54 | wantList = append(wantList, strings.TrimPrefix(file, "/")) 55 | } 56 | sort.Strings(got) 57 | sort.Strings(wantList) 58 | if !reflect.DeepEqual(got, wantList) { 59 | t.Fatalf("ListAllFiles does not match want:\ngot: %v\nwant: %v", got, wantList) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /langserver/handler_common.go: -------------------------------------------------------------------------------- 1 | package langserver 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "sync" 8 | 9 | opentracing "github.com/opentracing/opentracing-go" 10 | 11 | "github.com/sourcegraph/go-langserver/langserver/util" 12 | "github.com/sourcegraph/go-lsp" 13 | ) 14 | 15 | // HandlerCommon contains functionality that both the build and lang 16 | // handlers need. They do NOT share the memory of this HandlerCommon 17 | // struct; it is just common functionality. (Unlike HandlerCommon, 18 | // HandlerShared is shared in-memory.) 19 | type HandlerCommon struct { 20 | mu sync.Mutex // guards all fields 21 | RootFSPath string // root path of the project's files in the (possibly virtual) file system, without the "file://" prefix (typically /src/github.com/foo/bar) 22 | shutdown bool 23 | tracer opentracing.Tracer 24 | } 25 | 26 | func (h *HandlerCommon) Reset(rootURI lsp.DocumentURI) error { 27 | h.mu.Lock() 28 | defer h.mu.Unlock() 29 | if h.shutdown { 30 | return errors.New("unable to reset a server that is shutting down") 31 | } 32 | if !util.IsURI(rootURI) { 33 | return fmt.Errorf("invalid root path %q: must be file:// URI", rootURI) 34 | } 35 | h.RootFSPath = util.UriToPath(rootURI) // retain leading slash 36 | return nil 37 | } 38 | 39 | // ShutDown marks this server as being shut down and causes all future calls to checkReady to return an error. 40 | func (h *HandlerCommon) ShutDown() { 41 | h.mu.Lock() 42 | if h.shutdown { 43 | log.Printf("Warning: server received a shutdown request after it was already shut down.") 44 | } 45 | h.shutdown = true 46 | h.mu.Unlock() 47 | } 48 | 49 | // CheckReady returns an error if the handler has been shut 50 | // down. 51 | func (h *HandlerCommon) CheckReady() error { 52 | h.mu.Lock() 53 | defer h.mu.Unlock() 54 | if h.shutdown { 55 | return errors.New("server is shutting down") 56 | } 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /langserver/internal/godef/go/parser/universe.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import "go/ast" 4 | 5 | var Universe = ast.NewScope(nil) 6 | 7 | func declObj(kind ast.ObjKind, name string) { 8 | // don't use Insert because it forbids adding to Universe 9 | Universe.Objects[name] = ast.NewObj(kind, name) 10 | } 11 | 12 | func init() { 13 | declObj(ast.Typ, "bool") 14 | 15 | declObj(ast.Typ, "complex64") 16 | declObj(ast.Typ, "complex128") 17 | 18 | declObj(ast.Typ, "int") 19 | declObj(ast.Typ, "int8") 20 | declObj(ast.Typ, "int16") 21 | declObj(ast.Typ, "int32") 22 | declObj(ast.Typ, "int64") 23 | 24 | declObj(ast.Typ, "uint") 25 | declObj(ast.Typ, "uintptr") 26 | declObj(ast.Typ, "uint8") 27 | declObj(ast.Typ, "uint16") 28 | declObj(ast.Typ, "uint32") 29 | declObj(ast.Typ, "uint64") 30 | 31 | declObj(ast.Typ, "float") 32 | declObj(ast.Typ, "float32") 33 | declObj(ast.Typ, "float64") 34 | 35 | declObj(ast.Typ, "string") 36 | declObj(ast.Typ, "error") 37 | 38 | // predeclared constants 39 | // TODO(gri) provide constant value 40 | declObj(ast.Con, "false") 41 | declObj(ast.Con, "true") 42 | declObj(ast.Con, "iota") 43 | declObj(ast.Con, "nil") 44 | 45 | // predeclared functions 46 | // TODO(gri) provide "type" 47 | declObj(ast.Fun, "append") 48 | declObj(ast.Fun, "cap") 49 | declObj(ast.Fun, "close") 50 | declObj(ast.Fun, "complex") 51 | declObj(ast.Fun, "copy") 52 | declObj(ast.Fun, "delete") 53 | declObj(ast.Fun, "imag") 54 | declObj(ast.Fun, "len") 55 | declObj(ast.Fun, "make") 56 | declObj(ast.Fun, "new") 57 | declObj(ast.Fun, "panic") 58 | declObj(ast.Fun, "panicln") 59 | declObj(ast.Fun, "print") 60 | declObj(ast.Fun, "println") 61 | declObj(ast.Fun, "real") 62 | declObj(ast.Fun, "recover") 63 | 64 | // byte is an alias for uint8, so cheat 65 | // by storing the same object for both name 66 | // entries 67 | Universe.Objects["byte"] = Universe.Objects["uint8"] 68 | 69 | // The same applies to rune. 70 | Universe.Objects["rune"] = Universe.Objects["uint32"] 71 | } 72 | -------------------------------------------------------------------------------- /brew/generate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | die () { 4 | echo >&2 "$@" 5 | exit 1 6 | } 7 | 8 | if [ "$1" = "" ] 9 | then 10 | die "Usage: $0 " 11 | fi 12 | 13 | REPO_OWNER=sourcegraph 14 | REPO_NAME=go-langserver 15 | WORK_DIR=`mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir'` 16 | GOPATH=$WORK_DIR 17 | VERSION=$1 18 | TEMPLATE=`cat $PWD/go-langserver.rb.template` 19 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 20 | OUT_FILE=$DIR/go-langserver.rb 21 | 22 | echo "Calculating SHA256 sum" 23 | SHA256SUM=`curl -sL https://github.com/$REPO_OWNER/$REPO_NAME/archive/v$VERSION.tar.gz | sha256sum | awk '{print $1}' || die "Failed to calculate SHA256 checksum"` 24 | echo "Downloading source code" 25 | mkdir -p $WORK_DIR/src/github.com/$REPO_OWNER/$REPO_NAME 26 | git clone -q https://github.com/$REPO_OWNER/$REPO_NAME $WORK_DIR/src/github.com/$REPO_OWNER/$REPO_NAME || (rm -rf $WORK_DIR && die "Failed to clone repository") 27 | cd $WORK_DIR/src/github.com/$REPO_OWNER/$REPO_NAME 28 | git checkout -q v$VERSION || (rm -rf $WORK_DIR && die "Failed to switch repository to specific version") 29 | echo "Fetching dependencies" 30 | go get -d ./... || (rm -rf $WORK_DIR && die "Failed to fetch Go dependencies") 31 | echo "Installing homebrew-go-resources" 32 | go get github.com/samertm/homebrew-go-resources || (rm -rf $WORK_DIR && die "Failed to fetch homebrew-go-resources tool") 33 | echo "Processing dependencies" 34 | GO_RESOURCES=`$GOPATH/bin/homebrew-go-resources github.com/$REPO_OWNER/$REPO_NAME/... || (rm -rf $WORK_DIR && die "Failed to generate list of Go dependencies")` 35 | # Removing leading and trailing spaces to make `brew audit --strict` happy 36 | GO_RESOURCES="${GO_RESOURCES#"${GO_RESOURCES%%[![:space:]]*}"}" 37 | GO_RESOURCES="${GO_RESOURCES#"${GO_RESOURCES##*[![:space:]]}"}" 38 | TEMPLATE=${TEMPLATE//#\{GO_RESOURCES\}/${GO_RESOURCES}} 39 | TEMPLATE=${TEMPLATE//#\{VERSION\}/${VERSION}} 40 | TEMPLATE=${TEMPLATE//#\{REPO_OWNER\}/${REPO_OWNER}} 41 | TEMPLATE=${TEMPLATE//#\{REPO_NAME\}/${REPO_NAME}} 42 | TEMPLATE=${TEMPLATE//#\{SHA256SUM\}/${SHA256SUM}} 43 | echo "$TEMPLATE" > $OUT_FILE 44 | rm -rf $WORK_DIR 45 | 46 | -------------------------------------------------------------------------------- /vfsutil/cache.go: -------------------------------------------------------------------------------- 1 | package vfsutil 2 | 3 | import ( 4 | "archive/zip" 5 | "context" 6 | "io" 7 | "os" 8 | 9 | "github.com/prometheus/client_golang/prometheus" 10 | 11 | "github.com/sourcegraph/go-langserver/diskcache" 12 | ) 13 | 14 | // ArchiveCacheDir is the location on disk that archives are cached. It is 15 | // configurable so that in production we can point it into CACHE_DIR. 16 | var ArchiveCacheDir = "/tmp/go-langserver-archive-cache" 17 | 18 | // MaxCacheSizeBytes is the maximum size of the cache directory after evicting 19 | // entries. Defaults to 50 GB. 20 | var MaxCacheSizeBytes = int64(50 * 1024 * 1024 * 1024) 21 | 22 | // Evicter implements Evict 23 | type Evicter interface { 24 | // Evict evicts an item from a cache. 25 | Evict() 26 | } 27 | 28 | type cachedFile struct { 29 | // File is an open FD to the fetched data 30 | File *os.File 31 | 32 | // path is the disk path for File 33 | path string 34 | } 35 | 36 | // Evict will remove the file from the cache. It does not close File. It also 37 | // does not protect against other open readers or concurrent fetches. 38 | func (f *cachedFile) Evict() { 39 | // Best-effort. Ignore error 40 | _ = os.Remove(f.path) 41 | cachedFileEvict.Inc() 42 | } 43 | 44 | // cachedFetch will open a file from the local cache with key. If missing, 45 | // fetcher will fill the cache first. cachedFetch also performs 46 | // single-flighting. 47 | func cachedFetch(ctx context.Context, key string, s *diskcache.Store, fetcher func(context.Context) (io.ReadCloser, error)) (ff *cachedFile, err error) { 48 | f, err := s.Open(ctx, key, fetcher) 49 | if err != nil { 50 | return nil, err 51 | } 52 | return &cachedFile{ 53 | File: f.File, 54 | path: f.Path, 55 | }, nil 56 | } 57 | 58 | func zipNewFileReader(f *os.File) (*zip.Reader, error) { 59 | fi, err := f.Stat() 60 | if err != nil { 61 | return nil, err 62 | } 63 | return zip.NewReader(f, fi.Size()) 64 | } 65 | 66 | var cachedFileEvict = prometheus.NewCounter(prometheus.CounterOpts{ 67 | Name: "golangserver_vfs_cached_file_evict", 68 | Help: "Total number of evictions to cachedFetch archives.", 69 | }) 70 | 71 | func init() { 72 | prometheus.MustRegister(cachedFileEvict) 73 | } 74 | -------------------------------------------------------------------------------- /tracer/tracer.go: -------------------------------------------------------------------------------- 1 | package tracer 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "path/filepath" 7 | "strconv" 8 | 9 | lightstep "github.com/lightstep/lightstep-tracer-go" 10 | opentracing "github.com/opentracing/opentracing-go" 11 | jaeger "github.com/uber/jaeger-client-go" 12 | jaegercfg "github.com/uber/jaeger-client-go/config" 13 | jaegerlog "github.com/uber/jaeger-client-go/log" 14 | jaegermetrics "github.com/uber/jaeger-lib/metrics" 15 | ) 16 | 17 | var ( 18 | lightstepAccessToken = os.Getenv("LIGHTSTEP_ACCESS_TOKEN") 19 | lightstepProject = os.Getenv("LIGHTSTEP_PROJECT") 20 | lightstepIncludeSensitive, _ = strconv.ParseBool(os.Getenv("LIGHTSTEP_INCLUDE_SENSITIVE")) 21 | useJaeger, _ = strconv.ParseBool(os.Getenv("USE_JAEGER")) 22 | ) 23 | 24 | func Init() { 25 | serviceName := filepath.Base(os.Args[0]) 26 | if useJaeger { 27 | log.Println("Distributed tracing enabled", "tracer", "jaeger") 28 | cfg := jaegercfg.Configuration{ 29 | Sampler: &jaegercfg.SamplerConfig{ 30 | Type: jaeger.SamplerTypeConst, 31 | Param: 1, 32 | }, 33 | } 34 | _, err := cfg.InitGlobalTracer( 35 | serviceName, 36 | jaegercfg.Logger(jaegerlog.StdLogger), 37 | jaegercfg.Metrics(jaegermetrics.NullFactory), 38 | ) 39 | if err != nil { 40 | log.Printf("Could not initialize jaeger tracer: %s", err.Error()) 41 | return 42 | } 43 | return 44 | } 45 | 46 | if lightstepAccessToken != "" { 47 | log.Println("Distributed tracing enabled", "tracer", "Lightstep") 48 | opentracing.InitGlobalTracer(lightstep.NewTracer(lightstep.Options{ 49 | AccessToken: lightstepAccessToken, 50 | UseGRPC: true, 51 | Tags: opentracing.Tags{ 52 | lightstep.ComponentNameKey: serviceName, 53 | }, 54 | DropSpanLogs: !lightstepIncludeSensitive, 55 | })) 56 | 57 | // Ignore warnings from the tracer about SetTag calls with unrecognized value types. The 58 | // github.com/lightstep/lightstep-tracer-go package calls fmt.Sprintf("%#v", ...) on them, which is fine. 59 | defaultHandler := lightstep.NewEventLogOneError() 60 | lightstep.SetGlobalEventHandler(func(e lightstep.Event) { 61 | if _, ok := e.(lightstep.EventUnsupportedValue); ok { 62 | // ignore 63 | } else { 64 | defaultHandler(e) 65 | } 66 | }) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /langserver/internal/gocode/suggest/formatters.go: -------------------------------------------------------------------------------- 1 | package suggest 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | type Formatter func(w io.Writer, candidates []Candidate, num int) 10 | 11 | var Formatters = map[string]Formatter{ 12 | "csv": csvFormat, 13 | "csv-with-package": csvFormat, 14 | "emacs": emacsFormat, 15 | "godit": goditFormat, 16 | "json": jsonFormat, 17 | "nice": NiceFormat, 18 | "vim": vimFormat, 19 | } 20 | 21 | func NiceFormat(w io.Writer, candidates []Candidate, num int) { 22 | if candidates == nil { 23 | fmt.Fprintf(w, "Nothing to complete.\n") 24 | return 25 | } 26 | 27 | fmt.Fprintf(w, "Found %d candidates:\n", len(candidates)) 28 | for _, c := range candidates { 29 | fmt.Fprintf(w, " %s\n", c.String()) 30 | } 31 | } 32 | 33 | func vimFormat(w io.Writer, candidates []Candidate, num int) { 34 | if candidates == nil { 35 | fmt.Fprint(w, "[0, []]") 36 | return 37 | } 38 | 39 | fmt.Fprintf(w, "[%d, [", num) 40 | for i, c := range candidates { 41 | if i != 0 { 42 | fmt.Fprintf(w, ", ") 43 | } 44 | 45 | word := c.Suggestion() 46 | abbr := c.String() 47 | fmt.Fprintf(w, "{'word': '%s', 'abbr': '%s', 'info': '%s'}", word, abbr, abbr) 48 | } 49 | fmt.Fprintf(w, "]]") 50 | } 51 | 52 | func goditFormat(w io.Writer, candidates []Candidate, num int) { 53 | fmt.Fprintf(w, "%d,,%d\n", num, len(candidates)) 54 | for _, c := range candidates { 55 | fmt.Fprintf(w, "%s,,%s\n", c.String(), c.Suggestion()) 56 | } 57 | } 58 | 59 | func emacsFormat(w io.Writer, candidates []Candidate, num int) { 60 | for _, c := range candidates { 61 | var hint string 62 | switch { 63 | case c.Class == "func": 64 | hint = c.Type 65 | case c.Type == "": 66 | hint = c.Class 67 | default: 68 | hint = c.Class + " " + c.Type 69 | } 70 | fmt.Fprintf(w, "%s,,%s\n", c.Name, hint) 71 | } 72 | } 73 | 74 | func csvFormat(w io.Writer, candidates []Candidate, num int) { 75 | for _, c := range candidates { 76 | fmt.Fprintf(w, "%s,,%s,,%s,,%s\n", c.Class, c.Name, c.Type, c.PkgPath) 77 | } 78 | } 79 | 80 | func jsonFormat(w io.Writer, candidates []Candidate, num int) { 81 | var x []interface{} 82 | if candidates != nil { 83 | x = []interface{}{num, candidates} 84 | } 85 | json.NewEncoder(w).Encode(x) 86 | } 87 | -------------------------------------------------------------------------------- /langserver/vendor/go/doc/synopsis.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 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 | package doc 6 | 7 | import ( 8 | "strings" 9 | "unicode" 10 | ) 11 | 12 | // firstSentenceLen returns the length of the first sentence in s. 13 | // The sentence ends after the first period followed by space and 14 | // not preceded by exactly one uppercase letter. 15 | // 16 | func firstSentenceLen(s string) int { 17 | var ppp, pp, p rune 18 | for i, q := range s { 19 | if q == '\n' || q == '\r' || q == '\t' { 20 | q = ' ' 21 | } 22 | if q == ' ' && p == '.' && (!unicode.IsUpper(pp) || unicode.IsUpper(ppp)) { 23 | return i 24 | } 25 | if p == '。' || p == '.' { 26 | return i 27 | } 28 | ppp, pp, p = pp, p, q 29 | } 30 | return len(s) 31 | } 32 | 33 | const ( 34 | keepNL = 1 << iota 35 | ) 36 | 37 | // clean replaces each sequence of space, \n, \r, or \t characters 38 | // with a single space and removes any trailing and leading spaces. 39 | // If the keepNL flag is set, newline characters are passed through 40 | // instead of being change to spaces. 41 | func clean(s string, flags int) string { 42 | var b []byte 43 | p := byte(' ') 44 | for i := 0; i < len(s); i++ { 45 | q := s[i] 46 | if (flags&keepNL) == 0 && q == '\n' || q == '\r' || q == '\t' { 47 | q = ' ' 48 | } 49 | if q != ' ' || p != ' ' { 50 | b = append(b, q) 51 | p = q 52 | } 53 | } 54 | // remove trailing blank, if any 55 | if n := len(b); n > 0 && p == ' ' { 56 | b = b[0 : n-1] 57 | } 58 | return string(b) 59 | } 60 | 61 | // Synopsis returns a cleaned version of the first sentence in s. 62 | // That sentence ends after the first period followed by space and 63 | // not preceded by exactly one uppercase letter. The result string 64 | // has no \n, \r, or \t characters and uses only single spaces between 65 | // words. If s starts with any of the IllegalPrefixes, the result 66 | // is the empty string. 67 | // 68 | func Synopsis(s string) string { 69 | s = clean(s[0:firstSentenceLen(s)], 0) 70 | for _, prefix := range IllegalPrefixes { 71 | if strings.HasPrefix(strings.ToLower(s), prefix) { 72 | return "" 73 | } 74 | } 75 | return s 76 | } 77 | 78 | var IllegalPrefixes = []string{ 79 | "copyright", 80 | "all rights", 81 | "author", 82 | } 83 | -------------------------------------------------------------------------------- /langserver/lsp.go: -------------------------------------------------------------------------------- 1 | package langserver 2 | 3 | import ( 4 | "github.com/sourcegraph/go-lsp" 5 | "github.com/sourcegraph/go-lsp/lspext" 6 | ) 7 | 8 | // This file contains lspext but redefined to suit go-langserver 9 | // needs. Everything in here should be wire compatible. 10 | 11 | // symbolDescriptor is the exact fields go-langserver uses for 12 | // lspext.SymbolDescriptor. It should have the same JSON wire format. We make 13 | // it a struct for both type safety as well as better memory efficiency. 14 | type symbolDescriptor struct { 15 | Package string `json:"package"` 16 | PackageName string `json:"packageName"` 17 | Recv string `json:"recv"` 18 | Name string `json:"name"` 19 | ID string `json:"id"` 20 | Vendor bool `json:"vendor"` 21 | } 22 | 23 | // Contains ensures that b is a subset of our symbolDescriptor 24 | func (a *symbolDescriptor) Contains(b lspext.SymbolDescriptor) bool { 25 | for k, v := range b { 26 | switch k { 27 | case "package": 28 | if s, ok := v.(string); !ok || s != a.Package { 29 | return false 30 | } 31 | case "packageName": 32 | if s, ok := v.(string); !ok || s != a.PackageName { 33 | return false 34 | } 35 | case "recv": 36 | if s, ok := v.(string); !ok || s != a.Recv { 37 | return false 38 | } 39 | case "name": 40 | if s, ok := v.(string); !ok || s != a.Name { 41 | return false 42 | } 43 | case "id": 44 | if s, ok := v.(string); !ok || s != a.ID { 45 | return false 46 | } 47 | case "vendor": 48 | if s, ok := v.(bool); !ok || s != a.Vendor { 49 | return false 50 | } 51 | default: 52 | return false 53 | } 54 | } 55 | return true 56 | } 57 | 58 | // symbolLocationInformation is lspext.SymbolLocationInformation, but using 59 | // our custom symbolDescriptor 60 | type symbolLocationInformation struct { 61 | // A concrete location at which the definition is located, if any. 62 | Location lsp.Location `json:"location,omitempty"` 63 | // Metadata about the definition. 64 | Symbol *symbolDescriptor `json:"symbol"` 65 | // the location of a type declaration, if one is available 66 | TypeLocation lsp.Location `json:"-"` 67 | } 68 | 69 | // referenceInformation is lspext.ReferenceInformation using our custom symbolDescriptor 70 | type referenceInformation struct { 71 | // Reference is the location in the workspace where the `symbol` has been 72 | // referenced. 73 | Reference lsp.Location `json:"reference"` 74 | 75 | // Symbol is metadata information describing the symbol being referenced. 76 | Symbol *symbolDescriptor `json:"symbol"` 77 | } 78 | -------------------------------------------------------------------------------- /brew/go-langserver.rb.template: -------------------------------------------------------------------------------- 1 | require "language/go" 2 | 3 | class GoLangserver < Formula 4 | desc "Go language LSP server" 5 | homepage "https://github.com/#{REPO_OWNER}/#{REPO_NAME}" 6 | url "https://github.com/#{REPO_OWNER}/#{REPO_NAME}/archive/v#{VERSION}.tar.gz" 7 | sha256 "#{SHA256SUM}" 8 | 9 | head "https://github.com/#{REPO_OWNER}/#{REPO_NAME}.git" 10 | 11 | depends_on "go" => :build 12 | 13 | #{GO_RESOURCES} 14 | 15 | def install 16 | mkdir_p buildpath/"src/github.com/sourcegraph" 17 | ln_s buildpath, buildpath/"src/github.com/sourcegraph/go-langserver" 18 | ENV["GOPATH"] = buildpath.to_s 19 | Language::Go.stage_deps resources, buildpath/"src" 20 | system "go", "build", "langserver/cmd/langserver-go/langserver-go.go" 21 | bin.install "langserver-go" 22 | end 23 | 24 | test do 25 | # Set up fake GOROOT and create test Go project 26 | mkdir_p testpath/"gopath/src/test" 27 | mkdir_p testpath/"goroot" 28 | ENV["GOPATH"] = "#{testpath}/gopath" 29 | ENV["GOROOT"] = "#{testpath}/goroot" 30 | (testpath/"gopath/src/test/p/a.go").write("package p; func A() {}") 31 | # Invoke initialize, hover, and finally exit requests and make sure that LSP server returns proper data 32 | init_req = "{\"id\":0,\"method\":\"initialize\",\"params\":{\"rootPath\":\"file://#{testpath}/gopath/src/test/p\"}}" 33 | init_res = "{\"id\":0,\"result\":{\"capabilities\":{\"textDocumentSync\":1,\"hoverProvider\":true,\"definitionProvider\":true,\"referencesProvider\":true,\"documentSymbolProvider\":true,\"workspaceSymbolProvider\":true}},\"jsonrpc\":\"2.0\"}" 34 | hover_req = "{\"id\":1,\"method\":\"textDocument/hover\",\"params\":{\"textDocument\":{\"uri\":\"file://#{testpath}/gopath/src/test/p/a.go\"},\"position\":{\"line\":0,\"character\":16}}}" 35 | hover_res = "{\"id\":1,\"result\":{\"contents\":[{\"language\":\"go\",\"value\":\"func A()\"}],\"range\":{\"start\":{\"line\":0,\"character\":16},\"end\":{\"line\":0,\"character\":17}}},\"jsonrpc\":\"2.0\"}" 36 | exit_req = "{\"id\":2,\"method\":\"exit\",\"params\":{}}" 37 | require "open3" 38 | Open3.popen3("langserver-go") do |stdin, stdout, _| 39 | stdin.write("Content-Length: #{init_req.length}\r\n\r\n#{init_req}") 40 | sleep(1) 41 | stdin.write("Content-Length: #{hover_req.length}\r\n\r\n#{hover_req}") 42 | sleep(1) 43 | stdin.write("Content-Length: #{exit_req.length}\r\n\r\n#{exit_req}") 44 | stdin.close 45 | assert_equal "Content-Length: #{init_res.length}\r\nContent-Type: application/vscode-jsonrpc; charset=utf8\r\n\r\n#{init_res}Content-Length: #{hover_res.length}\r\nContent-Type: application/vscode-jsonrpc; charset=utf8\r\n\r\n#{hover_res}", stdout.read 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /langserver/vendor/go/doc/filter.go: -------------------------------------------------------------------------------- 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 | package doc 6 | 7 | import "go/ast" 8 | 9 | type Filter func(string) bool 10 | 11 | func matchFields(fields *ast.FieldList, f Filter) bool { 12 | if fields != nil { 13 | for _, field := range fields.List { 14 | for _, name := range field.Names { 15 | if f(name.Name) { 16 | return true 17 | } 18 | } 19 | } 20 | } 21 | return false 22 | } 23 | 24 | func matchDecl(d *ast.GenDecl, f Filter) bool { 25 | for _, d := range d.Specs { 26 | switch v := d.(type) { 27 | case *ast.ValueSpec: 28 | for _, name := range v.Names { 29 | if f(name.Name) { 30 | return true 31 | } 32 | } 33 | case *ast.TypeSpec: 34 | if f(v.Name.Name) { 35 | return true 36 | } 37 | switch t := v.Type.(type) { 38 | case *ast.StructType: 39 | if matchFields(t.Fields, f) { 40 | return true 41 | } 42 | case *ast.InterfaceType: 43 | if matchFields(t.Methods, f) { 44 | return true 45 | } 46 | } 47 | } 48 | } 49 | return false 50 | } 51 | 52 | func filterValues(a []*Value, f Filter) []*Value { 53 | w := 0 54 | for _, vd := range a { 55 | if matchDecl(vd.Decl, f) { 56 | a[w] = vd 57 | w++ 58 | } 59 | } 60 | return a[0:w] 61 | } 62 | 63 | func filterFuncs(a []*Func, f Filter) []*Func { 64 | w := 0 65 | for _, fd := range a { 66 | if f(fd.Name) { 67 | a[w] = fd 68 | w++ 69 | } 70 | } 71 | return a[0:w] 72 | } 73 | 74 | func filterTypes(a []*Type, f Filter) []*Type { 75 | w := 0 76 | for _, td := range a { 77 | n := 0 // number of matches 78 | if matchDecl(td.Decl, f) { 79 | n = 1 80 | } else { 81 | // type name doesn't match, but we may have matching consts, vars, factories or methods 82 | td.Consts = filterValues(td.Consts, f) 83 | td.Vars = filterValues(td.Vars, f) 84 | td.Funcs = filterFuncs(td.Funcs, f) 85 | td.Methods = filterFuncs(td.Methods, f) 86 | n += len(td.Consts) + len(td.Vars) + len(td.Funcs) + len(td.Methods) 87 | } 88 | if n > 0 { 89 | a[w] = td 90 | w++ 91 | } 92 | } 93 | return a[0:w] 94 | } 95 | 96 | // Filter eliminates documentation for names that don't pass through the filter f. 97 | // TODO(gri): Recognize "Type.Method" as a name. 98 | // 99 | func (p *Package) Filter(f Filter) { 100 | p.Consts = filterValues(p.Consts, f) 101 | p.Vars = filterValues(p.Vars, f) 102 | p.Types = filterTypes(p.Types, f) 103 | p.Funcs = filterFuncs(p.Funcs, f) 104 | p.Doc = "" // don't show top-level package doc 105 | } 106 | -------------------------------------------------------------------------------- /langserver/lspx.go: -------------------------------------------------------------------------------- 1 | package langserver 2 | 3 | import "github.com/sourcegraph/go-lsp" 4 | 5 | // This file contains Go-specific extensions to LSP types. 6 | // 7 | // The Go language server MUST NOT rely on these extensions for 8 | // standalone operation on the local file system. (VSCode has no way 9 | // of including these fields.) 10 | 11 | // InitializationOptions are the options supported by go-langserver. It is the 12 | // Config struct, but each field is optional. 13 | type InitializationOptions struct { 14 | // FuncSnippetEnabled is an optional version of Config.FuncSnippetEnabled 15 | FuncSnippetEnabled *bool `json:"funcSnippetEnabled"` 16 | 17 | // GocodeCompletionEnabled is an optional version of 18 | // Config.GocodeCompletionEnabled 19 | GocodeCompletionEnabled *bool `json:"gocodeCompletionEnabled"` 20 | 21 | // FormatTool is an optional version of 22 | // Config.FormatTool 23 | FormatTool *string `json:"formatTool"` 24 | 25 | // LintTool is an optional version of 26 | // Config.LintTool 27 | LintTool *string `json:"lintTool"` 28 | 29 | // GoimportsLocalPrefix is an optional version of 30 | // Config.GoimportsLocalPrefix 31 | GoimportsLocalPrefix *string `json:"goimportsLocalPrefix"` 32 | 33 | // DiagnosticsEnabled enables is an optional version of 34 | // Config.DiagnosticsEnabled 35 | DiagnosticsEnabled *bool `json:"diagnosticsEnabled"` 36 | 37 | // MaxParallelism is an optional version of Config.MaxParallelism 38 | MaxParallelism *int `json:"maxParallelism"` 39 | 40 | // UseBinaryPkgCache is an optional version of Config.UseBinaryPkgCache 41 | UseBinaryPkgCache *bool `json:"useBinaryPkgCache"` 42 | } 43 | 44 | type InitializeParams struct { 45 | lsp.InitializeParams 46 | 47 | InitializationOptions *InitializationOptions `json:"initializationOptions,omitempty"` 48 | 49 | // TODO these should be InitializationOptions 50 | 51 | // NoOSFileSystemAccess makes the server never access the OS file 52 | // system. It exclusively uses the file overlay (from 53 | // textDocument/didOpen) and the LSP proxy's VFS. 54 | NoOSFileSystemAccess bool 55 | 56 | // BuildContext, if set, configures the language server's default 57 | // go/build.Context. 58 | BuildContext *InitializeBuildContextParams 59 | 60 | // RootImportPath is the root Go import path for this 61 | // workspace. For example, 62 | // "golang.org/x/tools" is the root import 63 | // path for "github.com/golang/tools". 64 | RootImportPath string 65 | } 66 | 67 | type InitializeBuildContextParams struct { 68 | // These fields correspond to the fields of the same name from 69 | // go/build.Context. 70 | 71 | GOOS string 72 | GOARCH string 73 | GOPATH string 74 | GOROOT string 75 | CgoEnabled bool 76 | UseAllFiles bool 77 | Compiler string 78 | BuildTags []string 79 | 80 | // Irrelevant fields: ReleaseTags, InstallSuffix. 81 | } 82 | -------------------------------------------------------------------------------- /langserver/fs_test.go: -------------------------------------------------------------------------------- 1 | package langserver 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/sourcegraph/go-lsp" 7 | ) 8 | 9 | type applyContentChangesTestCase struct { 10 | code string 11 | changes []lsp.TextDocumentContentChangeEvent 12 | expected string 13 | } 14 | 15 | var applyContentChangesTestCases = map[string]applyContentChangesTestCase{ 16 | "add new line at end": applyContentChangesTestCase{ 17 | code: "package langserver\n", 18 | changes: []lsp.TextDocumentContentChangeEvent{ 19 | toContentChange(toRange(1, 0, 1, 0), 0, "\n"), 20 | }, 21 | expected: "package langserver\n\n", 22 | }, 23 | "remove line": applyContentChangesTestCase{ 24 | code: "package langserver\n\n// my comment\n", 25 | changes: []lsp.TextDocumentContentChangeEvent{ 26 | toContentChange(toRange(2, 0, 3, 0), 14, ""), 27 | }, 28 | expected: "package langserver\n\n", 29 | }, 30 | "add code in line": applyContentChangesTestCase{ 31 | code: "package langserver\n\n// my comment\n", 32 | changes: []lsp.TextDocumentContentChangeEvent{ 33 | toContentChange(toRange(2, 6, 2, 6), 0, "awesome "), 34 | }, 35 | expected: "package langserver\n\n// my awesome comment\n", 36 | }, 37 | "replace code in line": applyContentChangesTestCase{ 38 | code: "package langserver\n\n// my awesome comment\n", 39 | changes: []lsp.TextDocumentContentChangeEvent{ 40 | toContentChange(toRange(2, 6, 2, 13), 7, "terrible"), 41 | }, 42 | expected: "package langserver\n\n// my terrible comment\n", 43 | }, 44 | "complete replace and change afterwards": applyContentChangesTestCase{ 45 | code: "package langserver\n\n// some code ...\n", 46 | changes: []lsp.TextDocumentContentChangeEvent{ 47 | // complete replace of the contents 48 | lsp.TextDocumentContentChangeEvent{Range: nil, RangeLength: 0, Text: "package langserver_2\n"}, 49 | // with an additional change afterwards 50 | toContentChange(toRange(1, 0, 1, 0), 0, "\n// code for langserver_2\n"), 51 | }, 52 | expected: "package langserver_2\n\n// code for langserver_2\n", 53 | }, 54 | } 55 | 56 | func TestApplyContentChanges(t *testing.T) { 57 | for label, test := range applyContentChangesTestCases { 58 | t.Run(label, func(t *testing.T) { 59 | newCode, err := applyContentChanges(lsp.DocumentURI("/src/langserver.go"), []byte(test.code), test.changes) 60 | if err != nil { 61 | t.Error(err) 62 | } 63 | if string(newCode) != test.expected { 64 | t.Errorf("Expected %q but got %q", test.expected, newCode) 65 | } 66 | }) 67 | } 68 | } 69 | 70 | func toContentChange(r lsp.Range, rl uint, t string) lsp.TextDocumentContentChangeEvent { 71 | return lsp.TextDocumentContentChangeEvent{Range: &r, RangeLength: rl, Text: t} 72 | } 73 | 74 | func toRange(sl, sc, el, ec int) lsp.Range { 75 | return lsp.Range{Start: toPosition(sl, sc), End: toPosition(el, ec)} 76 | } 77 | 78 | func toPosition(l, c int) lsp.Position { 79 | return lsp.Position{Line: l, Character: c} 80 | } 81 | -------------------------------------------------------------------------------- /pkg/tools/importgraph.go: -------------------------------------------------------------------------------- 1 | // Original importgraph.Build contains the below copyright notice: 2 | // 3 | // Copyright 2014 The Go Authors. All rights reserved. 4 | // Use of this source code is governed by a BSD-style 5 | // license that can be found in the LICENSE file. 6 | 7 | package tools 8 | 9 | import ( 10 | "go/build" 11 | "sync" 12 | 13 | "golang.org/x/tools/refactor/importgraph" 14 | ) 15 | 16 | // FindPackageFunc is the same type as loader.Config.FindPackage. Refer to its docstring. 17 | type FindPackageFunc func(ctxt *build.Context, fromDir, importPath string, mode build.ImportMode) (*build.Package, error) 18 | 19 | // BuildReverseImportGraph is much like importgraph.Build, except: 20 | // * it only returns the reverse graph 21 | // * it does not return errors 22 | // * it uses a custom FindPackageFunc 23 | // * it only searches pkgs under dir (but graph can contain pkgs outside of dir) 24 | // * it searches xtest pkgs as well 25 | // 26 | // The code is adapted from the original function. 27 | func BuildReverseImportGraph(ctxt *build.Context, findPackage FindPackageFunc, dir string) importgraph.Graph { 28 | type importEdge struct { 29 | from, to string 30 | } 31 | 32 | ch := make(chan importEdge) 33 | 34 | go func() { 35 | sema := make(chan int, 20) // I/O concurrency limiting semaphore 36 | var wg sync.WaitGroup 37 | for _, path := range ListPkgsUnderDir(ctxt, dir) { 38 | wg.Add(1) 39 | go func(path string) { 40 | defer wg.Done() 41 | 42 | sema <- 1 43 | // Even in error cases, Import usually returns a package. 44 | bp, _ := findPackage(ctxt, path, "", 0) 45 | <-sema 46 | 47 | memo := make(map[string]string) 48 | absolutize := func(path string) string { 49 | canon, ok := memo[path] 50 | if !ok { 51 | sema <- 1 52 | bp2, _ := findPackage(ctxt, path, bp.Dir, build.FindOnly) 53 | <-sema 54 | 55 | if bp2 != nil { 56 | canon = bp2.ImportPath 57 | } else { 58 | canon = path 59 | } 60 | memo[path] = canon 61 | } 62 | return canon 63 | } 64 | 65 | if bp != nil { 66 | for _, imp := range bp.Imports { 67 | ch <- importEdge{path, absolutize(imp)} 68 | } 69 | for _, imp := range bp.TestImports { 70 | ch <- importEdge{path, absolutize(imp)} 71 | } 72 | for _, imp := range bp.XTestImports { 73 | ch <- importEdge{path, absolutize(imp)} 74 | } 75 | } 76 | 77 | }(path) 78 | } 79 | wg.Wait() 80 | close(ch) 81 | }() 82 | 83 | reverse := make(importgraph.Graph) 84 | 85 | for e := range ch { 86 | if e.to == "C" { 87 | continue // "C" is fake 88 | } 89 | addEdge(reverse, e.to, e.from) 90 | } 91 | 92 | return reverse 93 | } 94 | 95 | func addEdge(g importgraph.Graph, from, to string) { 96 | edges := g[from] 97 | if edges == nil { 98 | edges = make(map[string]bool) 99 | g[from] = edges 100 | } 101 | edges[to] = true 102 | } 103 | -------------------------------------------------------------------------------- /gituri/uri.go: -------------------------------------------------------------------------------- 1 | package gituri 2 | 3 | import ( 4 | "errors" 5 | "net/url" 6 | "path" 7 | "strings" 8 | ) 9 | 10 | // A URI is a wrapper around url.URL that makes it easier to get and 11 | // manipulate Sourcegraph-specific components. All URIs are valid 12 | // URLs, but Sourcegraph assigns special meaning to certain URL components as described below. 13 | // 14 | // Sourcegraph URIs can refer to repos (at an optional revision), or a 15 | // file or directory thereof. 16 | // 17 | // The format is "CLONEURL?REV#PATH". For example: 18 | // 19 | // git://github.com/facebook/react?master 20 | // git://github.com/gorilla/mux?HEAD 21 | // git://github.com/golang/go?0dc31fb#src/net/http/server.go 22 | // git://github.com/golang/tools?79f4a1#godoc/page.go 23 | // 24 | // A Sourcegraph URI is not guaranteed (or intended) to be a unique or 25 | // canonical reference to a resource. A repository can be clonable at 26 | // several different URLs, and any of them can be used in the URI. A 27 | // given file in a repository has any number of URIs that refer to it 28 | // (e.g., using the branch name vs. the commit ID, using clean 29 | // vs. non-clean file paths, etc.). 30 | type URI struct { 31 | url.URL 32 | } 33 | 34 | // Parse parses uriStr to a URI. The uriStr should be an absolute URL. 35 | func Parse(uriStr string) (*URI, error) { 36 | u, err := url.Parse(uriStr) 37 | if err != nil { 38 | return nil, err 39 | } 40 | if !u.IsAbs() { 41 | return nil, &url.Error{Op: "gituri.Parse", URL: uriStr, Err: errors.New("sourcegraph URI must be absolute")} 42 | } 43 | return &URI{*u}, nil 44 | } 45 | 46 | // CloneURL returns the repository clone URL component of the URI. 47 | func (u *URI) CloneURL() *url.URL { 48 | return &url.URL{ 49 | Scheme: u.Scheme, 50 | Host: u.Host, 51 | Path: u.Path, 52 | } 53 | } 54 | 55 | // Repo returns the repository name (e.g., "github.com/foo/bar"). 56 | func (u *URI) Repo() string { return u.Host + strings.TrimPrefix(u.Path, ".git") } 57 | 58 | // Rev returns the repository revision component of the URI (the raw 59 | // query string). 60 | func (u *URI) Rev() string { return u.RawQuery } 61 | 62 | // FilePath returns the cleaned file path component of the URI (in the 63 | // URL fragment). Leading slashes are removed. If it is ".", an empty 64 | // string is returned. 65 | func (u *URI) FilePath() string { return cleanPath(u.Fragment) } 66 | 67 | // ResolveFilePath returns the cleaned file path component obtained by 68 | // appending p to the URI's file path. It is called "resolve" not 69 | // "join" because it strips p's leading slash (if any). 70 | func (u *URI) ResolveFilePath(p string) string { 71 | return cleanPath(path.Join(u.FilePath(), strings.TrimPrefix(p, "/"))) 72 | } 73 | 74 | // WithFilePath returns a copy of u with the file path p overwriting 75 | // the existing file path (if any). 76 | func (u *URI) WithFilePath(p string) *URI { 77 | copy := *u 78 | copy.Fragment = cleanPath(p) 79 | return © 80 | } 81 | 82 | func cleanPath(p string) string { 83 | p = path.Clean(p) 84 | p = strings.TrimPrefix(p, "/") 85 | if p == "." { 86 | p = "" 87 | } 88 | return p 89 | } 90 | -------------------------------------------------------------------------------- /langserver/vendor/go/doc/headscan.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 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 | // +build ignore 6 | 7 | /* 8 | The headscan command extracts comment headings from package files; 9 | it is used to detect false positives which may require an adjustment 10 | to the comment formatting heuristics in comment.go. 11 | 12 | Usage: headscan [-root root_directory] 13 | 14 | By default, the $GOROOT/src directory is scanned. 15 | */ 16 | package main 17 | 18 | import ( 19 | "bytes" 20 | "flag" 21 | "fmt" 22 | "go/doc" 23 | "go/parser" 24 | "go/token" 25 | "os" 26 | "path/filepath" 27 | "regexp" 28 | "runtime" 29 | "strings" 30 | ) 31 | 32 | var ( 33 | root = flag.String("root", filepath.Join(runtime.GOROOT(), "src"), "root of filesystem tree to scan") 34 | verbose = flag.Bool("v", false, "verbose mode") 35 | ) 36 | 37 | // ToHTML in comment.go assigns a (possibly blank) ID to each heading 38 | var html_h = regexp.MustCompile(`

`) 39 | 40 | const html_endh = "

\n" 41 | 42 | func isGoFile(fi os.FileInfo) bool { 43 | return strings.HasSuffix(fi.Name(), ".go") && 44 | !strings.HasSuffix(fi.Name(), "_test.go") 45 | } 46 | 47 | func appendHeadings(list []string, comment string) []string { 48 | var buf bytes.Buffer 49 | doc.ToHTML(&buf, comment, nil) 50 | for s := buf.String(); ; { 51 | loc := html_h.FindStringIndex(s) 52 | if len(loc) == 0 { 53 | break 54 | } 55 | i := loc[1] 56 | j := strings.Index(s, html_endh) 57 | if j < 0 { 58 | list = append(list, s[i:]) // incorrect HTML 59 | break 60 | } 61 | list = append(list, s[i:j]) 62 | s = s[j+len(html_endh):] 63 | } 64 | return list 65 | } 66 | 67 | func main() { 68 | flag.Parse() 69 | fset := token.NewFileSet() 70 | nheadings := 0 71 | err := filepath.Walk(*root, func(path string, fi os.FileInfo, err error) error { 72 | if !fi.IsDir() { 73 | return nil 74 | } 75 | pkgs, err := parser.ParseDir(fset, path, isGoFile, parser.ParseComments) 76 | if err != nil { 77 | if *verbose { 78 | fmt.Fprintln(os.Stderr, err) 79 | } 80 | return nil 81 | } 82 | for _, pkg := range pkgs { 83 | d := doc.New(pkg, path, doc.Mode(0)) 84 | list := appendHeadings(nil, d.Doc) 85 | for _, d := range d.Consts { 86 | list = appendHeadings(list, d.Doc) 87 | } 88 | for _, d := range d.Types { 89 | list = appendHeadings(list, d.Doc) 90 | } 91 | for _, d := range d.Vars { 92 | list = appendHeadings(list, d.Doc) 93 | } 94 | for _, d := range d.Funcs { 95 | list = appendHeadings(list, d.Doc) 96 | } 97 | if len(list) > 0 { 98 | // directories may contain multiple packages; 99 | // print path and package name 100 | fmt.Printf("%s (package %s)\n", path, pkg.Name) 101 | for _, h := range list { 102 | fmt.Printf("\t%s\n", h) 103 | } 104 | nheadings += len(list) 105 | } 106 | } 107 | return nil 108 | }) 109 | if err != nil { 110 | fmt.Fprintln(os.Stderr, err) 111 | os.Exit(1) 112 | } 113 | fmt.Println(nheadings, "headings found") 114 | } 115 | -------------------------------------------------------------------------------- /pkg/tools/buildutil.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "go/build" 5 | "path" 6 | "sort" 7 | "sync" 8 | 9 | "golang.org/x/tools/go/buildutil" 10 | 11 | "github.com/sourcegraph/go-langserver/langserver/util" 12 | ) 13 | 14 | // ListPkgsUnderDir is buildutil.ExpandPattern(ctxt, []string{dir + 15 | // "/..."}). The implementation is modified from the upstream 16 | // buildutil.ExpandPattern so we can be much faster. buildutil.ExpandPattern 17 | // looks at all directories under GOPATH if there is a `...` pattern. This 18 | // instead only explores the directories under dir. In future 19 | // buildutil.ExpandPattern may be more performant (there are TODOs for it). 20 | func ListPkgsUnderDir(ctxt *build.Context, dir string) []string { 21 | ch := make(chan string) 22 | dir = path.Clean(dir) 23 | 24 | var ( 25 | wg sync.WaitGroup 26 | dirInGOPATH bool 27 | ) 28 | 29 | for _, root := range ctxt.SrcDirs() { 30 | root = path.Clean(root) 31 | 32 | if util.PathHasPrefix(root, dir) { 33 | // If we are a child of dir, we can just start at the 34 | // root. A concrete example of this happening is when 35 | // root=/goroot/src and dir=/goroot 36 | dir = root 37 | } 38 | 39 | if !util.PathHasPrefix(dir, root) { 40 | continue 41 | } 42 | 43 | wg.Add(1) 44 | go func() { 45 | allPackages(ctxt, root, dir, ch) 46 | wg.Done() 47 | }() 48 | dirInGOPATH = true 49 | } 50 | 51 | if !dirInGOPATH { 52 | root := path.Dir(dir) 53 | wg.Add(1) 54 | go func() { 55 | allPackages(ctxt, root, dir, ch) 56 | wg.Done() 57 | }() 58 | } 59 | 60 | go func() { 61 | wg.Wait() 62 | close(ch) 63 | }() 64 | 65 | var pkgs []string 66 | for p := range ch { 67 | pkgs = append(pkgs, p) 68 | } 69 | sort.Strings(pkgs) 70 | return pkgs 71 | } 72 | 73 | // We use a process-wide counting semaphore to limit 74 | // the number of parallel calls to ReadDir. 75 | var ioLimit = make(chan bool, 20) 76 | 77 | // allPackages is from tools/go/buildutil. We don't use the exported method 78 | // since it doesn't allow searching from a directory. We need from a specific 79 | // directory for performance on large GOPATHs. 80 | func allPackages(ctxt *build.Context, root, start string, ch chan<- string) { 81 | 82 | var wg sync.WaitGroup 83 | 84 | var walkDir func(dir string) 85 | walkDir = func(dir string) { 86 | // Avoid .foo, _foo, and testdata directory trees. 87 | base := path.Base(dir) 88 | if base == "" || base[0] == '.' || base[0] == '_' || base == "testdata" { 89 | return 90 | } 91 | 92 | pkg := util.PathTrimPrefix(dir, root) 93 | 94 | // Prune search if we encounter any of these import paths. 95 | switch pkg { 96 | case "builtin": 97 | return 98 | } 99 | 100 | if pkg != "" { 101 | ch <- pkg 102 | } 103 | 104 | ioLimit <- true 105 | files, _ := buildutil.ReadDir(ctxt, dir) 106 | <-ioLimit 107 | for _, fi := range files { 108 | fi := fi 109 | if fi.IsDir() { 110 | wg.Add(1) 111 | go func() { 112 | walkDir(buildutil.JoinPath(ctxt, dir, fi.Name())) 113 | wg.Done() 114 | }() 115 | } 116 | } 117 | } 118 | 119 | walkDir(start) 120 | wg.Wait() 121 | } 122 | -------------------------------------------------------------------------------- /langserver/signature.go: -------------------------------------------------------------------------------- 1 | package langserver 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "go/ast" 7 | "go/token" 8 | "go/types" 9 | 10 | "github.com/sourcegraph/go-langserver/langserver/util" 11 | "github.com/sourcegraph/go-lsp" 12 | "github.com/sourcegraph/jsonrpc2" 13 | ) 14 | 15 | func (h *LangHandler) handleTextDocumentSignatureHelp(ctx context.Context, conn jsonrpc2.JSONRPC2, req *jsonrpc2.Request, params lsp.TextDocumentPositionParams) (*lsp.SignatureHelp, error) { 16 | if !util.IsURI(params.TextDocument.URI) { 17 | return nil, &jsonrpc2.Error{ 18 | Code: jsonrpc2.CodeInvalidParams, 19 | Message: fmt.Sprintf("textDocument/signatureHelp not yet supported for out-of-workspace URI (%q)", params.TextDocument.URI), 20 | } 21 | } 22 | 23 | fset, _, nodes, prog, pkg, start, err := h.typecheck(ctx, conn, params.TextDocument.URI, params.Position) 24 | if err != nil { 25 | if _, ok := err.(*invalidNodeError); !ok { 26 | return nil, err 27 | } 28 | } 29 | 30 | call := callExpr(fset, nodes) 31 | if call == nil { 32 | return nil, nil 33 | } 34 | t := pkg.TypeOf(call.Fun) 35 | signature, ok := t.(*types.Signature) 36 | if !ok { 37 | return nil, nil 38 | } 39 | info := lsp.SignatureInformation{Label: shortType(signature)} 40 | sParams := signature.Params() 41 | info.Parameters = make([]lsp.ParameterInformation, sParams.Len()) 42 | for i := 0; i < sParams.Len(); i++ { 43 | info.Parameters[i] = lsp.ParameterInformation{Label: shortParam(sParams.At(i))} 44 | } 45 | activeParameter := len(call.Args) 46 | for index, arg := range call.Args { 47 | if arg.End() >= *start { 48 | activeParameter = index 49 | break 50 | } 51 | } 52 | 53 | funcIdent, funcOk := call.Fun.(*ast.Ident) 54 | if !funcOk { 55 | selExpr, selOk := call.Fun.(*ast.SelectorExpr) 56 | if selOk { 57 | funcIdent = selExpr.Sel 58 | funcOk = true 59 | } 60 | } 61 | if funcIdent != nil && funcOk { 62 | funcObj := pkg.ObjectOf(funcIdent) 63 | _, path, _ := prog.PathEnclosingInterval(funcObj.Pos(), funcObj.Pos()) 64 | for i := 0; i < len(path); i++ { 65 | a, b := path[i].(*ast.FuncDecl) 66 | if b && a.Doc != nil { 67 | info.Documentation = a.Doc.Text() 68 | break 69 | } 70 | } 71 | } 72 | 73 | return &lsp.SignatureHelp{Signatures: []lsp.SignatureInformation{info}, ActiveSignature: 0, ActiveParameter: activeParameter}, nil 74 | } 75 | 76 | // callExpr climbs AST tree up until call expression 77 | func callExpr(fset *token.FileSet, nodes []ast.Node) *ast.CallExpr { 78 | for _, node := range nodes { 79 | callExpr, ok := node.(*ast.CallExpr) 80 | if ok { 81 | return callExpr 82 | } 83 | } 84 | return nil 85 | } 86 | 87 | // shortTyoe returns shorthand type notation without specifying type's import path 88 | func shortType(t types.Type) string { 89 | return types.TypeString(t, func(*types.Package) string { 90 | return "" 91 | }) 92 | } 93 | 94 | // shortParam returns shorthand parameter notation in form "name type" without specifying type's import path 95 | func shortParam(param *types.Var) string { 96 | ret := param.Name() 97 | if ret != "" { 98 | ret += " " 99 | } 100 | return ret + shortType(param.Type()) 101 | } 102 | -------------------------------------------------------------------------------- /gituri/uri_test.go: -------------------------------------------------------------------------------- 1 | package gituri 2 | 3 | import ( 4 | "net/url" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestParse(t *testing.T) { 10 | tests := map[string]url.URL{} 11 | for uriStr, want := range tests { 12 | t.Run(strings.Replace(uriStr, "/", "-", -1), func(t *testing.T) { 13 | uri, err := Parse(uriStr) 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | if uri.URL != want { 18 | t.Errorf("got %+v, want %+v", uri.URL, want) 19 | } 20 | }) 21 | } 22 | } 23 | 24 | func TestParse_error(t *testing.T) { 25 | tests := map[string]string{ 26 | "github.com/foo/bar": "must be absolute", 27 | "/github.com/foo/bar": "must be absolute", 28 | "//github.com/foo/bar": "must be absolute", 29 | "%": "invalid", 30 | } 31 | for uriStr, want := range tests { 32 | t.Run(strings.Replace(uriStr, "/", "-", -1), func(t *testing.T) { 33 | uri, err := Parse(uriStr) 34 | if err == nil { 35 | t.Fatalf("got nil error, want %q", want) 36 | } 37 | if uri != nil { 38 | t.Error("got non-nil URL, want nil") 39 | } 40 | if !strings.Contains(err.Error(), want) { 41 | t.Errorf("got %q, want it to contain %q", err, want) 42 | } 43 | }) 44 | } 45 | } 46 | 47 | func TestURI_CloneURL(t *testing.T) { 48 | want := "https://github.com/foo/bar" 49 | uriStrs := []string{ 50 | "https://github.com/foo/bar", 51 | "https://github.com/foo/bar?v", 52 | "https://github.com/foo/bar?v#f", 53 | "https://github.com/foo/bar#f", 54 | } 55 | for _, uriStr := range uriStrs { 56 | t.Run(strings.Replace(uriStr, "/", "-", -1), func(t *testing.T) { 57 | uri, err := Parse(uriStr) 58 | if err != nil { 59 | t.Fatal(err) 60 | } 61 | if uri.CloneURL().String() != want { 62 | t.Errorf("got %s, want %s", uri.CloneURL(), want) 63 | } 64 | }) 65 | } 66 | } 67 | 68 | func TestURI_Rev(t *testing.T) { 69 | tests := map[string]string{ 70 | "https://github.com/foo/bar": "", 71 | "https://github.com/foo/bar?v": "v", 72 | "https://github.com/foo/bar?v#": "v", 73 | "https://github.com/foo/bar?v#f": "v", 74 | } 75 | for uriStr, want := range tests { 76 | t.Run(strings.Replace(uriStr, "/", "-", -1), func(t *testing.T) { 77 | uri, err := Parse(uriStr) 78 | if err != nil { 79 | t.Fatal(err) 80 | } 81 | if uri.Rev() != want { 82 | t.Errorf("got %s, want %s", uri.Rev(), want) 83 | } 84 | }) 85 | } 86 | } 87 | 88 | func TestURI_FilePath(t *testing.T) { 89 | tests := map[string]string{ 90 | "https://github.com/foo/bar": "", 91 | "https://github.com/foo/bar?v": "", 92 | "https://github.com/foo/bar?v#": "", 93 | "https://github.com/foo/bar?v#.": "", 94 | "https://github.com/foo/bar?v#f": "f", 95 | "https://github.com/foo/bar?v#/f": "f", 96 | "https://github.com/foo/bar?v#f/d": "f/d", 97 | "https://github.com/foo/bar?v#f/..": "", 98 | "https://github.com/foo/bar?v#f/d/..": "f", 99 | "https://github.com/foo/bar?v#//": "", 100 | "https://github.com/foo/bar?v#d%2Ff": "d/f", 101 | } 102 | for uriStr, want := range tests { 103 | t.Run(strings.Replace(uriStr, "/", "-", -1), func(t *testing.T) { 104 | uri, err := Parse(uriStr) 105 | if err != nil { 106 | t.Fatal(err) 107 | } 108 | if uri.FilePath() != want { 109 | t.Errorf("got %s, want %s", uri.FilePath(), want) 110 | } 111 | }) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /langserver/internal/godef/go/parser/parser_test.go: -------------------------------------------------------------------------------- 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 | package parser 6 | 7 | import ( 8 | "os" 9 | "testing" 10 | 11 | "go/token" 12 | ) 13 | 14 | var fset = token.NewFileSet() 15 | 16 | var illegalInputs = []interface{}{ 17 | nil, 18 | 3.14, 19 | []byte(nil), 20 | "foo!", 21 | `package p; func f() { if /* should have condition */ {} };`, 22 | `package p; func f() { if ; /* should have condition */ {} };`, 23 | `package p; func f() { if f(); /* should have condition */ {} };`, 24 | } 25 | 26 | func TestParseIllegalInputs(t *testing.T) { 27 | for _, src := range illegalInputs { 28 | _, err := ParseFile(fset, "", src, 0, nil, naiveImportPathToName) 29 | if err == nil { 30 | t.Errorf("ParseFile(%v) should have failed", src) 31 | } 32 | } 33 | } 34 | 35 | var validPrograms = []interface{}{ 36 | "package p\n", 37 | `package p;`, 38 | `package p; import "fmt"; func f() { fmt.Println("Hello, World!") };`, 39 | `package p; func f() { if f(T{}) {} };`, 40 | `package p; func f() { _ = (<-chan int)(x) };`, 41 | `package p; func f() { _ = (<-chan <-chan int)(x) };`, 42 | `package p; func f(func() func() func());`, 43 | `package p; func f(...T);`, 44 | `package p; func f(float, ...int);`, 45 | `package p; func f(x int, a ...int) { f(0, a...); f(1, a...,) };`, 46 | `package p; type T []int; var a []bool; func f() { if a[T{42}[0]] {} };`, 47 | `package p; type T []int; func g(int) bool { return true }; func f() { if g(T{42}[0]) {} };`, 48 | `package p; type T []int; func f() { for _ = range []int{T{42}[0]} {} };`, 49 | `package p; var a = T{{1, 2}, {3, 4}}`, 50 | `package p; func f() { select { case <- c: case c <- d: case c <- <- d: case <-c <- d: } };`, 51 | `package p; func f() { if ; true {} };`, 52 | `package p; func f() { switch ; {} };`, 53 | `package p; func f() (int,) {}`, 54 | `package p; func _(x []int) { for range x {} }`, 55 | } 56 | 57 | func TestParseValidPrograms(t *testing.T) { 58 | for _, src := range validPrograms { 59 | _, err := ParseFile(fset, "", src, Trace, nil, naiveImportPathToName) 60 | if err != nil { 61 | t.Errorf("ParseFile(%q): %v", src, err) 62 | } 63 | } 64 | } 65 | 66 | var validFiles = []string{ 67 | "parser.go", 68 | "parser_test.go", 69 | } 70 | 71 | func TestParse3(t *testing.T) { 72 | for _, filename := range validFiles { 73 | _, err := ParseFile(fset, filename, nil, DeclarationErrors, nil, nil) 74 | if err != nil { 75 | t.Errorf("ParseFile(%s): %v", filename, err) 76 | } 77 | } 78 | } 79 | 80 | func nameFilter(filename string) bool { 81 | switch filename { 82 | case "parser.go": 83 | case "interface.go": 84 | case "parser_test.go": 85 | default: 86 | return false 87 | } 88 | return true 89 | } 90 | 91 | func dirFilter(f os.FileInfo) bool { return nameFilter(f.Name()) } 92 | 93 | func TestParse4(t *testing.T) { 94 | path := "." 95 | pkgs, err := ParseDir(fset, path, dirFilter, 0, naiveImportPathToName) 96 | if err != nil { 97 | t.Fatalf("ParseDir(%s): %v", path, err) 98 | } 99 | if len(pkgs) != 1 { 100 | t.Errorf("incorrect number of packages: %d", len(pkgs)) 101 | } 102 | pkg := pkgs["parser"] 103 | if pkg == nil { 104 | t.Errorf(`package "parser" not found`) 105 | return 106 | } 107 | for filename := range pkg.Files { 108 | if !nameFilter(filename) { 109 | t.Errorf("unexpected package file: %s", filename) 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /vfsutil/git_test.go: -------------------------------------------------------------------------------- 1 | package vfsutil 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "os/exec" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func TestGitRepoVFS(t *testing.T) { 12 | fs := &GitRepoVFS{ 13 | CloneURL: "git://github.com/gorilla/schema", 14 | Rev: "0164a00ab4cd01d814d8cd5bf63fd9fcea30e23b", 15 | } 16 | want := map[string]string{ 17 | "/LICENSE": "...", 18 | "/README.md": "schema...", 19 | "/cache.go": "// Copyright...", 20 | "/converter.go": "// Copyright...", 21 | "/decoder.go": "// Copyright...", 22 | "/decoder_test.go": "// Copyright...", 23 | "/doc.go": "// Copyright...", 24 | "/.travis.yml": "...", 25 | } 26 | 27 | testVFS(t, fs, want) 28 | } 29 | 30 | func TestGitRepoVFS_subtree(t *testing.T) { 31 | // Any public repo will work. 32 | fs := &GitRepoVFS{ 33 | CloneURL: "git://github.com/gorilla/rpc", 34 | Rev: "e592e2e099465ae27afa66ec089d570904cd2d53", 35 | Subtree: "protorpc", 36 | } 37 | want := map[string]string{ 38 | "/doc.go": "// Copyright 2...", 39 | "/protorpc_test.go": "// Copyright 2...", 40 | "/server.go": "// Copyright 2...", 41 | } 42 | 43 | testVFS(t, fs, want) 44 | } 45 | 46 | func TestGitRepoVFS_cache(t *testing.T) { 47 | // We use a different gitArchiveBasePath to ensure it is empty 48 | { 49 | d, err := ioutil.TempDir("", "vfsutil_test") 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | defer os.RemoveAll(d) 54 | defer func(orig string) { gitArchiveBasePath = orig }(gitArchiveBasePath) 55 | gitArchiveBasePath = d 56 | } 57 | 58 | cloneURL, err := ioutil.TempDir("", "vfsutil_test") 59 | if err != nil { 60 | t.Fatal(err) 61 | } 62 | defer os.RemoveAll(cloneURL) 63 | 64 | runCmds := func(cmds ...string) string { 65 | var out []byte 66 | var err error 67 | cmds = append(cmds, "git rev-parse HEAD") 68 | for _, cmd := range cmds { 69 | c := exec.Command("bash", "-c", cmd) 70 | c.Dir = cloneURL 71 | c.Env = os.Environ() 72 | c.Env = append(c.Env, []string{ 73 | "GIT_COMMITTER_NAME=a", 74 | "GIT_COMMITTER_EMAIL=a@a.com", 75 | "GIT_AUTHOR_NAME=a", 76 | "GIT_AUTHOR_EMAIL=a@a.com", 77 | }...) 78 | out, err = c.CombinedOutput() 79 | if err != nil { 80 | t.Fatalf("Command %q failed. Output was:\n\n%s", cmd, out) 81 | } 82 | } 83 | return strings.TrimSpace(string(out)) 84 | } 85 | 86 | rev1 := runCmds( 87 | "git init", 88 | "echo -n text1 > file", 89 | "git add file", 90 | "git commit -m msg", 91 | ) 92 | rev2 := runCmds( 93 | "echo -n text2 > file", 94 | "git add file", 95 | "git commit -m msg", 96 | ) 97 | 98 | // On first attempt the cache should be empty, so this tests we can 99 | // clone 100 | fs := &GitRepoVFS{ 101 | CloneURL: cloneURL, 102 | Rev: rev1, 103 | } 104 | want := map[string]string{ 105 | "/file": "text1", 106 | } 107 | testVFS(t, fs, want) 108 | 109 | // Our second attempt should have the commit already. So this tests we 110 | // just directly use it 111 | fs = &GitRepoVFS{ 112 | CloneURL: cloneURL, 113 | Rev: rev2, 114 | } 115 | want = map[string]string{ 116 | "/file": "text2", 117 | } 118 | testVFS(t, fs, want) 119 | 120 | // Now we add a commit to test the update path 121 | rev3 := runCmds( 122 | "echo -n text3 > file", 123 | "git add file", 124 | "git commit -m msg", 125 | ) 126 | fs = &GitRepoVFS{ 127 | CloneURL: cloneURL, 128 | Rev: rev3, 129 | } 130 | want = map[string]string{ 131 | "/file": "text3", 132 | } 133 | testVFS(t, fs, want) 134 | } 135 | -------------------------------------------------------------------------------- /langserver/vendor/go/doc/doc.go: -------------------------------------------------------------------------------- 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 | // Package doc extracts source code documentation from a Go AST. 6 | package doc 7 | 8 | import ( 9 | "go/ast" 10 | "go/token" 11 | ) 12 | 13 | // Package is the documentation for an entire package. 14 | type Package struct { 15 | Doc string 16 | Name string 17 | ImportPath string 18 | Imports []string 19 | Filenames []string 20 | Notes map[string][]*Note 21 | 22 | // Deprecated: For backward compatibility Bugs is still populated, 23 | // but all new code should use Notes instead. 24 | Bugs []string 25 | 26 | // declarations 27 | Consts []*Value 28 | Types []*Type 29 | Vars []*Value 30 | Funcs []*Func 31 | } 32 | 33 | // Value is the documentation for a (possibly grouped) var or const declaration. 34 | type Value struct { 35 | Doc string 36 | Names []string // var or const names in declaration order 37 | Decl *ast.GenDecl 38 | 39 | order int 40 | } 41 | 42 | // Type is the documentation for a type declaration. 43 | type Type struct { 44 | Doc string 45 | Name string 46 | Decl *ast.GenDecl 47 | 48 | // associated declarations 49 | Consts []*Value // sorted list of constants of (mostly) this type 50 | Vars []*Value // sorted list of variables of (mostly) this type 51 | Funcs []*Func // sorted list of functions returning this type 52 | Methods []*Func // sorted list of methods (including embedded ones) of this type 53 | } 54 | 55 | // Func is the documentation for a func declaration. 56 | type Func struct { 57 | Doc string 58 | Name string 59 | Decl *ast.FuncDecl 60 | 61 | // methods 62 | // (for functions, these fields have the respective zero value) 63 | Recv string // actual receiver "T" or "*T" 64 | Orig string // original receiver "T" or "*T" 65 | Level int // embedding level; 0 means not embedded 66 | } 67 | 68 | // A Note represents a marked comment starting with "MARKER(uid): note body". 69 | // Any note with a marker of 2 or more upper case [A-Z] letters and a uid of 70 | // at least one character is recognized. The ":" following the uid is optional. 71 | // Notes are collected in the Package.Notes map indexed by the notes marker. 72 | type Note struct { 73 | Pos, End token.Pos // position range of the comment containing the marker 74 | UID string // uid found with the marker 75 | Body string // note body text 76 | } 77 | 78 | // Mode values control the operation of New. 79 | type Mode int 80 | 81 | const ( 82 | // extract documentation for all package-level declarations, 83 | // not just exported ones 84 | AllDecls Mode = 1 << iota 85 | 86 | // show all embedded methods, not just the ones of 87 | // invisible (unexported) anonymous fields 88 | AllMethods 89 | ) 90 | 91 | // New computes the package documentation for the given package AST. 92 | // New takes ownership of the AST pkg and may edit or overwrite it. 93 | // 94 | func New(pkg *ast.Package, importPath string, mode Mode) *Package { 95 | var r reader 96 | r.readPackage(pkg, mode) 97 | r.computeMethodSets() 98 | r.cleanupTypes() 99 | return &Package{ 100 | Doc: r.doc, 101 | Name: pkg.Name, 102 | ImportPath: importPath, 103 | Imports: sortedKeys(r.imports), 104 | Filenames: r.filenames, 105 | Notes: r.notes, 106 | Bugs: noteBodies(r.notes["BUG"]), 107 | Consts: sortedValues(r.values, token.CONST), 108 | Types: sortedTypes(r.types, mode&AllMethods != 0), 109 | Vars: sortedValues(r.values, token.VAR), 110 | Funcs: sortedFuncs(r.funcs, true), 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /debugserver/debug.go: -------------------------------------------------------------------------------- 1 | package debugserver 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "net/http/pprof" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | 13 | "golang.org/x/net/trace" 14 | 15 | "github.com/prometheus/client_golang/prometheus/promhttp" 16 | ) 17 | 18 | func getenv(key, fallback string) string { 19 | value := os.Getenv(key) 20 | if value == "" { 21 | return fallback 22 | } 23 | return value 24 | } 25 | 26 | var ( 27 | addr = getenv("PROF_HTTP", ":6060") 28 | ) 29 | 30 | func init() { 31 | err := json.Unmarshal([]byte(getenv("PROF_SERVICES", "[]")), &Services) 32 | if err != nil { 33 | panic("failed to JSON unmarshal PROF_SERVICES: " + err.Error()) 34 | } 35 | 36 | if addr == "" { 37 | // Look for our binname in the services list 38 | name := filepath.Base(os.Args[0]) 39 | for _, svc := range Services { 40 | if svc.Name == name { 41 | addr = svc.Host 42 | break 43 | } 44 | } 45 | } 46 | } 47 | 48 | // Endpoint is a handler for the debug server. It will be displayed on the 49 | // debug index page. 50 | type Endpoint struct { 51 | // Name is the name shown on the index page for the endpoint 52 | Name string 53 | // Path is passed to http.Mux.Handle as the pattern. 54 | Path string 55 | // Handler is the debug handler 56 | Handler http.Handler 57 | } 58 | 59 | // Services is the list of registered services' debug addresses. Populated 60 | // from SRC_PROF_MAP. 61 | var Services []Service 62 | 63 | // Service is a service's debug addr (host:port). 64 | type Service struct { 65 | // Name of the service. Always the binary name. example: "gitserver" 66 | Name string 67 | 68 | // Host is the host:port for the services SRC_PROF_HTTP. example: 69 | // "127.0.0.1:6060" 70 | Host string 71 | } 72 | 73 | // Start runs a debug server (pprof, prometheus, etc) if it is configured (via 74 | // SRC_PROF_HTTP environment variable). It is blocking. 75 | func Start(extra ...Endpoint) { 76 | if addr == "" { 77 | return 78 | } 79 | 80 | pp := http.NewServeMux() 81 | index := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 82 | w.Write([]byte(` 83 | Vars
84 | PProf
85 | Metrics
86 | Requests
87 | Events
88 | `)) 89 | for _, e := range extra { 90 | fmt.Fprintf(w, `%s
`, strings.TrimPrefix(e.Path, "/"), e.Name) 91 | } 92 | w.Write([]byte(` 93 |
94 |
95 |
96 | `)) 97 | }) 98 | pp.Handle("/", index) 99 | pp.Handle("/debug", index) 100 | pp.Handle("/vars", http.HandlerFunc(expvarHandler)) 101 | pp.Handle("/gc", http.HandlerFunc(gcHandler)) 102 | pp.Handle("/freeosmemory", http.HandlerFunc(freeOSMemoryHandler)) 103 | pp.Handle("/debug/pprof/", http.HandlerFunc(pprof.Index)) 104 | pp.Handle("/debug/pprof/cmdline", http.HandlerFunc(pprof.Cmdline)) 105 | pp.Handle("/debug/pprof/profile", http.HandlerFunc(pprof.Profile)) 106 | pp.Handle("/debug/pprof/symbol", http.HandlerFunc(pprof.Symbol)) 107 | pp.Handle("/debug/pprof/trace", http.HandlerFunc(pprof.Trace)) 108 | pp.Handle("/debug/requests", http.HandlerFunc(trace.Traces)) 109 | pp.Handle("/debug/events", http.HandlerFunc(trace.Events)) 110 | pp.Handle("/metrics", promhttp.Handler()) 111 | for _, e := range extra { 112 | pp.Handle(e.Path, e.Handler) 113 | } 114 | log.Println("warning: could not start debug HTTP server:", http.ListenAndServe(addr, pp)) 115 | } 116 | -------------------------------------------------------------------------------- /langserver/build_context.go: -------------------------------------------------------------------------------- 1 | package langserver 2 | 3 | import ( 4 | "fmt" 5 | "go/build" 6 | "path" 7 | "path/filepath" 8 | "strings" 9 | 10 | "golang.org/x/net/context" 11 | 12 | "golang.org/x/tools/go/buildutil" 13 | 14 | "github.com/sourcegraph/go-langserver/langserver/util" 15 | ) 16 | 17 | // BuildContext creates a build.Context which uses the overlay FS and the InitializeParams.BuildContext overrides. 18 | func (h *LangHandler) BuildContext(ctx context.Context) *build.Context { 19 | var bctx *build.Context 20 | if override := h.init.BuildContext; override != nil { 21 | bctx = &build.Context{ 22 | GOOS: override.GOOS, 23 | GOARCH: override.GOARCH, 24 | GOPATH: override.GOPATH, 25 | GOROOT: override.GOROOT, 26 | CgoEnabled: override.CgoEnabled, 27 | UseAllFiles: override.UseAllFiles, 28 | Compiler: override.Compiler, 29 | BuildTags: override.BuildTags, 30 | 31 | // Enable analysis of all go version build tags that 32 | // our compiler should understand. 33 | ReleaseTags: build.Default.ReleaseTags, 34 | } 35 | } else { 36 | // make a copy since we will mutate it 37 | copy := build.Default 38 | bctx = © 39 | } 40 | 41 | h.Mu.Lock() 42 | fs := h.FS 43 | h.Mu.Unlock() 44 | 45 | util.PrepareContext(bctx, ctx, fs) 46 | return bctx 47 | } 48 | 49 | // ContainingPackage returns the package that contains the given 50 | // filename. It is like buildutil.ContainingPackage, except that: 51 | // 52 | // * it returns the whole package (i.e., it doesn't use build.FindOnly) 53 | // * it does not perform FS calls that are unnecessary for us (such 54 | // as searching the GOROOT; this is only called on the main 55 | // workspace's code, not its deps). 56 | // * if the file is in the xtest package (package p_test not package p), 57 | // it returns build.Package only representing that xtest package 58 | func ContainingPackage(bctx *build.Context, filename, rootPath string) (*build.Package, error) { 59 | gopaths := buildutil.SplitPathList(bctx, bctx.GOPATH) // list will be empty with no GOPATH 60 | for _, gopath := range gopaths { 61 | if !buildutil.IsAbsPath(bctx, gopath) { 62 | return nil, fmt.Errorf("build context GOPATH must be an absolute path (GOPATH=%q)", gopath) 63 | } 64 | } 65 | 66 | pkgDir := filename 67 | if !bctx.IsDir(filename) { 68 | pkgDir = path.Dir(filename) 69 | } 70 | 71 | var srcDir string 72 | if util.PathHasPrefix(filename, bctx.GOROOT) { 73 | srcDir = bctx.GOROOT // if workspace is Go stdlib 74 | } else { 75 | for _, gopath := range gopaths { 76 | if util.PathHasPrefix(pkgDir, gopath) { 77 | srcDir = gopath 78 | break 79 | } 80 | } 81 | } 82 | 83 | var ( 84 | pkg *build.Package 85 | err error 86 | xtest bool 87 | ) 88 | 89 | if srcDir == "" { 90 | // workspace is out of GOPATH 91 | pkg, err = bctx.ImportDir(pkgDir, 0) 92 | if pkg != nil { 93 | parts := strings.Split(util.PathTrimPrefix(pkgDir, filepath.Dir(rootPath)), "vendor/") 94 | pkg.ImportPath = parts[len(parts)-1] 95 | } 96 | } else { 97 | srcDir = path.Join(filepath.ToSlash(srcDir), "src") 98 | importPath := util.PathTrimPrefix(pkgDir, srcDir) 99 | pkg, err = bctx.Import(importPath, pkgDir, 0) 100 | } 101 | 102 | if pkg != nil { 103 | base := path.Base(filename) 104 | for _, f := range pkg.XTestGoFiles { 105 | if f == base { 106 | xtest = true 107 | break 108 | } 109 | } 110 | } 111 | 112 | // If the filename we want refers to a file in an xtest package 113 | // (package p_test not package p), then munge the package so that 114 | // it only refers to that xtest package. 115 | if pkg != nil && xtest && !strings.HasSuffix(pkg.Name, "_test") { 116 | pkg.Name += "_test" 117 | pkg.GoFiles = nil 118 | pkg.CgoFiles = nil 119 | pkg.TestGoFiles = nil 120 | } 121 | 122 | return pkg, err 123 | } 124 | -------------------------------------------------------------------------------- /langserver/handler_shared.go: -------------------------------------------------------------------------------- 1 | package langserver 2 | 3 | import ( 4 | "context" 5 | "go/build" 6 | "path/filepath" 7 | "sync" 8 | 9 | "github.com/sourcegraph/ctxvfs" 10 | 11 | "github.com/sourcegraph/go-langserver/langserver/util" 12 | ) 13 | 14 | // HandlerShared contains data structures that a build server and its 15 | // wrapped lang server may share in memory. 16 | type HandlerShared struct { 17 | Mu sync.Mutex // guards all fields 18 | Shared bool // true if this struct is shared with a build server 19 | FS *AtomicFS // full filesystem (mounts both deps and overlay) 20 | 21 | // FindPackage if non-nil is used by our typechecker. See 22 | // loader.Config.FindPackage. We use this in production to lazily 23 | // fetch dependencies + cache lookups. 24 | FindPackage FindPackageFunc 25 | 26 | overlay *overlay // files to overlay 27 | } 28 | 29 | // FindPackageFunc matches the signature of loader.Config.FindPackage, except 30 | // also takes a context.Context. 31 | type FindPackageFunc func(ctx context.Context, bctx *build.Context, importPath, fromDir, rootPath string, mode build.ImportMode) (*build.Package, error) 32 | 33 | func defaultFindPackageFunc(ctx context.Context, bctx *build.Context, importPath, fromDir, rootPath string, mode build.ImportMode) (*build.Package, error) { 34 | var ( 35 | res *build.Package 36 | err error 37 | dirInGOPATH bool 38 | ) 39 | 40 | if util.PathHasPrefix(rootPath, bctx.GOROOT) { 41 | dirInGOPATH = true 42 | } else { 43 | gopaths := filepath.SplitList(bctx.GOPATH) 44 | for _, gopath := range gopaths { 45 | if util.PathHasPrefix(rootPath, gopath) { 46 | dirInGOPATH = true 47 | break 48 | } 49 | } 50 | } 51 | 52 | res, err = bctx.Import(importPath, fromDir, mode) 53 | if err != nil && !dirInGOPATH { 54 | // Workspace is out of GOPATH, we have 2 fallback dirs: 55 | // 1. local package; 56 | // 2. project level vendored package; 57 | // Packages in go.mod file but not in vendor dir are not supported yet. :( 58 | fallBackDirs := make([]string, 0, 3) 59 | 60 | // Local imports always have same prefix -- the current dir's name. 61 | if util.PathHasPrefix(importPath, filepath.Base(rootPath)) { 62 | fallBackDirs = append(fallBackDirs, filepath.Join(filepath.Dir(rootPath), importPath)) 63 | } 64 | // Vendored package. 65 | fallBackDirs = append(fallBackDirs, filepath.Join(rootPath, "vendor", importPath)) 66 | if fromDir != rootPath && fromDir != "" { 67 | fallBackDirs = append(fallBackDirs, filepath.Join(fromDir, "vendor", importPath)) 68 | } 69 | 70 | // In case of import error, use ImportDir instead. 71 | // We must set ImportPath manually. 72 | for _, importDir := range fallBackDirs { 73 | res, err = bctx.ImportDir(importDir, mode) 74 | if res != nil { 75 | res.ImportPath = util.PathTrimPrefix(importDir, filepath.Dir(rootPath)) 76 | } 77 | if err == nil { 78 | break 79 | } 80 | if _, ok := err.(*build.NoGoError); ok { 81 | break 82 | } 83 | if _, ok := err.(*build.MultiplePackageError); ok { 84 | break 85 | } 86 | } 87 | } 88 | 89 | return res, err 90 | } 91 | 92 | // getFindPackageFunc is a helper which returns h.FindPackage if non-nil, otherwise defaultFindPackageFunc 93 | func (h *HandlerShared) getFindPackageFunc() FindPackageFunc { 94 | if h.FindPackage != nil { 95 | return h.FindPackage 96 | } 97 | return defaultFindPackageFunc 98 | } 99 | 100 | func (h *HandlerShared) Reset(useOSFS bool) error { 101 | h.Mu.Lock() 102 | defer h.Mu.Unlock() 103 | h.overlay = newOverlay() 104 | h.FS = NewAtomicFS() 105 | 106 | if useOSFS { 107 | // The overlay FS takes precedence, but we fall back to the OS 108 | // file system. 109 | h.FS.Bind("/", ctxvfs.OS("/"), "/", ctxvfs.BindAfter) 110 | } 111 | h.FS.Bind("/", h.overlay.FS(), "/", ctxvfs.BindBefore) 112 | return nil 113 | } 114 | -------------------------------------------------------------------------------- /langserver/internal/gocode/gbimporter/gbimporter.go: -------------------------------------------------------------------------------- 1 | package gbimporter 2 | 3 | import ( 4 | "fmt" 5 | "go/build" 6 | goimporter "go/importer" 7 | "go/types" 8 | "path/filepath" 9 | "strings" 10 | "sync" 11 | ) 12 | 13 | // We need to mangle go/build.Default to make gcimporter work as 14 | // intended, so use a lock to protect against concurrent accesses. 15 | var buildDefaultLock sync.Mutex 16 | 17 | var srcImporter types.ImporterFrom = goimporter.For("source", nil).(types.ImporterFrom) 18 | 19 | // importer implements types.ImporterFrom and provides transparent 20 | // support for gb-based projects. 21 | type importer struct { 22 | underlying types.ImporterFrom 23 | ctx *PackedContext 24 | gbroot string 25 | gbpaths []string 26 | } 27 | 28 | func New(ctx *PackedContext, filename string, underlying types.ImporterFrom) types.ImporterFrom { 29 | imp := &importer{ 30 | ctx: ctx, 31 | underlying: underlying, 32 | } 33 | 34 | slashed := filepath.ToSlash(filename) 35 | i := strings.LastIndex(slashed, "/vendor/src/") 36 | if i < 0 { 37 | i = strings.LastIndex(slashed, "/src/") 38 | } 39 | if i > 0 { 40 | paths := filepath.SplitList(imp.ctx.GOPATH) 41 | 42 | gbroot := filepath.FromSlash(slashed[:i]) 43 | gbvendor := filepath.Join(gbroot, "vendor") 44 | if samePath(gbroot, imp.ctx.GOROOT) { 45 | goto Found 46 | } 47 | for _, path := range paths { 48 | if samePath(path, gbroot) || samePath(path, gbvendor) { 49 | goto Found 50 | } 51 | } 52 | 53 | imp.gbroot = gbroot 54 | imp.gbpaths = append(paths, gbroot, gbvendor) 55 | Found: 56 | } 57 | 58 | return imp 59 | } 60 | 61 | func (i *importer) Import(path string) (*types.Package, error) { 62 | return i.ImportFrom(path, "", 0) 63 | } 64 | 65 | func (i *importer) ImportFrom(path, srcDir string, mode types.ImportMode) (*types.Package, error) { 66 | buildDefaultLock.Lock() 67 | defer buildDefaultLock.Unlock() 68 | 69 | origDef := build.Default 70 | defer func() { build.Default = origDef }() 71 | 72 | def := &build.Default 73 | def.GOARCH = i.ctx.GOARCH 74 | def.GOOS = i.ctx.GOOS 75 | def.GOROOT = i.ctx.GOROOT 76 | def.GOPATH = i.ctx.GOPATH 77 | def.CgoEnabled = i.ctx.CgoEnabled 78 | def.UseAllFiles = i.ctx.UseAllFiles 79 | def.Compiler = i.ctx.Compiler 80 | def.BuildTags = i.ctx.BuildTags 81 | def.ReleaseTags = i.ctx.ReleaseTags 82 | def.InstallSuffix = i.ctx.InstallSuffix 83 | 84 | def.SplitPathList = i.splitPathList 85 | def.JoinPath = i.joinPath 86 | 87 | pkg, err := i.underlying.ImportFrom(path, srcDir, mode) 88 | if pkg == nil { 89 | // If importing fails, try importing with source importer. 90 | pkg, _ = srcImporter.ImportFrom(path, srcDir, mode) 91 | } 92 | return pkg, err 93 | } 94 | 95 | func (i *importer) splitPathList(list string) []string { 96 | if i.gbroot != "" { 97 | return i.gbpaths 98 | } 99 | return filepath.SplitList(list) 100 | } 101 | 102 | func (i *importer) joinPath(elem ...string) string { 103 | res := filepath.Join(elem...) 104 | 105 | if i.gbroot != "" { 106 | // Want to rewrite "$GBROOT/(vendor/)?pkg/$GOOS_$GOARCH(_)?" 107 | // into "$GBROOT/pkg/$GOOS-$GOARCH(-)?". 108 | // Note: gb doesn't use vendor/pkg. 109 | if gbrel, err := filepath.Rel(i.gbroot, res); err == nil { 110 | gbrel = filepath.ToSlash(gbrel) 111 | gbrel, _ = match(gbrel, "vendor/") 112 | if gbrel, ok := match(gbrel, fmt.Sprintf("pkg/%s_%s", i.ctx.GOOS, i.ctx.GOARCH)); ok { 113 | gbrel, hasSuffix := match(gbrel, "_") 114 | 115 | // Reassemble into result. 116 | if hasSuffix { 117 | gbrel = "-" + gbrel 118 | } 119 | gbrel = fmt.Sprintf("pkg/%s_%s/", i.ctx.GOOS, i.ctx.GOARCH) + gbrel 120 | gbrel = filepath.FromSlash(gbrel) 121 | res = filepath.Join(i.gbroot, gbrel) 122 | } 123 | } 124 | } 125 | 126 | return res 127 | } 128 | 129 | func match(s, prefix string) (string, bool) { 130 | rest := strings.TrimPrefix(s, prefix) 131 | return rest, len(rest) < len(s) 132 | } 133 | -------------------------------------------------------------------------------- /langserver/config.go: -------------------------------------------------------------------------------- 1 | package langserver 2 | 3 | import ( 4 | "os" 5 | "runtime" 6 | ) 7 | 8 | var ( 9 | // GOLSP_WARMUP_ON_INITIALIZE toggles if we typecheck the whole 10 | // workspace in the background on initialize. This trades off initial 11 | // CPU and memory to hide perceived latency of the first few 12 | // requests. If the LSP server is long lived the tradeoff is usually 13 | // worth it. 14 | envWarmupOnInitialize = os.Getenv("GOLSP_WARMUP_ON_INITIALIZE") 15 | ) 16 | 17 | // Config adjusts the behaviour of go-langserver. Please keep in sync with 18 | // InitializationOptions in the README. 19 | type Config struct { 20 | // FuncSnippetEnabled enables the returning of argument snippets on `func` 21 | // completions, eg. func(foo string, arg2 bar). Requires code completion 22 | // to be enabled. 23 | // 24 | // Defaults to true if not specified. 25 | FuncSnippetEnabled bool 26 | 27 | // GocodeCompletionEnabled enables code completion feature (using gocode) 28 | // 29 | // Defaults to false if not specified. 30 | GocodeCompletionEnabled bool 31 | 32 | // FormatTool decides which tool is used to format documents. Supported: goimports and gofmt 33 | // 34 | // Defaults to goimports if not specified. 35 | FormatTool string 36 | 37 | // LintTool decides which tool is used for linting documents. Supported: golint and none 38 | // 39 | // Diagnostics must be enabled for linting to work. 40 | // 41 | // Defaults to none if not specified. 42 | LintTool string 43 | 44 | // GoimportsLocalPrefix sets the local prefix (comma-separated string) that goimports will use 45 | // 46 | // Defaults to empty string if not specified. 47 | GoimportsLocalPrefix string 48 | 49 | // DiagnosticsEnabled enables handling of diagnostics 50 | // 51 | // Defaults to false if not specified. 52 | DiagnosticsEnabled bool 53 | 54 | // MaxParallelism controls the maximum number of goroutines that should be used 55 | // to fulfill requests. This is useful in editor environments where users do 56 | // not want results ASAP, but rather just semi quickly without eating all of 57 | // their CPU. 58 | // 59 | // Defaults to half of your CPU cores if not specified. 60 | MaxParallelism int 61 | 62 | // UseBinaryPkgCache controls whether or not $GOPATH/pkg binary .a files should 63 | // be used. 64 | // 65 | // Defaults to true if not specified. 66 | UseBinaryPkgCache bool 67 | } 68 | 69 | // Apply sets the corresponding field in c for each non-nil field in o. 70 | func (c Config) Apply(o *InitializationOptions) Config { 71 | if o == nil { 72 | return c 73 | } 74 | if o.FuncSnippetEnabled != nil { 75 | c.FuncSnippetEnabled = *o.FuncSnippetEnabled 76 | } 77 | if o.GocodeCompletionEnabled != nil { 78 | c.GocodeCompletionEnabled = *o.GocodeCompletionEnabled 79 | } 80 | if o.FormatTool != nil { 81 | c.FormatTool = *o.FormatTool 82 | } 83 | if o.LintTool != nil { 84 | c.LintTool = *o.LintTool 85 | } 86 | if o.GoimportsLocalPrefix != nil { 87 | c.GoimportsLocalPrefix = *o.GoimportsLocalPrefix 88 | } 89 | if o.MaxParallelism != nil { 90 | c.MaxParallelism = *o.MaxParallelism 91 | } 92 | if o.UseBinaryPkgCache != nil { 93 | c.UseBinaryPkgCache = *o.UseBinaryPkgCache 94 | } 95 | if o.DiagnosticsEnabled != nil { 96 | c.DiagnosticsEnabled = *o.DiagnosticsEnabled 97 | } 98 | return c 99 | } 100 | 101 | // NewDefaultConfig returns the default config. See the field comments for the 102 | // defaults. 103 | func NewDefaultConfig() Config { 104 | // Default max parallelism to half the CPU cores, but at least always one. 105 | maxparallelism := runtime.NumCPU() / 2 106 | if maxparallelism <= 0 { 107 | maxparallelism = 1 108 | } 109 | 110 | return Config{ 111 | FuncSnippetEnabled: true, 112 | GocodeCompletionEnabled: false, 113 | FormatTool: formatToolGoimports, 114 | LintTool: lintToolNone, 115 | DiagnosticsEnabled: false, 116 | MaxParallelism: maxparallelism, 117 | UseBinaryPkgCache: true, 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /langserver/tracing.go: -------------------------------------------------------------------------------- 1 | package langserver 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "log" 7 | "sync" 8 | 9 | basictracer "github.com/opentracing/basictracer-go" 10 | opentracing "github.com/opentracing/opentracing-go" 11 | "github.com/opentracing/opentracing-go/ext" 12 | "github.com/sourcegraph/jsonrpc2" 13 | ) 14 | 15 | // InitTracer initializes the tracer for the connection if it has not 16 | // already been initialized. 17 | // 18 | // It assumes that h is only ever called for this conn. 19 | func (h *HandlerCommon) InitTracer(conn *jsonrpc2.Conn) { 20 | h.mu.Lock() 21 | defer h.mu.Unlock() 22 | if h.tracer != nil { 23 | return 24 | } 25 | 26 | if _, isNoopTracer := opentracing.GlobalTracer().(opentracing.NoopTracer); !isNoopTracer { 27 | // We have configured a tracer, use that instead of telemetry/event 28 | h.tracer = opentracing.GlobalTracer() 29 | return 30 | } 31 | 32 | t := tracer{conn: conn} 33 | opt := basictracer.DefaultOptions() 34 | opt.Recorder = &t 35 | h.tracer = basictracer.NewWithOptions(opt) 36 | go func() { 37 | <-conn.DisconnectNotify() 38 | t.mu.Lock() 39 | t.conn = nil 40 | t.mu.Unlock() 41 | }() 42 | } 43 | 44 | func (h *HandlerCommon) SpanForRequest(ctx context.Context, buildOrLang string, req *jsonrpc2.Request, tags opentracing.Tags) (opentracing.Span, context.Context, error) { 45 | opName := "LSP " + buildOrLang + " server: " + req.Method 46 | var span opentracing.Span 47 | 48 | // The parent span context can come from a few sources, depending 49 | // on how we're running this server and whether we are a build or 50 | // (wrapped) lang server. 51 | 52 | if span == nil && req.Meta != nil { 53 | // Try to get our parent span context from the JSON-RPC request from the LSP proxy. 54 | var carrier opentracing.TextMapCarrier 55 | if req.Meta != nil { 56 | if err := json.Unmarshal(*req.Meta, &carrier); err != nil { 57 | return nil, nil, err 58 | } 59 | } 60 | if clientCtx, err := h.tracer.Extract(opentracing.TextMap, carrier); err == nil { 61 | span = h.tracer.StartSpan(opName, ext.RPCServerOption(clientCtx), tags) 62 | } else if err != opentracing.ErrSpanContextNotFound { 63 | return nil, nil, err 64 | } 65 | } 66 | 67 | // Try to get our parent span context from the ctx. If this 68 | // succeeds, it means we're a language server being wrapped by a 69 | // build server, and the parent span is the build server's. 70 | if span == nil { 71 | if parentSpan := opentracing.SpanFromContext(ctx); parentSpan != nil { 72 | span = parentSpan.Tracer().StartSpan(opName, tags, opentracing.ChildOf(parentSpan.Context())) 73 | } 74 | } 75 | 76 | if span == nil { 77 | // No opentracing context from our JSON-RPC peer, so we need to create our own. 78 | span = opentracing.StartSpan(opName, tags) 79 | } 80 | 81 | if !isFileSystemRequest(req.Method) && req.Params != nil { 82 | span.SetTag("params", string(*req.Params)) 83 | } 84 | 85 | return span, opentracing.ContextWithSpan(ctx, span), nil 86 | } 87 | 88 | type tracer struct { 89 | mu sync.Mutex 90 | conn *jsonrpc2.Conn 91 | } 92 | 93 | func (t *tracer) RecordSpan(span basictracer.RawSpan) { 94 | t.mu.Lock() 95 | if t.conn == nil { 96 | t.mu.Unlock() 97 | return 98 | } 99 | t.mu.Unlock() 100 | 101 | ctx := context.Background() 102 | if err := t.conn.Notify(ctx, "telemetry/event", span); err != nil { 103 | log.Println("Error sending LSP telemetry/event notification:", err) 104 | } 105 | } 106 | 107 | // FollowsFrom means the parent span does not depend on the child span, but 108 | // caused it to start. 109 | func startSpanFollowsFromContext(ctx context.Context, operationName string, opts ...opentracing.StartSpanOption) opentracing.Span { 110 | if parentSpan := opentracing.SpanFromContext(ctx); parentSpan != nil { 111 | opts = append(opts, opentracing.FollowsFrom(parentSpan.Context())) 112 | return parentSpan.Tracer().StartSpan(operationName, opts...) 113 | } 114 | return opentracing.GlobalTracer().StartSpan(operationName, opts...) 115 | } 116 | -------------------------------------------------------------------------------- /langserver/util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/url" 7 | "path/filepath" 8 | "regexp" 9 | "runtime" 10 | "strings" 11 | 12 | "github.com/sourcegraph/go-lsp" 13 | ) 14 | 15 | func trimFilePrefix(s string) string { 16 | return strings.TrimPrefix(s, "file://") 17 | } 18 | 19 | func normalizePath(s string) string { 20 | if isURI(s) { 21 | return UriToPath(lsp.DocumentURI(s)) 22 | } 23 | s = filepath.ToSlash(s) 24 | if !strings.HasPrefix(s, "/") { 25 | s = "/" + s 26 | } 27 | return s 28 | } 29 | 30 | // PathHasPrefix returns true if s is starts with the given prefix 31 | func PathHasPrefix(s, prefix string) bool { 32 | s = normalizePath(s) 33 | prefix = normalizePath(prefix) 34 | if s == prefix { 35 | return true 36 | } 37 | if !strings.HasSuffix(prefix, "/") { 38 | prefix += "/" 39 | } 40 | return s == prefix || strings.HasPrefix(s, prefix) 41 | } 42 | 43 | // PathTrimPrefix removes the prefix from s 44 | func PathTrimPrefix(s, prefix string) string { 45 | s = normalizePath(s) 46 | prefix = normalizePath(prefix) 47 | if s == prefix { 48 | return "" 49 | } 50 | if !strings.HasSuffix(prefix, "/") { 51 | prefix += "/" 52 | } 53 | return strings.TrimPrefix(s, prefix) 54 | } 55 | 56 | // PathEqual returns true if both a and b are equal 57 | func PathEqual(a, b string) bool { 58 | return PathTrimPrefix(a, b) == "" 59 | } 60 | 61 | // IsVendorDir tells if the specified directory is a vendor directory. 62 | func IsVendorDir(dir string) bool { 63 | return strings.HasPrefix(dir, "vendor/") || strings.Contains(dir, "/vendor/") 64 | } 65 | 66 | // IsURI tells if s denotes an URI 67 | func IsURI(s lsp.DocumentURI) bool { 68 | return isURI(string(s)) 69 | } 70 | 71 | func isURI(s string) bool { 72 | return strings.HasPrefix(s, "file://") 73 | } 74 | 75 | // PathToURI converts given absolute path to file URI 76 | func PathToURI(path string) lsp.DocumentURI { 77 | path = filepath.ToSlash(path) 78 | parts := strings.SplitN(path, "/", 2) 79 | 80 | // If the first segment is a Windows drive letter, prefix with a slash and skip encoding 81 | head := parts[0] 82 | if head != "" { 83 | head = "/" + head 84 | } 85 | 86 | rest := "" 87 | if len(parts) > 1 { 88 | rest = "/" + parts[1] 89 | } 90 | 91 | return lsp.DocumentURI("file://" + head + rest) 92 | } 93 | 94 | // UriToPath converts given file URI to path 95 | func UriToPath(uri lsp.DocumentURI) string { 96 | u, err := url.Parse(string(uri)) 97 | if err != nil { 98 | return trimFilePrefix(string(uri)) 99 | } 100 | return u.Path 101 | } 102 | 103 | var regDriveLetter = regexp.MustCompile("^/[a-zA-Z]:") 104 | 105 | // UriToRealPath converts the given file URI to the platform specific path 106 | func UriToRealPath(uri lsp.DocumentURI) string { 107 | path := UriToPath(uri) 108 | 109 | if regDriveLetter.MatchString(path) { 110 | // remove the leading slash if it starts with a drive letter 111 | // and convert to back slashes 112 | path = filepath.FromSlash(path[1:]) 113 | } 114 | 115 | return path 116 | } 117 | 118 | // IsAbs returns true if the given path is absolute 119 | func IsAbs(path string) bool { 120 | // Windows implementation accepts path-like and filepath-like arguments 121 | return strings.HasPrefix(path, "/") || filepath.IsAbs(path) 122 | } 123 | 124 | // Panicf takes the return value of recover() and outputs data to the log with 125 | // the stack trace appended. Arguments are handled in the manner of 126 | // fmt.Printf. Arguments should format to a string which identifies what the 127 | // panic code was doing. Returns a non-nil error if it recovered from a panic. 128 | func Panicf(r interface{}, format string, v ...interface{}) error { 129 | if r != nil { 130 | // Same as net/http 131 | const size = 64 << 10 132 | buf := make([]byte, size) 133 | buf = buf[:runtime.Stack(buf, false)] 134 | id := fmt.Sprintf(format, v...) 135 | log.Printf("panic serving %s: %v\n%s", id, r, string(buf)) 136 | return fmt.Errorf("unexpected panic: %v", r) 137 | } 138 | return nil 139 | } 140 | -------------------------------------------------------------------------------- /langserver/format.go: -------------------------------------------------------------------------------- 1 | package langserver 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "go/ast" 8 | "go/parser" 9 | "go/printer" 10 | "go/token" 11 | "path" 12 | "strings" 13 | 14 | "github.com/pmezard/go-difflib/difflib" 15 | "golang.org/x/tools/go/buildutil" 16 | "golang.org/x/tools/imports" 17 | 18 | "github.com/sourcegraph/go-langserver/langserver/util" 19 | "github.com/sourcegraph/go-lsp" 20 | "github.com/sourcegraph/jsonrpc2" 21 | ) 22 | 23 | const ( 24 | formatToolGoimports string = "goimports" 25 | formatToolGofmt string = "gofmt" 26 | ) 27 | 28 | func (h *LangHandler) handleTextDocumentFormatting(ctx context.Context, conn jsonrpc2.JSONRPC2, req *jsonrpc2.Request, params lsp.DocumentFormattingParams) ([]lsp.TextEdit, error) { 29 | if !util.IsURI(params.TextDocument.URI) { 30 | return nil, &jsonrpc2.Error{ 31 | Code: jsonrpc2.CodeInvalidParams, 32 | Message: fmt.Sprintf("%s not yet supported for out-of-workspace URI (%q)", req.Method, params.TextDocument.URI), 33 | } 34 | } 35 | 36 | filename := h.FilePath(params.TextDocument.URI) 37 | unformatted, err := h.readFile(ctx, params.TextDocument.URI) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | var formatted []byte 43 | switch h.config.FormatTool { 44 | case formatToolGofmt: 45 | bctx := h.BuildContext(ctx) 46 | fset := token.NewFileSet() 47 | file, err := buildutil.ParseFile(fset, bctx, nil, path.Dir(filename), path.Base(filename), parser.ParseComments) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | ast.SortImports(fset, file) 53 | 54 | var buf bytes.Buffer 55 | cfg := printer.Config{Mode: printer.UseSpaces | printer.TabIndent, Tabwidth: 8} 56 | err = cfg.Fprint(&buf, fset, file) 57 | if err != nil { 58 | return nil, err 59 | } 60 | formatted = buf.Bytes() 61 | default: // goimports 62 | imports.LocalPrefix = h.config.GoimportsLocalPrefix 63 | var err error 64 | formatted, err = imports.Process(filename, unformatted, nil) 65 | if err != nil { 66 | return nil, err 67 | } 68 | } 69 | 70 | if bytes.Equal(formatted, unformatted) { 71 | return nil, nil 72 | } 73 | 74 | return ComputeTextEdits(string(unformatted), string(formatted)), nil 75 | } 76 | 77 | // ComputeTextEdits computes text edits that are required to 78 | // change the `unformatted` to the `formatted` text. 79 | func ComputeTextEdits(unformatted string, formatted string) []lsp.TextEdit { 80 | // LSP wants a list of TextEdits. We use difflib to compute a 81 | // non-naive TextEdit. Originally we returned an edit which deleted 82 | // everything followed by inserting everything. This leads to a poor 83 | // experience in vscode. 84 | unformattedLines := strings.Split(unformatted, "\n") 85 | formattedLines := strings.Split(formatted, "\n") 86 | m := difflib.NewMatcher(unformattedLines, formattedLines) 87 | var edits []lsp.TextEdit 88 | for _, op := range m.GetOpCodes() { 89 | switch op.Tag { 90 | case 'r': // 'r' (replace): a[i1:i2] should be replaced by b[j1:j2] 91 | edits = append(edits, lsp.TextEdit{ 92 | Range: lsp.Range{ 93 | Start: lsp.Position{ 94 | Line: op.I1, 95 | }, 96 | End: lsp.Position{ 97 | Line: op.I2, 98 | }, 99 | }, 100 | NewText: strings.Join(formattedLines[op.J1:op.J2], "\n") + "\n", 101 | }) 102 | case 'd': // 'd' (delete): a[i1:i2] should be deleted, j1==j2 in this case. 103 | edits = append(edits, lsp.TextEdit{ 104 | Range: lsp.Range{ 105 | Start: lsp.Position{ 106 | Line: op.I1, 107 | }, 108 | End: lsp.Position{ 109 | Line: op.I2, 110 | }, 111 | }, 112 | }) 113 | case 'i': // 'i' (insert): b[j1:j2] should be inserted at a[i1:i1], i1==i2 in this case. 114 | edits = append(edits, lsp.TextEdit{ 115 | Range: lsp.Range{ 116 | Start: lsp.Position{ 117 | Line: op.I1, 118 | }, 119 | End: lsp.Position{ 120 | Line: op.I1, 121 | }, 122 | }, 123 | NewText: strings.Join(formattedLines[op.J1:op.J2], "\n") + "\n", 124 | }) 125 | } 126 | } 127 | 128 | return edits 129 | } 130 | -------------------------------------------------------------------------------- /langserver/internal/gocode/lookdot/lookdot.go: -------------------------------------------------------------------------------- 1 | package lookdot 2 | 3 | import "go/types" 4 | 5 | type Visitor func(obj types.Object) 6 | 7 | func Walk(tv *types.TypeAndValue, v Visitor) bool { 8 | switch { 9 | case tv.IsType(): 10 | walk(tv.Type, false, false, v) 11 | case tv.IsValue(): 12 | walk(tv.Type, tv.Addressable(), true, v) 13 | default: 14 | return false 15 | } 16 | return true 17 | } 18 | 19 | func walk(typ0 types.Type, addable0, value bool, v Visitor) { 20 | // Enumerating valid selector expression identifiers is 21 | // surprisingly nuanced. 22 | 23 | // found is a map from selector identifiers to the objects 24 | // they select. Nil entries are used to track objects that 25 | // have already been reported to the visitor and to indicate 26 | // ambiguous identifiers. 27 | found := make(map[string]types.Object) 28 | 29 | addObj := func(obj types.Object, valid bool) { 30 | id := obj.Id() 31 | switch otherObj, isPresent := found[id]; { 32 | case !isPresent: 33 | if valid { 34 | found[id] = obj 35 | } else { 36 | found[id] = nil 37 | } 38 | case otherObj != nil: 39 | // Ambiguous selector. 40 | found[id] = nil 41 | } 42 | } 43 | 44 | // visited keeps track of named types that we've already 45 | // visited. We only need to track named types, because 46 | // recursion can only happen through embedded struct fields, 47 | // which must be either a named type or a pointer to a named 48 | // type. 49 | visited := make(map[*types.Named]bool) 50 | 51 | type todo struct { 52 | typ types.Type 53 | addable bool 54 | } 55 | 56 | var cur, next []todo 57 | cur = []todo{{typ0, addable0}} 58 | 59 | for { 60 | if len(cur) == 0 { 61 | // Flush discovered objects to visitor function. 62 | for id, obj := range found { 63 | if obj != nil { 64 | v(obj) 65 | found[id] = nil 66 | } 67 | } 68 | 69 | // Move unvisited types from next to cur. 70 | // It's important to check between levels to 71 | // ensure that ambiguous selections are 72 | // correctly handled. 73 | cur = next[:0] 74 | for _, t := range next { 75 | nt := namedOf(t.typ) 76 | if nt == nil { 77 | panic("embedded struct field without name?") 78 | } 79 | if !visited[nt] { 80 | cur = append(cur, t) 81 | } 82 | } 83 | next = nil 84 | 85 | if len(cur) == 0 { 86 | break 87 | } 88 | } 89 | 90 | now := cur[0] 91 | cur = cur[1:] 92 | 93 | // Look for methods declared on a named type. 94 | { 95 | typ, addable := chasePointer(now.typ) 96 | if !addable { 97 | addable = now.addable 98 | } 99 | if typ, ok := typ.(*types.Named); ok { 100 | visited[typ] = true 101 | for i, n := 0, typ.NumMethods(); i < n; i++ { 102 | m := typ.Method(i) 103 | addObj(m, addable || !hasPtrRecv(m)) 104 | } 105 | } 106 | } 107 | 108 | // Look for interface methods. 109 | if typ, ok := now.typ.Underlying().(*types.Interface); ok { 110 | for i, n := 0, typ.NumMethods(); i < n; i++ { 111 | addObj(typ.Method(i), true) 112 | } 113 | } 114 | 115 | // Look for struct fields. 116 | { 117 | typ, addable := chasePointer(now.typ.Underlying()) 118 | if !addable { 119 | addable = now.addable 120 | } 121 | if typ, ok := typ.Underlying().(*types.Struct); ok { 122 | for i, n := 0, typ.NumFields(); i < n; i++ { 123 | f := typ.Field(i) 124 | addObj(f, value) 125 | if f.Anonymous() { 126 | next = append(next, todo{f.Type(), addable}) 127 | } 128 | } 129 | } 130 | } 131 | } 132 | } 133 | 134 | // namedOf returns the named type T when given T or *T. 135 | // Otherwise, it returns nil. 136 | func namedOf(typ types.Type) *types.Named { 137 | if ptr, isPtr := typ.(*types.Pointer); isPtr { 138 | typ = ptr.Elem() 139 | } 140 | res, _ := typ.(*types.Named) 141 | return res 142 | } 143 | 144 | func hasPtrRecv(m *types.Func) bool { 145 | _, ok := m.Type().(*types.Signature).Recv().Type().(*types.Pointer) 146 | return ok 147 | } 148 | 149 | func chasePointer(typ types.Type) (types.Type, bool) { 150 | if ptr, isPtr := typ.(*types.Pointer); isPtr { 151 | return ptr.Elem(), true 152 | } 153 | return typ, false 154 | } 155 | -------------------------------------------------------------------------------- /vfsutil/zip.go: -------------------------------------------------------------------------------- 1 | package vfsutil 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "net/url" 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | 14 | "golang.org/x/net/context/ctxhttp" 15 | 16 | "github.com/fhs/go-netrc/netrc" 17 | "github.com/pkg/errors" 18 | "github.com/sourcegraph/go-langserver/diskcache" 19 | 20 | opentracing "github.com/opentracing/opentracing-go" 21 | "github.com/opentracing/opentracing-go/ext" 22 | ) 23 | 24 | // NewZipVFS downloads a zip archive from a URL (or fetches from the local cache 25 | // on disk) and returns a new VFS backed by that zip archive. 26 | func NewZipVFS(ctx context.Context, url string, onFetchStart, onFetchFailed func(), evictOnClose bool) (*ArchiveFS, error) { 27 | request, err := http.NewRequest("HEAD", url, nil) 28 | if err != nil { 29 | return nil, errors.Wrapf(err, "failed to construct a new request with URL %s", url) 30 | } 31 | setAuthFromNetrc(request) 32 | response, err := ctxhttp.Do(ctx, nil, request) 33 | if err != nil { 34 | return nil, err 35 | } 36 | if response.StatusCode != http.StatusOK { 37 | return nil, fmt.Errorf("unable to fetch zip from %s (expected HTTP response code 200, but got %d)", url, response.StatusCode) 38 | } 39 | 40 | fetch := func(ctx context.Context) (ar *archiveReader, err error) { 41 | span, ctx := opentracing.StartSpanFromContext(ctx, "zip Fetch") 42 | ext.Component.Set(span, "zipvfs") 43 | span.SetTag("url", url) 44 | defer func() { 45 | if err != nil { 46 | ext.Error.Set(span, true) 47 | span.SetTag("err", err) 48 | } 49 | span.Finish() 50 | }() 51 | 52 | store := &diskcache.Store{ 53 | Dir: filepath.Join(ArchiveCacheDir, "zipvfs"), 54 | Component: "zipvfs", 55 | MaxCacheSizeBytes: MaxCacheSizeBytes, 56 | } 57 | 58 | ff, err := cachedFetch(ctx, withoutAuth(url), store, func(ctx context.Context) (io.ReadCloser, error) { 59 | onFetchStart() 60 | request, err := http.NewRequest("GET", url, nil) 61 | if err != nil { 62 | return nil, errors.Wrapf(err, "failed to construct a new request with URL %s", url) 63 | } 64 | request.Header.Add("Accept", "application/zip") 65 | setAuthFromNetrc(request) 66 | resp, err := ctxhttp.Do(ctx, nil, request) 67 | if err != nil { 68 | return nil, errors.Wrapf(err, "failed to fetch zip archive from %s", url) 69 | } 70 | if resp.StatusCode != http.StatusOK { 71 | resp.Body.Close() 72 | return nil, errors.Errorf("zip URL %s returned HTTP %d", url, resp.StatusCode) 73 | } 74 | return resp.Body, nil 75 | }) 76 | if err != nil { 77 | onFetchFailed() 78 | return nil, errors.Wrapf(err, "failed to fetch/write/open zip archive from %s", url) 79 | } 80 | f := ff.File 81 | 82 | zr, err := zipNewFileReader(f) 83 | if err != nil { 84 | f.Close() 85 | return nil, errors.Wrapf(err, "failed to read zip archive from %s", url) 86 | } 87 | 88 | if len(zr.File) == 0 { 89 | f.Close() 90 | return nil, errors.Errorf("zip archive from %s is empty", url) 91 | } 92 | 93 | return &archiveReader{ 94 | Reader: zr, 95 | Closer: f, 96 | StripTopLevelDir: true, 97 | Evicter: store, 98 | }, nil 99 | } 100 | 101 | return &ArchiveFS{fetch: fetch, EvictOnClose: evictOnClose}, nil 102 | } 103 | 104 | func setAuthFromNetrc(req *http.Request) { 105 | host := req.URL.Host 106 | if i := strings.Index(host, ":"); i != -1 { 107 | host = host[:i] 108 | } 109 | netrcFile := os.ExpandEnv("$HOME/.netrc") 110 | if _, err := os.Stat(netrcFile); os.IsNotExist(err) { 111 | return 112 | } 113 | machine, err := netrc.FindMachine(netrcFile, host) 114 | if err != nil || machine == nil { 115 | return 116 | } 117 | req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", machine.Login, machine.Password)))) 118 | } 119 | 120 | // Create a new URL that doesn't include the user:password (the access 121 | // token) so that the same repository at a revision for a different user 122 | // results in a cache hit. 123 | func withoutAuth(urlString string) string { 124 | u, err := url.Parse(urlString) 125 | if err != nil { 126 | return urlString 127 | } 128 | u.User = nil 129 | return u.String() 130 | } 131 | -------------------------------------------------------------------------------- /vfsutil/archive.go: -------------------------------------------------------------------------------- 1 | package vfsutil 2 | 3 | import ( 4 | "archive/zip" 5 | "context" 6 | "errors" 7 | "io" 8 | "os" 9 | "strings" 10 | "sync" 11 | 12 | "github.com/sourcegraph/ctxvfs" 13 | "golang.org/x/tools/godoc/vfs" 14 | "golang.org/x/tools/godoc/vfs/zipfs" 15 | ) 16 | 17 | // archiveReader is like zip.ReadCloser, but it allows us to use a custom 18 | // closer. 19 | type archiveReader struct { 20 | *zip.Reader 21 | io.Closer 22 | Evicter 23 | 24 | // StripTopLevelDir specifies whether or not to strip the top level 25 | // directory in the zip archive (e.g. GitHub archives always have 1 top 26 | // level directory "{repobasename}-{sha}/"). 27 | StripTopLevelDir bool 28 | 29 | // prefix is the name of the directory that was stripped from the archive 30 | // (or "" if nothing was stripped). 31 | prefix string 32 | } 33 | 34 | // ArchiveFS is a ctxvfs.FileSystem backed by an Archiver. 35 | type ArchiveFS struct { 36 | fetch func(context.Context) (*archiveReader, error) 37 | 38 | // EvictOnClose when true will evict the underlying archive from the 39 | // archive cache when closed. 40 | EvictOnClose bool 41 | 42 | once sync.Once 43 | err error // the error encountered during the fetch call (if any) 44 | ar *archiveReader 45 | fs vfs.FileSystem // the zipfs virtual file system 46 | 47 | // We have a mutex for closed to prevent Close and fetch racing. 48 | closedMu sync.Mutex 49 | closed bool 50 | } 51 | 52 | // fetchOrWait initiates the fetch if it has not yet 53 | // started. Otherwise it waits for it to finish. 54 | func (fs *ArchiveFS) fetchOrWait(ctx context.Context) error { 55 | fs.once.Do(func() { 56 | // If we have already closed, do not open new resources. If we 57 | // haven't closed, prevent closing while fetching by holding 58 | // the lock. 59 | fs.closedMu.Lock() 60 | defer fs.closedMu.Unlock() 61 | if fs.closed { 62 | fs.err = errors.New("closed") 63 | return 64 | } 65 | 66 | fs.ar, fs.err = fs.fetch(ctx) 67 | if fs.err == nil { 68 | fs.fs = zipfs.New(&zip.ReadCloser{Reader: *fs.ar.Reader}, "") 69 | if fs.ar.StripTopLevelDir { 70 | entries, err := fs.fs.ReadDir("/") 71 | if err == nil && len(entries) == 1 && entries[0].IsDir() { 72 | fs.ar.prefix = entries[0].Name() 73 | } 74 | } 75 | 76 | if fs.ar.prefix != "" { 77 | ns := vfs.NameSpace{} 78 | ns.Bind("/", fs.fs, "/"+fs.ar.prefix, vfs.BindReplace) 79 | fs.fs = ns 80 | } 81 | } 82 | }) 83 | return fs.err 84 | } 85 | 86 | func (fs *ArchiveFS) Open(ctx context.Context, name string) (ctxvfs.ReadSeekCloser, error) { 87 | if err := fs.fetchOrWait(ctx); err != nil { 88 | return nil, err 89 | } 90 | return fs.fs.Open(name) 91 | } 92 | 93 | func (fs *ArchiveFS) Lstat(ctx context.Context, path string) (os.FileInfo, error) { 94 | if err := fs.fetchOrWait(ctx); err != nil { 95 | return nil, err 96 | } 97 | return fs.fs.Lstat(path) 98 | } 99 | 100 | func (fs *ArchiveFS) Stat(ctx context.Context, path string) (os.FileInfo, error) { 101 | if err := fs.fetchOrWait(ctx); err != nil { 102 | return nil, err 103 | } 104 | return fs.fs.Stat(path) 105 | } 106 | 107 | func (fs *ArchiveFS) ReadDir(ctx context.Context, path string) ([]os.FileInfo, error) { 108 | if err := fs.fetchOrWait(ctx); err != nil { 109 | return nil, err 110 | } 111 | return fs.fs.ReadDir(path) 112 | } 113 | 114 | func (fs *ArchiveFS) ListAllFiles(ctx context.Context) ([]string, error) { 115 | if err := fs.fetchOrWait(ctx); err != nil { 116 | return nil, err 117 | } 118 | 119 | filenames := make([]string, 0, len(fs.ar.File)) 120 | for _, f := range fs.ar.File { 121 | if f.Mode().IsRegular() { 122 | filenames = append(filenames, strings.TrimPrefix(f.Name, fs.ar.prefix+"/")) 123 | } 124 | } 125 | return filenames, nil 126 | } 127 | 128 | func (fs *ArchiveFS) Close() error { 129 | fs.closedMu.Lock() 130 | defer fs.closedMu.Unlock() 131 | if fs.closed { 132 | return errors.New("already closed") 133 | } 134 | 135 | fs.closed = true 136 | if fs.ar != nil && fs.ar.Closer != nil { 137 | err := fs.ar.Close() 138 | if err != nil { 139 | return err 140 | } 141 | if fs.EvictOnClose && fs.ar.Evicter != nil { 142 | fs.ar.Evict() 143 | } 144 | } 145 | return nil 146 | } 147 | 148 | func (fs *ArchiveFS) String() string { return "ArchiveFS(" + fs.fs.String() + ")" } 149 | -------------------------------------------------------------------------------- /langserver/cache.go: -------------------------------------------------------------------------------- 1 | package langserver 2 | 3 | import ( 4 | "os" 5 | "strconv" 6 | "sync" 7 | "sync/atomic" 8 | 9 | lru "github.com/hashicorp/golang-lru" 10 | "github.com/prometheus/client_golang/prometheus" 11 | ) 12 | 13 | var ( 14 | // typecheckCache is a process level cache for storing typechecked 15 | // values. Do not directly use this, instead use newTypecheckCache() 16 | typecheckCache = newLRU("SRC_TYPECHECK_CACHE_SIZE", 10) 17 | 18 | // symbolCache is a process level cache for storing symbols found. Do 19 | // not directly use this, instead use newSymbolCache() 20 | symbolCache = newLRU("SRC_SYMBOL_CACHE_SIZE", 500) 21 | 22 | // cacheID is used to prevent key conflicts between different 23 | // LangHandlers in the same process. 24 | cacheID int64 25 | 26 | typecheckCacheSize = prometheus.NewGauge(prometheus.GaugeOpts{ 27 | Name: "golangserver_typecheck_cache_size", 28 | Help: "Number of items in the typecheck cache", 29 | }) 30 | typecheckCacheTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ 31 | Name: "golangserver_typecheck_cache_request_total", 32 | Help: "Count of requests to cache.", 33 | }, []string{"type"}) 34 | symbolCacheSize = prometheus.NewGauge(prometheus.GaugeOpts{ 35 | Name: "golangserver_symbol_cache_size", 36 | Help: "Number of items in the symbol cache", 37 | }) 38 | symbolCacheTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ 39 | Name: "golangserver_symbol_cache_request_total", 40 | Help: "Count of requests to cache.", 41 | }, []string{"type"}) 42 | ) 43 | 44 | func init() { 45 | prometheus.MustRegister(typecheckCacheSize) 46 | prometheus.MustRegister(typecheckCacheTotal) 47 | prometheus.MustRegister(symbolCacheSize) 48 | prometheus.MustRegister(symbolCacheTotal) 49 | } 50 | 51 | type cache interface { 52 | Get(key interface{}, fill func() interface{}) interface{} 53 | Purge() 54 | } 55 | 56 | func newTypecheckCache() *boundedCache { 57 | return &boundedCache{ 58 | id: nextCacheID(), 59 | c: typecheckCache, 60 | size: typecheckCacheSize, 61 | counter: typecheckCacheTotal, 62 | } 63 | } 64 | 65 | func newSymbolCache() *boundedCache { 66 | return &boundedCache{ 67 | id: nextCacheID(), 68 | c: symbolCache, 69 | size: symbolCacheSize, 70 | counter: symbolCacheTotal, 71 | } 72 | } 73 | 74 | type cacheKey struct { 75 | id int64 76 | k interface{} 77 | } 78 | 79 | type cacheValue struct { 80 | ready chan struct{} // closed to broadcast readiness 81 | value interface{} 82 | } 83 | 84 | type boundedCache struct { 85 | mu sync.Mutex 86 | id int64 87 | c *lru.Cache 88 | size prometheus.Gauge 89 | counter *prometheus.CounterVec 90 | } 91 | 92 | func (c *boundedCache) Get(k interface{}, fill func() interface{}) interface{} { 93 | // c.c is already thread safe, but we need c.mu so we can insert a 94 | // cacheValue only once if we have a miss. 95 | c.mu.Lock() 96 | key := cacheKey{c.id, k} 97 | var v *cacheValue 98 | if vi, ok := c.c.Get(key); ok { 99 | // cache hit, wait until ready 100 | c.mu.Unlock() 101 | c.counter.WithLabelValues("hit").Inc() 102 | v = vi.(*cacheValue) 103 | <-v.ready 104 | } else { 105 | // cache miss. Add unready result to cache and fill 106 | v = &cacheValue{ready: make(chan struct{})} 107 | c.c.Add(key, v) 108 | c.mu.Unlock() 109 | c.size.Set(float64(c.c.Len())) 110 | c.counter.WithLabelValues("miss").Inc() 111 | 112 | defer close(v.ready) 113 | v.value = fill() 114 | } 115 | 116 | return v.value 117 | } 118 | 119 | func (c *boundedCache) Purge() { 120 | // c.c is a process level cache. We could increment c.id to make it seem 121 | // like we've purged the cache, but that would leave the objects in memory 122 | // (and typechecking objects can be several hundreds of MB). So instead, 123 | // we manually remove the objects from cache. 124 | c.mu.Lock() 125 | for _, key := range c.c.Keys() { 126 | if k := key.(cacheKey); k.id == c.id { 127 | c.c.Remove(key) 128 | } 129 | } 130 | c.mu.Unlock() 131 | } 132 | 133 | // newLRU returns an LRU based cache. 134 | func newLRU(env string, defaultSize int) *lru.Cache { 135 | size := defaultSize 136 | if i, err := strconv.Atoi(os.Getenv(env)); err == nil && i > 0 { 137 | size = i 138 | } 139 | c, err := lru.New(size) 140 | if err != nil { 141 | // This should never happen since our size is always > 0 142 | panic(err) 143 | } 144 | return c 145 | } 146 | 147 | func nextCacheID() int64 { 148 | return atomic.AddInt64(&cacheID, 1) 149 | } 150 | -------------------------------------------------------------------------------- /langserver/completion.go: -------------------------------------------------------------------------------- 1 | package langserver 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "regexp" 7 | "strings" 8 | 9 | "github.com/sourcegraph/go-langserver/langserver/internal/gocode" 10 | "github.com/sourcegraph/go-langserver/langserver/internal/gocode/gbimporter" 11 | "github.com/sourcegraph/go-langserver/langserver/util" 12 | "github.com/sourcegraph/go-lsp" 13 | "github.com/sourcegraph/jsonrpc2" 14 | ) 15 | 16 | var ( 17 | CIKConstantSupported = lsp.CIKVariable // or lsp.CIKConstant if client supported 18 | funcArgsRegexp = regexp.MustCompile(`func\(([^)]+)\)`) 19 | ) 20 | 21 | func (h *LangHandler) handleTextDocumentCompletion(ctx context.Context, conn jsonrpc2.JSONRPC2, req *jsonrpc2.Request, params lsp.CompletionParams) (*lsp.CompletionList, error) { 22 | if !util.IsURI(params.TextDocument.URI) { 23 | return nil, &jsonrpc2.Error{ 24 | Code: jsonrpc2.CodeInvalidParams, 25 | Message: fmt.Sprintf("textDocument/completion not yet supported for out-of-workspace URI (%q)", params.TextDocument.URI), 26 | } 27 | } 28 | 29 | // In the case of testing, our OS paths and VFS paths do not match. In the 30 | // real world, this is never the case. Give the test suite the opportunity 31 | // to correct the path now. 32 | vfsURI := params.TextDocument.URI 33 | if testOSToVFSPath != nil { 34 | vfsURI = util.PathToURI(testOSToVFSPath(util.UriToPath(vfsURI))) 35 | } 36 | 37 | // Read file contents and calculate byte offset. 38 | contents, err := h.readFile(ctx, vfsURI) 39 | if err != nil { 40 | return nil, err 41 | } 42 | // convert the path into a real path because 3rd party tools 43 | // might load additional code based on the file's package 44 | filename := util.UriToRealPath(params.TextDocument.URI) 45 | offset, valid, why := offsetForPosition(contents, params.Position) 46 | if !valid { 47 | return nil, fmt.Errorf("invalid position: %s:%d:%d (%s)", filename, params.Position.Line, params.Position.Character, why) 48 | } 49 | 50 | ac, err := gocode.AutoComplete(&gocode.AutoCompleteRequest{ 51 | Filename: filename, 52 | Data: contents, 53 | Cursor: offset, 54 | Builtin: true, 55 | Source: !h.config.UseBinaryPkgCache, 56 | Context: gbimporter.PackContext(h.BuildContext(ctx)), 57 | }) 58 | if err != nil { 59 | return nil, fmt.Errorf("could not autocomplete %s: %v", filename, err) 60 | } 61 | citems := make([]lsp.CompletionItem, len(ac.Candidates)) 62 | for i, it := range ac.Candidates { 63 | var kind lsp.CompletionItemKind 64 | switch it.Class { 65 | case "const": 66 | kind = CIKConstantSupported 67 | case "func": 68 | kind = lsp.CIKFunction 69 | case "import": 70 | kind = lsp.CIKModule 71 | case "package": 72 | kind = lsp.CIKModule 73 | case "type": 74 | kind = lsp.CIKClass 75 | case "var": 76 | kind = lsp.CIKVariable 77 | } 78 | 79 | itf, newText := h.getNewText(kind, it.Name, it.Type) 80 | citems[i] = lsp.CompletionItem{ 81 | Label: it.Name, 82 | Kind: kind, 83 | Detail: it.Type, 84 | InsertTextFormat: itf, 85 | // InsertText is deprecated in favour of TextEdit, but added here for legacy client support 86 | InsertText: newText, 87 | TextEdit: &lsp.TextEdit{ 88 | Range: lsp.Range{ 89 | Start: lsp.Position{Line: params.Position.Line, Character: params.Position.Character - ac.Len}, 90 | End: lsp.Position{Line: params.Position.Line, Character: params.Position.Character}, 91 | }, 92 | NewText: newText, 93 | }, 94 | } 95 | } 96 | return &lsp.CompletionList{ 97 | IsIncomplete: false, 98 | Items: citems, 99 | }, nil 100 | } 101 | 102 | func (h *LangHandler) getNewText(kind lsp.CompletionItemKind, name, detail string) (lsp.InsertTextFormat, string) { 103 | if h.config.FuncSnippetEnabled && 104 | kind == lsp.CIKFunction && 105 | h.init.Capabilities.TextDocument.Completion.CompletionItem.SnippetSupport { 106 | args := genSnippetArgs(parseFuncArgs(detail)) 107 | text := fmt.Sprintf("%s(%s)$0", name, strings.Join(args, ", ")) 108 | return lsp.ITFSnippet, text 109 | } 110 | return lsp.ITFPlainText, name 111 | } 112 | 113 | func parseFuncArgs(def string) []string { 114 | m := funcArgsRegexp.FindStringSubmatch(def) 115 | var args []string 116 | if len(m) > 1 { 117 | args = strings.Split(m[1], ", ") 118 | } 119 | return args 120 | } 121 | 122 | func genSnippetArgs(args []string) []string { 123 | newArgs := make([]string, len(args)) 124 | for i, a := range args { 125 | // Closing curly braces must be escaped 126 | a = strings.Replace(a, "}", "\\}", -1) 127 | newArgs[i] = fmt.Sprintf("${%d:%s}", i+1, a) 128 | } 129 | return newArgs 130 | } 131 | -------------------------------------------------------------------------------- /langserver/handler_shared_test.go: -------------------------------------------------------------------------------- 1 | package langserver 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "golang.org/x/tools/go/buildutil" 8 | ) 9 | 10 | type testCase struct { 11 | ImportPath string 12 | FromDir string 13 | WantPackageName string 14 | WantImportPath string 15 | } 16 | 17 | func TestDefaultFindPackageFuncInGOPATH(t *testing.T) { 18 | bctx := buildutil.FakeContext(map[string]map[string]string{ 19 | // local packages 20 | "/gopath/src/test": { 21 | "main.go": "package main", 22 | }, 23 | "/gopath/src/test/foo": { 24 | "foo.go": "package foo", 25 | }, 26 | "/gopath/src/test/bar": { 27 | "bar.go": "package bar", 28 | }, 29 | // vendored packages 30 | "/gopath/src/test/vendor/baz": { 31 | "baz.go": "package baz", 32 | }, 33 | // stdlib packages 34 | "/goroot/src/strings": { 35 | "strings.go": "package strings", 36 | }, 37 | // other packages under gopath 38 | "/gopath/src/other": { 39 | "other.go": "package other", 40 | }, 41 | }) 42 | bctx.GOPATH = "/gopath" 43 | bctx.GOROOT = "/goroot" 44 | 45 | ctx := context.Background() 46 | 47 | tests := []testCase{ 48 | // local packages 49 | testCase{"test/foo", "", "foo", "test/foo"}, 50 | testCase{"test/foo", "/gopath/src/test", "foo", "test/foo"}, 51 | testCase{"test/bar", "/gopath/src/test", "bar", "test/bar"}, 52 | testCase{"test/bar", "/gopath/src/test/foo", "bar", "test/bar"}, 53 | // vendored packages, fakeContext now doesn't support vendor dir, :( 54 | // testCase{"baz", "/home/go/test", "baz", "test/vendor/baz"}, 55 | // testCase{"baz", "/home/go/test/foo", "baz", "test/vendor/baz"}, 56 | // stdlib packages 57 | testCase{"strings", "/gopath/src/test", "strings", "strings"}, 58 | testCase{"strings", "/gopath/src/test/foo", "strings", "strings"}, 59 | // other packages 60 | testCase{"other", "/gopath/src/test", "other", "other"}, 61 | testCase{"other", "/gopath/src/test/foo", "other", "other"}, 62 | } 63 | 64 | for _, test := range tests { 65 | pkg, err := defaultFindPackageFunc(ctx, bctx, test.ImportPath, test.FromDir, "/gopath/src/test", 0) 66 | if err != nil { 67 | t.Fatal(err) 68 | } 69 | if pkg.Name != test.WantPackageName { 70 | t.Errorf("import %s from %s: got pkg name %q, want %q", test.ImportPath, test.FromDir, pkg.Name, test.WantPackageName) 71 | } 72 | if pkg.ImportPath != test.WantImportPath { 73 | t.Errorf("import %s from %s: got import path %q, want %q", test.ImportPath, test.FromDir, pkg.ImportPath, test.WantImportPath) 74 | } 75 | } 76 | } 77 | 78 | func TestDefaultFindPackageFuncOutGOPATH(t *testing.T) { 79 | bctx := buildutil.FakeContext(map[string]map[string]string{ 80 | // local packages 81 | "/home/go/test": { 82 | "main.go": "package main", 83 | }, 84 | "/home/go/test/foo": { 85 | "foo.go": "package foo", 86 | }, 87 | "/home/go/test/bar": { 88 | "bar.go": "package bar", 89 | }, 90 | // vendored packages 91 | "/home/go/test/vendor/baz": { 92 | "baz.go": "package baz", 93 | }, 94 | // stdlib packages 95 | "/goroot/src/strings": { 96 | "strings.go": "package strings", 97 | }, 98 | // other packages under gopath 99 | "/gopath/src/other": { 100 | "other.go": "package other", 101 | }, 102 | }) 103 | bctx.GOPATH = "/gopath" 104 | bctx.GOROOT = "/goroot" 105 | 106 | ctx := context.Background() 107 | 108 | tests := []testCase{ 109 | // local packages 110 | testCase{"test/foo", "", "foo", "test/foo"}, 111 | testCase{"test/foo", "/home/go/test", "foo", "test/foo"}, 112 | testCase{"test/bar", "/home/go/test", "bar", "test/bar"}, 113 | testCase{"test/bar", "/home/go/test/foo", "bar", "test/bar"}, 114 | // vendored packages 115 | testCase{"baz", "/home/go/test", "baz", "test/vendor/baz"}, 116 | testCase{"baz", "/home/go/test/foo", "baz", "test/vendor/baz"}, 117 | // stdlib packages 118 | testCase{"strings", "/home/go/test", "strings", "strings"}, 119 | testCase{"strings", "/home/go/test/foo", "strings", "strings"}, 120 | // other packages 121 | testCase{"other", "/home/go/test", "other", "other"}, 122 | testCase{"other", "/home/go/test/foo", "other", "other"}, 123 | } 124 | 125 | for _, test := range tests { 126 | pkg, err := defaultFindPackageFunc(ctx, bctx, test.ImportPath, test.FromDir, "/home/go/test", 0) 127 | if err != nil { 128 | t.Fatal(err) 129 | } 130 | if pkg.Name != test.WantPackageName { 131 | t.Errorf("import %s from %s: got pkg name %q, want %q", test.ImportPath, test.FromDir, pkg.Name, test.WantPackageName) 132 | } 133 | if pkg.ImportPath != test.WantImportPath { 134 | t.Errorf("import %s from %s: got import path %q, want %q", test.ImportPath, test.FromDir, pkg.ImportPath, test.WantImportPath) 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /langserver/symbol_test.go: -------------------------------------------------------------------------------- 1 | package langserver 2 | 3 | import ( 4 | "reflect" 5 | "sort" 6 | "testing" 7 | 8 | "github.com/sourcegraph/go-lsp" 9 | ) 10 | 11 | func Test_resultSorter(t *testing.T) { 12 | type testcase struct { 13 | rawQuery string 14 | allSymbols []lsp.SymbolInformation 15 | expResults []lsp.SymbolInformation 16 | } 17 | tests := []testcase{{ 18 | rawQuery: "foo.bar", 19 | allSymbols: []lsp.SymbolInformation{{ 20 | ContainerName: "foo", Name: "bar", 21 | Location: lsp.Location{URI: "file:///file.go"}, 22 | Kind: lsp.SKFunction, 23 | }, { 24 | ContainerName: "foo", Name: "Bar", 25 | Location: lsp.Location{URI: "file:///file.go"}, 26 | Kind: lsp.SKFunction, 27 | }, { 28 | ContainerName: "asdf", Name: "foo", 29 | Location: lsp.Location{URI: "file:///file.go"}, 30 | Kind: lsp.SKFunction, 31 | }, { 32 | ContainerName: "asdf", Name: "asdf", 33 | Location: lsp.Location{URI: "file:///foo.go"}, 34 | Kind: lsp.SKFunction, 35 | }, { 36 | ContainerName: "one", Name: "two", 37 | Location: lsp.Location{URI: "file:///file.go"}, 38 | Kind: lsp.SKFunction, 39 | }}, 40 | expResults: []lsp.SymbolInformation{{ 41 | ContainerName: "foo", Name: "Bar", 42 | Location: lsp.Location{URI: "file:///file.go"}, 43 | Kind: lsp.SKFunction, 44 | }, { 45 | ContainerName: "foo", Name: "bar", 46 | Location: lsp.Location{URI: "file:///file.go"}, 47 | Kind: lsp.SKFunction, 48 | }, { 49 | ContainerName: "asdf", Name: "foo", 50 | Location: lsp.Location{URI: "file:///file.go"}, 51 | Kind: lsp.SKFunction, 52 | }, { 53 | ContainerName: "asdf", Name: "asdf", 54 | Location: lsp.Location{URI: "file:///foo.go"}, 55 | Kind: lsp.SKFunction, 56 | }}, 57 | }, { 58 | rawQuery: "foo bar", 59 | allSymbols: []lsp.SymbolInformation{{ 60 | ContainerName: "foo", Name: "bar", 61 | Location: lsp.Location{URI: "file:///file.go"}, 62 | Kind: lsp.SKFunction, 63 | }, { 64 | ContainerName: "asdf", Name: "foo", 65 | Location: lsp.Location{URI: "file:///file.go"}, 66 | Kind: lsp.SKFunction, 67 | }, { 68 | ContainerName: "asdf", Name: "asdf", 69 | Location: lsp.Location{URI: "file:///foo.go"}, 70 | Kind: lsp.SKFunction, 71 | }, { 72 | ContainerName: "one", Name: "two", 73 | Location: lsp.Location{URI: "file:///file.go"}, 74 | Kind: lsp.SKFunction, 75 | }}, 76 | expResults: []lsp.SymbolInformation{{ 77 | ContainerName: "foo", Name: "bar", 78 | Location: lsp.Location{URI: "file:///file.go"}, 79 | Kind: lsp.SKFunction, 80 | }, { 81 | ContainerName: "asdf", Name: "foo", 82 | Location: lsp.Location{URI: "file:///file.go"}, 83 | Kind: lsp.SKFunction, 84 | }, { 85 | ContainerName: "asdf", Name: "asdf", 86 | Location: lsp.Location{URI: "file:///foo.go"}, 87 | Kind: lsp.SKFunction, 88 | }}, 89 | }, { 90 | // Just tests that 'is:exported' does not affect resultSorter 91 | // results, as filtering is done elsewhere in (*LangHandler).collectFromPkg 92 | rawQuery: "is:exported", 93 | allSymbols: []lsp.SymbolInformation{{ 94 | ContainerName: "foo", Name: "bar", 95 | Location: lsp.Location{URI: "file:///file.go"}, 96 | Kind: lsp.SKFunction, 97 | }}, 98 | expResults: []lsp.SymbolInformation{{ 99 | ContainerName: "foo", Name: "bar", 100 | Location: lsp.Location{URI: "file:///file.go"}, 101 | Kind: lsp.SKFunction, 102 | }}, 103 | }, { 104 | rawQuery: "", 105 | allSymbols: []lsp.SymbolInformation{{ 106 | ContainerName: "foo", Name: "bar", 107 | Location: lsp.Location{URI: "file:///file.go"}, 108 | Kind: lsp.SKFunction, 109 | }}, 110 | expResults: []lsp.SymbolInformation{{ 111 | ContainerName: "foo", Name: "bar", 112 | Location: lsp.Location{URI: "file:///file.go"}, 113 | Kind: lsp.SKFunction, 114 | }}, 115 | }} 116 | 117 | for _, test := range tests { 118 | results := resultSorter{Query: ParseQuery(test.rawQuery)} 119 | for _, s := range test.allSymbols { 120 | results.Collect(symbolPair{SymbolInformation: s}) 121 | } 122 | sort.Sort(&results) 123 | if !reflect.DeepEqual(results.Results(), test.expResults) { 124 | t.Errorf("got %+v, but wanted %+v", results.Results(), test.expResults) 125 | } 126 | } 127 | } 128 | 129 | func TestQueryString(t *testing.T) { 130 | tests := []struct { 131 | input, expect string 132 | }{ 133 | // Basic queries. 134 | {input: "foo bar", expect: "foo bar"}, 135 | {input: "func bar", expect: "func bar"}, 136 | {input: "is:exported", expect: "is:exported"}, 137 | {input: "dir:foo", expect: "dir:foo"}, 138 | {input: "is:exported bar", expect: "is:exported bar"}, 139 | {input: "dir:foo bar", expect: "dir:foo bar"}, 140 | {input: "is:exported bar baz", expect: "is:exported bar baz"}, 141 | {input: "dir:foo bar baz", expect: "dir:foo bar baz"}, 142 | 143 | // Test guarantee of byte-wise ordering (hint: we only guarantee logical 144 | // equivalence, not byte-wise equality). 145 | {input: "bar baz is:exported", expect: "is:exported bar baz"}, 146 | {input: "bar baz dir:foo", expect: "dir:foo bar baz"}, 147 | {input: "func baz dir:foo", expect: "dir:foo func baz"}, 148 | } 149 | for _, test := range tests { 150 | t.Run(test.input, func(t *testing.T) { 151 | got := ParseQuery(test.input).String() 152 | if got != test.expect { 153 | t.Errorf("got %q, expect %q", got, test.expect) 154 | } 155 | }) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /langserver/internal/gocode/suggest/candidate.go: -------------------------------------------------------------------------------- 1 | package suggest 2 | 3 | import ( 4 | "fmt" 5 | "go/types" 6 | "sort" 7 | "strings" 8 | ) 9 | 10 | type Candidate struct { 11 | Class string `json:"class"` 12 | PkgPath string `json:"package"` 13 | Name string `json:"name"` 14 | Type string `json:"type"` 15 | } 16 | 17 | func (c Candidate) Suggestion() string { 18 | switch { 19 | case c.Class != "func": 20 | return c.Name 21 | case strings.HasPrefix(c.Type, "func()"): 22 | return c.Name + "()" 23 | default: 24 | return c.Name + "(" 25 | } 26 | } 27 | 28 | func (c Candidate) String() string { 29 | if c.Class == "func" { 30 | return fmt.Sprintf("%s %s%s", c.Class, c.Name, strings.TrimPrefix(c.Type, "func")) 31 | } 32 | return fmt.Sprintf("%s %s %s", c.Class, c.Name, c.Type) 33 | } 34 | 35 | type candidatesByClassAndName []Candidate 36 | 37 | func (s candidatesByClassAndName) Len() int { return len(s) } 38 | func (s candidatesByClassAndName) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 39 | 40 | func (s candidatesByClassAndName) Less(i, j int) bool { 41 | if s[i].Class != s[j].Class { 42 | return s[i].Class < s[j].Class 43 | } 44 | return s[i].Name < s[j].Name 45 | } 46 | 47 | type objectFilter func(types.Object) bool 48 | 49 | var objectFilters = map[string]objectFilter{ 50 | "const": func(obj types.Object) bool { _, ok := obj.(*types.Const); return ok }, 51 | "func": func(obj types.Object) bool { _, ok := obj.(*types.Func); return ok }, 52 | "package": func(obj types.Object) bool { _, ok := obj.(*types.PkgName); return ok }, 53 | "type": func(obj types.Object) bool { _, ok := obj.(*types.TypeName); return ok }, 54 | "var": func(obj types.Object) bool { _, ok := obj.(*types.Var); return ok }, 55 | } 56 | 57 | func classifyObject(obj types.Object) string { 58 | switch obj.(type) { 59 | case *types.Builtin: 60 | return "func" 61 | case *types.Const: 62 | return "const" 63 | case *types.Func: 64 | return "func" 65 | case *types.Nil: 66 | return "const" 67 | case *types.PkgName: 68 | return "package" 69 | case *types.TypeName: 70 | return "type" 71 | case *types.Var: 72 | return "var" 73 | } 74 | panic(fmt.Sprintf("unhandled types.Object: %T", obj)) 75 | } 76 | 77 | type candidateCollector struct { 78 | exact []types.Object 79 | badcase []types.Object 80 | localpkg *types.Package 81 | partial string 82 | filter objectFilter 83 | builtin bool 84 | } 85 | 86 | func (b *candidateCollector) getCandidates() []Candidate { 87 | objs := b.exact 88 | if objs == nil { 89 | objs = b.badcase 90 | } 91 | 92 | var res []Candidate 93 | for _, obj := range objs { 94 | res = append(res, b.asCandidate(obj)) 95 | } 96 | sort.Sort(candidatesByClassAndName(res)) 97 | return res 98 | } 99 | 100 | func (b *candidateCollector) asCandidate(obj types.Object) Candidate { 101 | objClass := classifyObject(obj) 102 | var typ types.Type 103 | switch objClass { 104 | case "const", "func", "var": 105 | typ = obj.Type() 106 | case "type": 107 | typ = obj.Type().Underlying() 108 | } 109 | 110 | var typStr string 111 | switch t := typ.(type) { 112 | case *types.Interface: 113 | typStr = "interface" 114 | case *types.Struct: 115 | typStr = "struct" 116 | default: 117 | if _, isBuiltin := obj.(*types.Builtin); isBuiltin { 118 | typStr = builtinTypes[obj.Name()] 119 | } else if t != nil { 120 | typStr = types.TypeString(t, b.qualify) 121 | } 122 | } 123 | 124 | path := "builtin" 125 | if pkg := obj.Pkg(); pkg != nil { 126 | path = pkg.Path() 127 | } 128 | 129 | return Candidate{ 130 | Class: objClass, 131 | PkgPath: path, 132 | Name: obj.Name(), 133 | Type: typStr, 134 | } 135 | } 136 | 137 | var builtinTypes = map[string]string{ 138 | // Universe. 139 | "append": "func(slice []Type, elems ..Type) []Type", 140 | "cap": "func(v Type) int", 141 | "close": "func(c chan<- Type)", 142 | "complex": "func(real FloatType, imag FloatType) ComplexType", 143 | "copy": "func(dst []Type, src []Type) int", 144 | "delete": "func(m map[Key]Type, key Key)", 145 | "imag": "func(c ComplexType) FloatType", 146 | "len": "func(v Type) int", 147 | "make": "func(Type, size IntegerType) Type", 148 | "new": "func(Type) *Type", 149 | "panic": "func(v interface{})", 150 | "print": "func(args ...Type)", 151 | "println": "func(args ...Type)", 152 | "real": "func(c ComplexType) FloatType", 153 | "recover": "func() interface{}", 154 | 155 | // Package unsafe. 156 | "Alignof": "func(x Type) uintptr", 157 | "Sizeof": "func(x Type) uintptr", 158 | "Offsetof": "func(x Type) uintptr", 159 | } 160 | 161 | func (b *candidateCollector) qualify(pkg *types.Package) string { 162 | if pkg == b.localpkg { 163 | return "" 164 | } 165 | return pkg.Name() 166 | } 167 | 168 | func (b *candidateCollector) appendObject(obj types.Object) { 169 | if obj.Pkg() != b.localpkg { 170 | if obj.Parent() == types.Universe { 171 | if !b.builtin { 172 | return 173 | } 174 | } else if !obj.Exported() { 175 | return 176 | } 177 | } 178 | 179 | // TODO(mdempsky): Reconsider this functionality. 180 | if b.filter != nil && !b.filter(obj) { 181 | return 182 | } 183 | 184 | if b.filter != nil || strings.HasPrefix(obj.Name(), b.partial) { 185 | b.exact = append(b.exact, obj) 186 | } else if strings.HasPrefix(strings.ToLower(obj.Name()), strings.ToLower(b.partial)) { 187 | b.badcase = append(b.badcase, obj) 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /langserver/lint.go: -------------------------------------------------------------------------------- 1 | package langserver 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | "fmt" 8 | "go/build" 9 | "log" 10 | "os/exec" 11 | "path" 12 | "regexp" 13 | "strconv" 14 | "strings" 15 | 16 | "github.com/sourcegraph/go-langserver/pkg/tools" 17 | "github.com/sourcegraph/go-lsp" 18 | "github.com/sourcegraph/jsonrpc2" 19 | ) 20 | 21 | const ( 22 | lintToolGolint = "golint" 23 | lintToolNone = "none" 24 | ) 25 | 26 | // Linter defines an interface for linting 27 | type Linter interface { 28 | IsInstalled(ctx context.Context, bctx *build.Context) error 29 | Lint(ctx context.Context, bctx *build.Context, args ...string) (diagnostics, error) 30 | } 31 | 32 | // lint runs the configured lint command with the given arguments then published the 33 | // results as diagnostics. 34 | func (h *LangHandler) lint(ctx context.Context, bctx *build.Context, conn jsonrpc2.JSONRPC2, args []string, files []string) error { 35 | if h.linter == nil { 36 | return nil 37 | } 38 | 39 | diags, err := h.linter.Lint(ctx, bctx, args...) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | return h.publishDiagnostics(ctx, conn, diags, h.config.LintTool, files) 45 | } 46 | 47 | // lintPackage runs LangHandler.lint for the package containing the uri. 48 | func (h *LangHandler) lintPackage(ctx context.Context, bctx *build.Context, conn jsonrpc2.JSONRPC2, uri lsp.DocumentURI) error { 49 | filename := h.FilePath(uri) 50 | pkg, err := ContainingPackage(h.BuildContext(ctx), filename, h.RootFSPath) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | files := make([]string, 0, len(pkg.GoFiles)) 56 | for _, f := range pkg.GoFiles { 57 | files = append(files, path.Join(pkg.Dir, f)) 58 | } 59 | return h.lint(ctx, bctx, conn, []string{path.Dir(filename)}, files) 60 | } 61 | 62 | // lintWorkspace runs LangHandler.lint for the entire workspace 63 | func (h *LangHandler) lintWorkspace(ctx context.Context, bctx *build.Context, conn jsonrpc2.JSONRPC2) error { 64 | var files []string 65 | pkgs := tools.ListPkgsUnderDir(bctx, h.RootFSPath) 66 | find := h.getFindPackageFunc() 67 | for _, pkg := range pkgs { 68 | p, err := find(ctx, bctx, pkg, h.RootFSPath, h.RootFSPath, 0) 69 | if err != nil { 70 | if _, ok := err.(*build.NoGoError); ok { 71 | continue 72 | } 73 | if _, ok := err.(*build.MultiplePackageError); ok { 74 | continue 75 | } 76 | return err 77 | } 78 | 79 | for _, f := range p.GoFiles { 80 | files = append(files, path.Join(p.Dir, f)) 81 | } 82 | } 83 | return h.lint(ctx, bctx, conn, []string{path.Join(h.RootFSPath, "/...")}, files) 84 | } 85 | 86 | // golint is a wrapper around the golint command that implements the 87 | // linter interface. 88 | type golint struct{} 89 | 90 | func (l golint) IsInstalled(ctx context.Context, bctx *build.Context) error { 91 | _, err := exec.LookPath("golint") 92 | return err 93 | } 94 | 95 | func (l golint) Lint(ctx context.Context, bctx *build.Context, args ...string) (diagnostics, error) { 96 | cmd := exec.CommandContext(ctx, "golint", args...) 97 | cmd.Env = []string{ 98 | "GOPATH=" + bctx.GOPATH, 99 | "GOROOT=" + bctx.GOROOT, 100 | } 101 | 102 | errBuff := new(bytes.Buffer) 103 | cmd.Stderr = errBuff 104 | 105 | stdout, err := cmd.StdoutPipe() 106 | if err != nil { 107 | return nil, fmt.Errorf("lint command error: %s", err) 108 | } 109 | defer stdout.Close() 110 | 111 | err = cmd.Start() 112 | if err != nil { 113 | return nil, fmt.Errorf("lint command error: %s", err) 114 | } 115 | 116 | diags := diagnostics{} 117 | scanner := bufio.NewScanner(stdout) 118 | 119 | for scanner.Scan() { 120 | file, line, _, message, err := parseLintResult(scanner.Text()) 121 | if err != nil { 122 | // If there is an error parsing a line still try to parse the remaining lines 123 | log.Printf("warning: error failed to parse lint result: %v", err) 124 | continue 125 | } 126 | 127 | diags[file] = append(diags[file], &lsp.Diagnostic{ 128 | Message: message, 129 | Severity: lsp.Warning, 130 | Source: lintToolGolint, 131 | Range: lsp.Range{ // currently only supporting line level errors 132 | Start: lsp.Position{ 133 | Line: line, 134 | Character: 0, 135 | }, 136 | End: lsp.Position{ 137 | Line: line, 138 | Character: 0, 139 | }, 140 | }, 141 | }) 142 | } 143 | 144 | if err := scanner.Err(); err != nil { 145 | return nil, fmt.Errorf("could not read lint command output: %s", err) 146 | } 147 | 148 | cmd.Wait() 149 | if err != nil { 150 | return nil, fmt.Errorf("lint command error: %s", err) 151 | } 152 | 153 | if errBuff.Len() > 0 { 154 | log.Printf("warning: lint command stderr: %q", errBuff.String()) 155 | } 156 | 157 | return diags, nil 158 | } 159 | 160 | var lintResultRe = regexp.MustCompile(`^(.+?):(\d+):(\d+:?)? (.+)$`) 161 | 162 | func parseLintResult(l string) (file string, line, char int, message string, err error) { 163 | m := lintResultRe.FindStringSubmatch(l) 164 | if len(m) == 0 { 165 | err = fmt.Errorf("invalid result %q", l) 166 | return 167 | } 168 | 169 | file = m[1] 170 | message = m[4] 171 | 172 | line, err = strconv.Atoi(m[2]) 173 | if err != nil { 174 | err = fmt.Errorf("invalid line number in %q: %s", l, err) 175 | return 176 | } 177 | if m[3] != "" { 178 | char, err = strconv.Atoi(strings.TrimRight(m[3], ":")) 179 | if err != nil { 180 | err = fmt.Errorf("invalid char number in %q: %s", l, err) 181 | return 182 | } 183 | } 184 | 185 | return file, line - 1, char - 1, message, nil // LSP is 0-indexed 186 | } 187 | -------------------------------------------------------------------------------- /langserver/diagnostics.go: -------------------------------------------------------------------------------- 1 | package langserver 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "go/scanner" 7 | "go/token" 8 | "go/types" 9 | "strings" 10 | "sync" 11 | 12 | "golang.org/x/tools/go/loader" 13 | 14 | "github.com/sourcegraph/go-lsp" 15 | "github.com/sourcegraph/jsonrpc2" 16 | 17 | "github.com/sourcegraph/go-langserver/langserver/util" 18 | ) 19 | 20 | type diagnostics map[string][]*lsp.Diagnostic // map of URI to diagnostics (for PublishDiagnosticParams) 21 | 22 | type diagnosticsCache struct { 23 | mu sync.Mutex 24 | cache diagnostics 25 | } 26 | 27 | func newDiagnosticsCache() *diagnosticsCache { 28 | return &diagnosticsCache{ 29 | cache: diagnostics{}, 30 | } 31 | } 32 | 33 | // sync updates the diagnosticsCache and returns diagnostics need to be 34 | // published. 35 | // 36 | // When a file no longer has any diagnostics the file will be removed from 37 | // the cache. The removed file will be included in the returned diagnostics 38 | // in order to clear the client. 39 | // 40 | // sync is thread safe and will only allow one go routine to modify the cache 41 | // at a time. 42 | func (p *diagnosticsCache) sync(update func(diagnostics) diagnostics, compare func(oldDiagnostics, newDiagnostics diagnostics) (publish diagnostics)) (publish diagnostics) { 43 | p.mu.Lock() 44 | defer p.mu.Unlock() 45 | 46 | if p.cache == nil { 47 | p.cache = diagnostics{} 48 | } 49 | 50 | newCache := update(p.cache) 51 | publish = compare(p.cache, newCache) 52 | p.cache = newCache 53 | return publish 54 | } 55 | 56 | // publishDiagnostics sends diagnostic information (such as compile 57 | // errors) to the client. 58 | func (h *LangHandler) publishDiagnostics(ctx context.Context, conn jsonrpc2.JSONRPC2, diags diagnostics, source string, files []string) error { 59 | if !h.config.DiagnosticsEnabled { 60 | return nil 61 | } 62 | 63 | if diags == nil { 64 | diags = diagnostics{} 65 | } 66 | 67 | publish := h.diagnosticsCache.sync( 68 | func(cached diagnostics) diagnostics { 69 | return updateCachedDiagnostics(cached, diags, source, files) 70 | }, 71 | func(oldDiagnostics, newDiagnostics diagnostics) (publish diagnostics) { 72 | return compareCachedDiagnostics(oldDiagnostics, newDiagnostics, files) 73 | }, 74 | ) 75 | 76 | for filename, diags := range publish { 77 | params := lsp.PublishDiagnosticsParams{ 78 | URI: util.PathToURI(filename), 79 | Diagnostics: make([]lsp.Diagnostic, len(diags)), 80 | } 81 | for i, d := range diags { 82 | params.Diagnostics[i] = *d 83 | } 84 | if err := conn.Notify(ctx, "textDocument/publishDiagnostics", params); err != nil { 85 | return err 86 | } 87 | } 88 | return nil 89 | } 90 | 91 | func updateCachedDiagnostics(cachedDiagnostics diagnostics, newDiagnostics diagnostics, source string, files []string) diagnostics { 92 | // copy cachedDiagnostics so we don't mutate it 93 | cache := make(diagnostics, len(cachedDiagnostics)) 94 | for k, v := range cachedDiagnostics { 95 | cache[k] = v 96 | } 97 | 98 | for _, file := range files { 99 | 100 | // remove all of the diagnostics for the given source/file combinations and add the new diagnostics to the cache. 101 | i := 0 102 | for _, diag := range cache[file] { 103 | if diag.Source != source { 104 | cache[file][i] = diag 105 | i++ 106 | } 107 | } 108 | cache[file] = append(cache[file][:i], newDiagnostics[file]...) 109 | 110 | // clear out empty cache 111 | if len(cache[file]) == 0 { 112 | delete(cache, file) 113 | } 114 | } 115 | 116 | return cache 117 | } 118 | 119 | // compareCachedDiagnostics compares new and old diagnostics to determine 120 | // what needs to be published. 121 | func compareCachedDiagnostics(oldDiagnostics, newDiagnostics diagnostics, files []string) (publish diagnostics) { 122 | publish = diagnostics{} 123 | for _, f := range files { 124 | _, inOld := oldDiagnostics[f] 125 | diags, inNew := newDiagnostics[f] 126 | 127 | if inOld && !inNew { 128 | publish[f] = nil 129 | continue 130 | } 131 | 132 | if inNew { 133 | publish[f] = diags 134 | } 135 | } 136 | 137 | return publish 138 | } 139 | 140 | func errsToDiagnostics(typeErrs []error, prog *loader.Program) (diagnostics, error) { 141 | var diags diagnostics 142 | for _, typeErr := range typeErrs { 143 | var ( 144 | p token.Position 145 | pEnd token.Position 146 | msg string 147 | ) 148 | switch e := typeErr.(type) { 149 | case types.Error: 150 | p = e.Fset.Position(e.Pos) 151 | _, path, _ := prog.PathEnclosingInterval(e.Pos, e.Pos) 152 | if len(path) > 0 { 153 | pEnd = e.Fset.Position(path[0].End()) 154 | } 155 | msg = e.Msg 156 | case scanner.Error: 157 | p = e.Pos 158 | msg = e.Msg 159 | case scanner.ErrorList: 160 | if len(e) == 0 { 161 | continue 162 | } 163 | p = e[0].Pos 164 | msg = e[0].Msg 165 | if len(e) > 1 { 166 | msg = fmt.Sprintf("%s (and %d more errors)", msg, len(e)-1) 167 | } 168 | default: 169 | return nil, fmt.Errorf("unexpected type error: %#+v", typeErr) 170 | } 171 | // LSP is 0-indexed, so subtract one from the numbers Go reports. 172 | start := lsp.Position{Line: p.Line - 1, Character: p.Column - 1} 173 | end := lsp.Position{Line: pEnd.Line - 1, Character: pEnd.Column - 1} 174 | if !pEnd.IsValid() { 175 | end = start 176 | } 177 | diag := &lsp.Diagnostic{ 178 | Range: lsp.Range{ 179 | Start: start, 180 | End: end, 181 | }, 182 | Severity: lsp.Error, 183 | Source: "go", 184 | Message: strings.TrimSpace(msg), 185 | } 186 | if diags == nil { 187 | diags = diagnostics{} 188 | } 189 | diags[p.Filename] = append(diags[p.Filename], diag) 190 | } 191 | return diags, nil 192 | } 193 | -------------------------------------------------------------------------------- /brew/go-langserver.rb: -------------------------------------------------------------------------------- 1 | require "language/go" 2 | 3 | class GoLangserver < Formula 4 | desc "Go language LSP server" 5 | homepage "https://github.com/sourcegraph/go-langserver" 6 | url "https://github.com/sourcegraph/go-langserver/archive/v1.0.0.tar.gz" 7 | sha256 "e803e69f3df40e6e840d04f648e27ffcfb382ff77899c0c7a7b62cf3b3b277a4" 8 | 9 | head "https://github.com/sourcegraph/go-langserver.git" 10 | 11 | depends_on "go" => :build 12 | 13 | go_resource "github.com/beorn7/perks" do 14 | url "https://github.com/beorn7/perks.git", 15 | :revision => "4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9" 16 | end 17 | 18 | go_resource "github.com/gogo/protobuf" do 19 | url "https://github.com/gogo/protobuf.git", 20 | :revision => "6a92c871a8f5333cf106b2cdf937567208dbb2b7" 21 | end 22 | 23 | go_resource "github.com/golang/protobuf" do 24 | url "https://github.com/golang/protobuf.git", 25 | :revision => "8ee79997227bf9b34611aee7946ae64735e6fd93" 26 | end 27 | 28 | go_resource "github.com/matttproud/golang_protobuf_extensions" do 29 | url "https://github.com/matttproud/golang_protobuf_extensions.git", 30 | :revision => "c12348ce28de40eed0136aa2b644d0ee0650e56c" 31 | end 32 | 33 | go_resource "github.com/neelance/parallel" do 34 | url "https://github.com/neelance/parallel.git", 35 | :revision => "4de9ce63d14c18517a79efe69e10e99d32c850c3" 36 | end 37 | 38 | go_resource "github.com/opentracing/basictracer-go" do 39 | url "https://github.com/opentracing/basictracer-go.git", 40 | :revision => "1b32af207119a14b1b231d451df3ed04a72efebf" 41 | end 42 | 43 | go_resource "github.com/opentracing/opentracing-go" do 44 | url "https://github.com/opentracing/opentracing-go.git", 45 | :revision => "137bfcefd3340b28186f4fd3608719fcb120f98f" 46 | end 47 | 48 | go_resource "github.com/prometheus/client_golang" do 49 | url "https://github.com/prometheus/client_golang.git", 50 | :revision => "575f371f7862609249a1be4c9145f429fe065e32" 51 | end 52 | 53 | go_resource "github.com/prometheus/client_model" do 54 | url "https://github.com/prometheus/client_model.git", 55 | :revision => "fa8ad6fec33561be4280a8f0514318c79d7f6cb6" 56 | end 57 | 58 | go_resource "github.com/prometheus/common" do 59 | url "https://github.com/prometheus/common.git", 60 | :revision => "195bde7883f7c39ea62b0d92ab7359b5327065cb" 61 | end 62 | 63 | go_resource "github.com/prometheus/procfs" do 64 | url "https://github.com/prometheus/procfs.git", 65 | :revision => "abf152e5f3e97f2fafac028d2cc06c1feb87ffa5" 66 | end 67 | 68 | go_resource "github.com/slimsag/godocmd" do 69 | url "https://github.com/slimsag/godocmd.git", 70 | :revision => "a1005ad29fe3e4831773a8184ee7ebb3a41d1347" 71 | end 72 | 73 | go_resource "github.com/sourcegraph/ctxvfs" do 74 | url "https://github.com/sourcegraph/ctxvfs.git", 75 | :revision => "8e2bd62a565a647defd704af3d81fce05c5f190c" 76 | end 77 | 78 | go_resource "github.com/sourcegraph/jsonrpc2" do 79 | url "https://github.com/sourcegraph/jsonrpc2.git", 80 | :revision => "9fdd802ab4655d2258cef1b05c81be8e0fe4e2ad" 81 | end 82 | 83 | go_resource "golang.org/x/net" do 84 | url "https://go.googlesource.com/net.git", 85 | :revision => "4971afdc2f162e82d185353533d3cf16188a9f4e" 86 | end 87 | 88 | go_resource "golang.org/x/tools" do 89 | url "https://go.googlesource.com/tools.git", 90 | :revision => "e04df2157ae7263e17159baabadc99fb03fc7514" 91 | end 92 | 93 | def install 94 | mkdir_p buildpath/"src/github.com/sourcegraph" 95 | ln_s buildpath, buildpath/"src/github.com/sourcegraph/go-langserver" 96 | ENV["GOPATH"] = buildpath.to_s 97 | Language::Go.stage_deps resources, buildpath/"src" 98 | system "go", "build", "langserver/cmd/langserver-go/langserver-go.go" 99 | bin.install "langserver-go" 100 | end 101 | 102 | test do 103 | # Set up fake GOROOT and create test Go project 104 | mkdir_p testpath/"gopath/src/test" 105 | mkdir_p testpath/"goroot" 106 | ENV["GOPATH"] = "#{testpath}/gopath" 107 | ENV["GOROOT"] = "#{testpath}/goroot" 108 | (testpath/"gopath/src/test/p/a.go").write("package p; func A() {}") 109 | # Invoke initialize, hover, and finally exit requests and make sure that LSP server returns proper data 110 | init_req = "{\"id\":0,\"method\":\"initialize\",\"params\":{\"rootPath\":\"file://#{testpath}/gopath/src/test/p\"}}" 111 | init_res = "{\"id\":0,\"result\":{\"capabilities\":{\"textDocumentSync\":1,\"hoverProvider\":true,\"definitionProvider\":true,\"referencesProvider\":true,\"documentSymbolProvider\":true,\"workspaceSymbolProvider\":true}},\"jsonrpc\":\"2.0\"}" 112 | hover_req = "{\"id\":1,\"method\":\"textDocument/hover\",\"params\":{\"textDocument\":{\"uri\":\"file://#{testpath}/gopath/src/test/p/a.go\"},\"position\":{\"line\":0,\"character\":16}}}" 113 | hover_res = "{\"id\":1,\"result\":{\"contents\":[{\"language\":\"go\",\"value\":\"func A()\"}],\"range\":{\"start\":{\"line\":0,\"character\":16},\"end\":{\"line\":0,\"character\":17}}},\"jsonrpc\":\"2.0\"}" 114 | exit_req = "{\"id\":2,\"method\":\"exit\",\"params\":{}}" 115 | require "open3" 116 | Open3.popen3("langserver-go") do |stdin, stdout, _| 117 | stdin.write("Content-Length: #{init_req.length}\r\n\r\n#{init_req}") 118 | sleep(1) 119 | stdin.write("Content-Length: #{hover_req.length}\r\n\r\n#{hover_req}") 120 | sleep(1) 121 | stdin.write("Content-Length: #{exit_req.length}\r\n\r\n#{exit_req}") 122 | stdin.close 123 | assert_equal "Content-Length: #{init_res.length}\r\nContent-Type: application/vscode-jsonrpc; charset=utf8\r\n\r\n#{init_res}Content-Length: #{hover_res.length}\r\nContent-Type: application/vscode-jsonrpc; charset=utf8\r\n\r\n#{hover_res}", stdout.read 124 | end 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /langserver/lint_test.go: -------------------------------------------------------------------------------- 1 | package langserver 2 | 3 | import ( 4 | "context" 5 | "go/build" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "reflect" 10 | "strings" 11 | "testing" 12 | 13 | "github.com/sourcegraph/go-langserver/langserver/util" 14 | "github.com/sourcegraph/go-lsp" 15 | ) 16 | 17 | func TestParseLintLine(t *testing.T) { 18 | tests := []struct { 19 | l string 20 | file string 21 | line int 22 | char int 23 | message string 24 | }{ 25 | { 26 | l: "A.go:1: message contents", 27 | file: "A.go", 28 | line: 0, 29 | char: -1, 30 | message: "message contents", 31 | }, 32 | { 33 | l: "A.go:1:2 message contents", 34 | file: "A.go", 35 | line: 0, 36 | char: 1, 37 | message: "message contents", 38 | }, 39 | { 40 | l: "A.go:1:2: message contents", 41 | file: "A.go", 42 | line: 0, 43 | char: 1, 44 | message: "message contents", 45 | }, 46 | } 47 | 48 | for _, test := range tests { 49 | file, line, char, message, err := parseLintResult(test.l) 50 | if err != nil { 51 | t.Errorf("unexpected error parsing %q: %v", test.l, err) 52 | } 53 | if file != test.file { 54 | t.Errorf("unexpected file parsing %q: want %q, have %q", test.l, test.file, file) 55 | } 56 | if line != test.line { 57 | t.Errorf("unexpected line parsing %q: want %d, have %d", test.l, test.line, line) 58 | } 59 | if char != test.char { 60 | t.Errorf("unexpected char parsing %q: want %d, have %d", test.l, test.char, char) 61 | } 62 | if message != test.message { 63 | t.Errorf("unexpected message parsing %q: want %q, have %q", test.l, test.message, message) 64 | } 65 | 66 | } 67 | } 68 | 69 | func TestLinterGolint(t *testing.T) { 70 | files := map[string]string{ 71 | "A.go": strings.Join([]string{ 72 | "package p", 73 | "", 74 | "func A(){}", 75 | "", 76 | "func AA(){}", 77 | }, "\n"), 78 | "B.go": strings.Join([]string{ 79 | "package p", 80 | "", 81 | "func B(){}", 82 | }, "\n"), 83 | "C.go": strings.Join([]string{ 84 | "package p", 85 | "", 86 | `import "test/p/sub"`, 87 | "", 88 | "// C is a function", 89 | "func C(){", 90 | " sub.D()", 91 | "}", 92 | }, "\n"), 93 | "sub/D.go": strings.Join([]string{ 94 | "package sub", 95 | "", 96 | "func D(){}", 97 | }, "\n"), 98 | } 99 | 100 | linterTest(t, files, func(ctx context.Context, bctx *build.Context, rootURI lsp.DocumentURI) { 101 | uriA := uriJoin(rootURI, "A.go") 102 | uriB := uriJoin(rootURI, "B.go") 103 | uriD := uriJoin(rootURI, "sub/D.go") 104 | 105 | l := &golint{} 106 | 107 | // skip tests if the golint command is not found 108 | err := l.IsInstalled(ctx, bctx) 109 | if err != nil { 110 | t.Skipf("lint command 'golint' not found: %s", err) 111 | } 112 | 113 | // Lint the package "test/p" and look for correct results 114 | actual, err := l.Lint(ctx, bctx, "test/p") 115 | if err != nil { 116 | t.Errorf("unexpected error: %s", err) 117 | } 118 | expected := diagnostics{ 119 | util.UriToRealPath(uriA): []*lsp.Diagnostic{ 120 | { 121 | Message: "exported function A should have comment or be unexported", 122 | Severity: lsp.Warning, 123 | Source: "golint", 124 | Range: lsp.Range{ 125 | Start: lsp.Position{Line: 2, Character: 0}, 126 | End: lsp.Position{Line: 2, Character: 0}, 127 | }, 128 | }, 129 | { 130 | Message: "exported function AA should have comment or be unexported", 131 | Severity: lsp.Warning, 132 | Source: "golint", 133 | Range: lsp.Range{ 134 | Start: lsp.Position{Line: 4, Character: 0}, 135 | End: lsp.Position{Line: 4, Character: 0}, 136 | }, 137 | }, 138 | }, 139 | util.UriToRealPath(uriB): []*lsp.Diagnostic{ 140 | { 141 | Message: "exported function B should have comment or be unexported", 142 | Severity: lsp.Warning, 143 | Source: "golint", 144 | Range: lsp.Range{ 145 | Start: lsp.Position{Line: 2, Character: 0}, 146 | End: lsp.Position{Line: 2, Character: 0}, 147 | }, 148 | }, 149 | }, 150 | } 151 | if !reflect.DeepEqual(actual, expected) { 152 | t.Errorf("Expected diagnostics %v but got %v", actual, expected) 153 | } 154 | 155 | // Lint the package and subpackages "test/p/..." look for correct lint results 156 | actual, err = l.Lint(ctx, bctx, "test/p/...") 157 | if err != nil { 158 | t.Errorf("unexpected error: %s", err) 159 | } 160 | expected[util.UriToRealPath(uriD)] = []*lsp.Diagnostic{ 161 | { 162 | Message: "exported function D should have comment or be unexported", 163 | Severity: lsp.Warning, 164 | Source: "golint", 165 | Range: lsp.Range{ // currently only supporting line level errors 166 | Start: lsp.Position{Line: 2, Character: 0}, 167 | End: lsp.Position{Line: 2, Character: 0}, 168 | }, 169 | }, 170 | } 171 | if !reflect.DeepEqual(actual, expected) { 172 | t.Errorf("Expected diagnostics %v but got %v", actual, expected) 173 | } 174 | }) 175 | } 176 | 177 | func linterTest(t *testing.T, files map[string]string, fn func(ctx context.Context, bctx *build.Context, rootURI lsp.DocumentURI)) { 178 | tmpDir, err := ioutil.TempDir("", "langserver-go-linter") 179 | if err != nil { 180 | t.Fatal(err) 181 | } 182 | defer os.RemoveAll(tmpDir) 183 | 184 | GOPATH := filepath.Join(tmpDir, "gopath") 185 | GOROOT := filepath.Join(tmpDir, "goroot") 186 | rootFSPath := filepath.Join(GOPATH, "src/test/p") 187 | 188 | if err := os.MkdirAll(GOROOT, 0700); err != nil { 189 | t.Fatal(err) 190 | } 191 | if err := os.MkdirAll(rootFSPath, 0700); err != nil { 192 | t.Fatal(err) 193 | } 194 | for filename, contents := range files { 195 | path := filepath.Join(rootFSPath, filename) 196 | if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { 197 | t.Fatal(err) 198 | } 199 | if err := ioutil.WriteFile(path, []byte(contents), 0600); err != nil { 200 | t.Fatal(err) 201 | } 202 | } 203 | 204 | ctx := context.Background() 205 | bctx := &build.Context{ 206 | GOPATH: GOPATH, 207 | GOROOT: GOROOT, 208 | } 209 | 210 | fn(ctx, bctx, util.PathToURI(rootFSPath)) 211 | } 212 | -------------------------------------------------------------------------------- /langserver/internal/gocode/suggest/suggest.go: -------------------------------------------------------------------------------- 1 | package suggest 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "go/ast" 7 | "go/parser" 8 | "go/scanner" 9 | "go/token" 10 | "go/types" 11 | "io/ioutil" 12 | "path/filepath" 13 | "strings" 14 | 15 | "github.com/sourcegraph/go-langserver/langserver/internal/gocode/lookdot" 16 | ) 17 | 18 | type Config struct { 19 | Importer types.Importer 20 | Logf func(fmt string, args ...interface{}) 21 | Builtin bool 22 | } 23 | 24 | type packageAnalysis struct { 25 | fset *token.FileSet 26 | pos token.Pos 27 | pkg *types.Package 28 | } 29 | 30 | // Suggest returns a list of suggestion candidates and the length of 31 | // the text that should be replaced, if any. 32 | func (c *Config) Suggest(filename string, data []byte, cursor int) ([]Candidate, int, error) { 33 | if cursor < 0 { 34 | return nil, 0, nil 35 | } 36 | 37 | a, err := c.analyzePackage(filename, data, cursor) 38 | if err != nil { 39 | return nil, 0, err 40 | } 41 | fset := a.fset 42 | pos := a.pos 43 | pkg := a.pkg 44 | if pkg == nil { 45 | return nil, 0, nil 46 | } 47 | scope := pkg.Scope().Innermost(pos) 48 | 49 | ctx, expr, partial := deduceCursorContext(data, cursor) 50 | b := candidateCollector{ 51 | localpkg: pkg, 52 | partial: partial, 53 | filter: objectFilters[partial], 54 | builtin: ctx != selectContext && c.Builtin, 55 | } 56 | 57 | switch ctx { 58 | case selectContext: 59 | tv, _ := types.Eval(fset, pkg, pos, expr) 60 | if lookdot.Walk(&tv, b.appendObject) { 61 | break 62 | } 63 | 64 | _, obj := scope.LookupParent(expr, pos) 65 | if pkgName, isPkg := obj.(*types.PkgName); isPkg { 66 | c.packageCandidates(pkgName.Imported(), &b) 67 | break 68 | } 69 | 70 | return nil, 0, nil 71 | 72 | case compositeLiteralContext: 73 | tv, _ := types.Eval(fset, pkg, pos, expr) 74 | if tv.IsType() { 75 | if _, isStruct := tv.Type.Underlying().(*types.Struct); isStruct { 76 | c.fieldNameCandidates(tv.Type, &b) 77 | break 78 | } 79 | } 80 | 81 | fallthrough 82 | default: 83 | c.scopeCandidates(scope, pos, &b) 84 | } 85 | 86 | res := b.getCandidates() 87 | if len(res) == 0 { 88 | return nil, 0, nil 89 | } 90 | return res, len(partial), nil 91 | } 92 | 93 | func (c *Config) analyzePackage(filename string, data []byte, cursor int) (*packageAnalysis, error) { 94 | // If we're in trailing white space at the end of a scope, 95 | // sometimes go/types doesn't recognize that variables should 96 | // still be in scope there. 97 | filesemi := bytes.Join([][]byte{data[:cursor], []byte(";"), data[cursor:]}, nil) 98 | 99 | fset := token.NewFileSet() 100 | fileAST, err := parser.ParseFile(fset, filename, filesemi, parser.AllErrors) 101 | if err != nil { 102 | c.logParseError("Error parsing input file (outer block)", err) 103 | } 104 | astPos := fileAST.Pos() 105 | if astPos == 0 { 106 | return &packageAnalysis{fset: nil, pos: token.NoPos, pkg: nil}, nil 107 | } 108 | pos := fset.File(astPos).Pos(cursor) 109 | 110 | files := []*ast.File{fileAST} 111 | otherPkgFiles, err := c.findOtherPackageFiles(filename, fileAST.Name.Name) 112 | if err != nil { 113 | return nil, err 114 | } 115 | for _, otherName := range otherPkgFiles { 116 | ast, err := parser.ParseFile(fset, otherName, nil, 0) 117 | if err != nil { 118 | c.logParseError("Error parsing other file", err) 119 | } 120 | files = append(files, ast) 121 | } 122 | 123 | // Clear any function bodies other than where the cursor 124 | // is. They're not relevant to suggestions and only slow down 125 | // typechecking. 126 | for _, file := range files { 127 | for _, decl := range file.Decls { 128 | if fd, ok := decl.(*ast.FuncDecl); ok && (pos < fd.Pos() || pos >= fd.End()) { 129 | fd.Body = nil 130 | } 131 | } 132 | } 133 | 134 | cfg := types.Config{ 135 | Importer: c.Importer, 136 | Error: func(err error) {}, 137 | } 138 | pkg, _ := cfg.Check("", fset, files, nil) 139 | 140 | return &packageAnalysis{fset: fset, pos: pos, pkg: pkg}, nil 141 | } 142 | 143 | func (c *Config) fieldNameCandidates(typ types.Type, b *candidateCollector) { 144 | s := typ.Underlying().(*types.Struct) 145 | for i, n := 0, s.NumFields(); i < n; i++ { 146 | b.appendObject(s.Field(i)) 147 | } 148 | } 149 | 150 | func (c *Config) packageCandidates(pkg *types.Package, b *candidateCollector) { 151 | c.scopeCandidates(pkg.Scope(), token.NoPos, b) 152 | } 153 | 154 | func (c *Config) scopeCandidates(scope *types.Scope, pos token.Pos, b *candidateCollector) { 155 | seen := make(map[string]bool) 156 | for scope != nil { 157 | for _, name := range scope.Names() { 158 | if seen[name] { 159 | continue 160 | } 161 | seen[name] = true 162 | _, obj := scope.LookupParent(name, pos) 163 | if obj != nil { 164 | b.appendObject(obj) 165 | } 166 | } 167 | scope = scope.Parent() 168 | } 169 | } 170 | 171 | func (c *Config) logParseError(intro string, err error) { 172 | if c.Logf == nil { 173 | return 174 | } 175 | if el, ok := err.(scanner.ErrorList); ok { 176 | c.Logf("%s:", intro) 177 | for _, er := range el { 178 | c.Logf(" %s", er) 179 | } 180 | } else { 181 | c.Logf("%s: %s", intro, err) 182 | } 183 | } 184 | 185 | func (c *Config) findOtherPackageFiles(filename, pkgName string) ([]string, error) { 186 | if filename == "" { 187 | return nil, nil 188 | } 189 | 190 | dir, file := filepath.Split(filename) 191 | dents, err := ioutil.ReadDir(dir) 192 | if err != nil { 193 | return nil, fmt.Errorf("could not read dir: %v", err) 194 | } 195 | isTestFile := strings.HasSuffix(file, "_test.go") 196 | 197 | // TODO(mdempsky): Use go/build.(*Context).MatchFile or 198 | // something to properly handle build tags? 199 | var out []string 200 | for _, dent := range dents { 201 | name := dent.Name() 202 | if strings.HasPrefix(name, ".") || strings.HasPrefix(name, "_") { 203 | continue 204 | } 205 | if name == file || !strings.HasSuffix(name, ".go") { 206 | continue 207 | } 208 | if !isTestFile && strings.HasSuffix(name, "_test.go") { 209 | continue 210 | } 211 | 212 | abspath := filepath.Join(dir, name) 213 | if pkgNameFor(abspath) == pkgName { 214 | out = append(out, abspath) 215 | } 216 | } 217 | 218 | return out, nil 219 | } 220 | 221 | func pkgNameFor(filename string) string { 222 | file, _ := parser.ParseFile(token.NewFileSet(), filename, nil, parser.PackageClauseOnly) 223 | return file.Name.Name 224 | } 225 | -------------------------------------------------------------------------------- /vfsutil/git.go: -------------------------------------------------------------------------------- 1 | package vfsutil 2 | 3 | import ( 4 | "archive/zip" 5 | "bytes" 6 | "context" 7 | "crypto/sha256" 8 | "encoding/hex" 9 | "fmt" 10 | "os" 11 | "os/exec" 12 | "path/filepath" 13 | "regexp" 14 | "strings" 15 | "sync" 16 | "time" 17 | 18 | opentracing "github.com/opentracing/opentracing-go" 19 | "github.com/opentracing/opentracing-go/ext" 20 | otlog "github.com/opentracing/opentracing-go/log" 21 | "github.com/sourcegraph/ctxvfs" 22 | "golang.org/x/tools/godoc/vfs" 23 | "golang.org/x/tools/godoc/vfs/zipfs" 24 | ) 25 | 26 | type GitRepoVFS struct { 27 | CloneURL string // Git clone URL (e.g., "https://github.com/foo/bar") 28 | Rev string // Git revision (should be absolute, 40-char for consistency, unless nondeterminism is OK) 29 | Subtree string // only include this subtree 30 | 31 | once sync.Once 32 | err error // the error encountered during the fetch 33 | fs vfs.FileSystem 34 | } 35 | 36 | var gitCommitSHARx = regexp.MustCompile(`^[0-9a-f]{40}$`) 37 | 38 | // fetchOrWait initiates the fetch if it has not yet 39 | // started. Otherwise it waits for it to finish. 40 | func (fs *GitRepoVFS) fetchOrWait(ctx context.Context) error { 41 | fs.once.Do(func() { 42 | fs.err = fs.fetch(ctx) 43 | }) 44 | return fs.err 45 | } 46 | 47 | var gitArchiveBasePath = "/tmp/go-langserver-git-clone-cache" 48 | 49 | func (fs *GitRepoVFS) fetch(ctx context.Context) (err error) { 50 | span, ctx := opentracing.StartSpanFromContext(ctx, "GitRepoVFS fetch") 51 | defer func() { 52 | if err != nil { 53 | ext.Error.Set(span, true) 54 | span.LogFields(otlog.Error(err)) 55 | } 56 | span.Finish() 57 | }() 58 | 59 | urlMu := urlMu(fs.CloneURL) 60 | urlMu.Lock() 61 | defer urlMu.Unlock() 62 | span.LogFields(otlog.String("event", "urlMu acquired")) 63 | 64 | h := sha256.Sum256([]byte(fs.CloneURL)) 65 | urlHash := hex.EncodeToString(h[:]) 66 | repoDir := filepath.Join(gitArchiveBasePath, urlHash+".git") 67 | 68 | // Make sure the rev arg can't be misinterpreted as a command-line 69 | // flag. 70 | if err := gitCheckArgSafety(fs.Rev); err != nil { 71 | return err 72 | } 73 | 74 | // Try resolving the revision immediately. If we can resolve it, no need to clone or update. 75 | commitID, err := gitObjectNameSHA(repoDir+"^0", fs.Rev) 76 | if err != nil { 77 | ctx, cancel := context.WithTimeout(ctx, 5*time.Minute) 78 | defer cancel() 79 | if _, err := os.Stat(repoDir); err == nil { 80 | // Update the repo and hope that we fetch the rev (and can 81 | // resolve it afterwards). 82 | cmd := exec.CommandContext(ctx, "git", "remote", "update") 83 | cmd.Dir = repoDir 84 | if out, err := cmd.CombinedOutput(); err != nil { 85 | return fmt.Errorf("command %v failed: %s (output follows)\n%s", cmd.Args, err, out) 86 | } 87 | } else if os.IsNotExist(err) { 88 | // Clone the repo with full history (we will reuse it). 89 | cmd := exec.CommandContext(ctx, "git", "clone", "--bare", "--mirror", "--", fs.CloneURL, repoDir) 90 | if out, err := cmd.CombinedOutput(); err != nil { 91 | return fmt.Errorf("command %v failed: %s (output follows)\n%s", cmd.Args, err, out) 92 | } 93 | } else if err != nil { 94 | return err 95 | } 96 | } 97 | 98 | // Resolve revision (if we didn't already do so above successfully 99 | // and needed to clone/update). 100 | if commitID == "" { 101 | commitID, err = gitObjectNameSHA(repoDir, fs.Rev) 102 | if err != nil { 103 | return err 104 | } 105 | } 106 | 107 | if !gitCommitSHARx.MatchString(commitID) { 108 | return fmt.Errorf("git rev %q from %s resolved to suspicious commit ID %q", fs.Rev, fs.CloneURL, commitID) 109 | } 110 | 111 | zr, err := gitZipArchive(repoDir, commitID, fs.Subtree) 112 | if err != nil { 113 | return err 114 | } 115 | fs.fs = zipfs.New(zr, "") 116 | return nil 117 | } 118 | 119 | func (fs *GitRepoVFS) Open(ctx context.Context, path string) (ctxvfs.ReadSeekCloser, error) { 120 | if err := fs.fetchOrWait(ctx); err != nil { 121 | return nil, err 122 | } 123 | return fs.fs.Open(path) 124 | } 125 | 126 | func (fs *GitRepoVFS) Lstat(ctx context.Context, path string) (os.FileInfo, error) { 127 | if err := fs.fetchOrWait(ctx); err != nil { 128 | return nil, err 129 | } 130 | return fs.fs.Lstat(path) 131 | } 132 | 133 | func (fs *GitRepoVFS) Stat(ctx context.Context, path string) (os.FileInfo, error) { 134 | if err := fs.fetchOrWait(ctx); err != nil { 135 | return nil, err 136 | } 137 | return fs.fs.Stat(path) 138 | } 139 | 140 | func (fs *GitRepoVFS) ReadDir(ctx context.Context, path string) ([]os.FileInfo, error) { 141 | if err := fs.fetchOrWait(ctx); err != nil { 142 | return nil, err 143 | } 144 | return fs.fs.ReadDir(path) 145 | } 146 | 147 | func (fs *GitRepoVFS) String() string { 148 | return fmt.Sprintf("GitRepoVFS{CloneURL: %q, Rev: %q}", fs.CloneURL, fs.Rev) 149 | } 150 | 151 | // gitCheckArgSafety returns a non-nil error if rev is unsafe to 152 | // use as a command-line argument (i.e., it begins with "-" and thus 153 | // could be confused with a command-line flag). 154 | func gitCheckArgSafety(rev string) error { 155 | if strings.HasPrefix(rev, "-") { 156 | return fmt.Errorf("invalid Git revision (can't start with '-'): %q", rev) 157 | } 158 | return nil 159 | } 160 | 161 | func gitObjectNameSHA(dir, arg string) (string, error) { 162 | if err := gitCheckArgSafety(arg); err != nil { 163 | return "", err 164 | } 165 | cmd := exec.Command("git", "rev-parse", "--verify", arg) 166 | cmd.Dir = dir 167 | out, err := cmd.CombinedOutput() 168 | if err != nil { 169 | return "", fmt.Errorf("command %v failed: %s (output follows)\n%s", cmd.Args, err, out) 170 | } 171 | return string(bytes.TrimSpace(out)), nil 172 | } 173 | 174 | func gitZipArchive(dir, rev, subtree string) (*zip.ReadCloser, error) { 175 | if err := gitCheckArgSafety(rev); err != nil { 176 | return nil, err 177 | } 178 | if err := gitCheckArgSafety(subtree); err != nil { 179 | return nil, err 180 | } 181 | if subtree != "" { 182 | // This is how you specify that you want all of a subtree, not 183 | // just a specific file. 184 | rev += ":" + subtree 185 | } 186 | // TODO(sqs): for efficiency, can specify a subtree here and then 187 | // the vfs is only of a certain subtree 188 | cmd := exec.Command("git", "archive", "--format=zip", rev) 189 | cmd.Dir = dir 190 | var buf bytes.Buffer 191 | cmd.Stderr = &buf 192 | out, err := cmd.Output() 193 | if err != nil { 194 | return nil, fmt.Errorf("command %v failed: %s (stderr follows)\n%s", cmd.Args, err, buf.Bytes()) 195 | } 196 | zr, err := zip.NewReader(bytes.NewReader(out), int64(len(out))) 197 | if err != nil { 198 | return nil, err 199 | } 200 | return &zip.ReadCloser{Reader: *zr}, nil 201 | } 202 | -------------------------------------------------------------------------------- /langserver/internal/godef/go/parser/interface.go: -------------------------------------------------------------------------------- 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 | } 201 | --------------------------------------------------------------------------------