├── .tool-versions ├── docs ├── examples │ ├── imports │ │ ├── .gitignore │ │ ├── go.mod │ │ ├── gen.sh │ │ └── main.go │ └── smollest │ │ ├── .gitignore │ │ ├── sub.go │ │ ├── go.mod │ │ ├── lib.go │ │ ├── gen.sh │ │ └── dump.svg ├── media │ ├── fmt_import.png │ ├── http_import.png │ ├── sort_import.png │ └── s_definition.png ├── structs.md ├── imports.md └── package_declarations.md ├── README.md ├── internal ├── testdata │ └── fixtures │ │ ├── duplicate_path_id │ │ ├── two.go │ │ └── main.go │ │ ├── internal │ │ ├── secret │ │ │ ├── doc.go │ │ │ └── secret.go │ │ └── shouldvisit │ │ │ ├── tests │ │ │ ├── tests.go │ │ │ └── tests_test.go │ │ │ ├── notests │ │ │ └── notests.go │ │ │ └── tests_separate │ │ │ ├── pkg_test.go │ │ │ └── pkg.go │ │ ├── go.mod │ │ ├── external_composite.go │ │ ├── pkg │ │ └── pkg.go │ │ ├── cmd │ │ └── minimal_main │ │ │ └── minimal_main.go │ │ ├── main.go │ │ ├── implementations_embedded.go │ │ ├── named_import.go │ │ ├── typealias.go │ │ ├── typeswitch.go │ │ ├── composite.go │ │ ├── implementations_remote.go │ │ ├── conflicting_test_symbols │ │ ├── sandbox_unsupported_test.go │ │ └── sandbox_unsupported.go │ │ ├── implementation_methods.go │ │ ├── illegal_multiple_mains │ │ └── main.go │ │ ├── parallel.go │ │ ├── implementations.go │ │ ├── child_symbols.go │ │ └── data.go ├── gomod │ ├── util.go │ ├── stdlib_test.go │ ├── module_name_test.go │ ├── module_name.go │ ├── dependencies_test.go │ ├── dependencies.go │ └── stdlib.go ├── git │ ├── check_git.go │ ├── toplevel.go │ ├── infer_module_version.go │ ├── infer_repo.go │ └── infer_repo_test.go ├── indexer │ ├── util.go │ ├── util_test.go │ ├── package_data_cache_test.go │ ├── striped_mutex.go │ ├── info.go │ ├── protocol.go │ ├── types.go │ ├── protocol_test.go │ ├── visit.go │ ├── typestring.go │ ├── hover_test.go │ ├── hover.go │ ├── typestring_test.go │ ├── moniker_test.go │ ├── moniker.go │ ├── package_data_cache.go │ └── implementation.go ├── command │ └── command.go ├── parallel │ ├── parallel.go │ └── parallel_test.go ├── util │ ├── duration.go │ └── duration_test.go └── output │ └── output.go ├── cmd └── lsif-go │ ├── version.go │ ├── cached_string.go │ ├── stats.go │ ├── paths.go │ ├── index.go │ ├── main.go │ └── args.go ├── Dockerfile ├── .buildkite └── hooks │ └── pre-command ├── .github ├── workflows │ ├── lsif.yml │ ├── pr-auditor.yml │ ├── release.yml │ └── project-board.yml └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── CONTRIBUTING.md ├── .goreleaser.yml ├── scripts └── gen_stdlib_map.sh ├── LICENSE ├── OLD_README.md ├── go.mod ├── BENCHMARK.md └── CHANGELOG.md /.tool-versions: -------------------------------------------------------------------------------- 1 | golang 1.18.2 2 | -------------------------------------------------------------------------------- /docs/examples/imports/.gitignore: -------------------------------------------------------------------------------- 1 | dump.lsif 2 | -------------------------------------------------------------------------------- /docs/examples/smollest/.gitignore: -------------------------------------------------------------------------------- 1 | dump.lsif 2 | -------------------------------------------------------------------------------- /docs/examples/smollest/sub.go: -------------------------------------------------------------------------------- 1 | package smollest 2 | -------------------------------------------------------------------------------- /docs/examples/imports/go.mod: -------------------------------------------------------------------------------- 1 | module imports 2 | 3 | go 1.16 4 | -------------------------------------------------------------------------------- /docs/examples/smollest/go.mod: -------------------------------------------------------------------------------- 1 | module smollest 2 | 3 | go 1.16 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **DEPRECATED:** Use [scip-go](https://github.com/sourcegraph/scip-go) instead. 2 | -------------------------------------------------------------------------------- /internal/testdata/fixtures/duplicate_path_id/two.go: -------------------------------------------------------------------------------- 1 | package gosrc 2 | 3 | func init() { 4 | } 5 | -------------------------------------------------------------------------------- /cmd/lsif-go/version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Set by go releaser 4 | var version string = "dev" 5 | -------------------------------------------------------------------------------- /docs/media/fmt_import.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcegraph/lsif-go/HEAD/docs/media/fmt_import.png -------------------------------------------------------------------------------- /docs/media/http_import.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcegraph/lsif-go/HEAD/docs/media/http_import.png -------------------------------------------------------------------------------- /docs/media/sort_import.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcegraph/lsif-go/HEAD/docs/media/sort_import.png -------------------------------------------------------------------------------- /docs/examples/smollest/lib.go: -------------------------------------------------------------------------------- 1 | // Hello world, this is a docstring. So we pick this file. 2 | package smollest 3 | -------------------------------------------------------------------------------- /docs/media/s_definition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcegraph/lsif-go/HEAD/docs/media/s_definition.png -------------------------------------------------------------------------------- /internal/testdata/fixtures/internal/secret/doc.go: -------------------------------------------------------------------------------- 1 | // secret is a package that holds secrets. 2 | package secret 3 | -------------------------------------------------------------------------------- /internal/testdata/fixtures/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sourcegraph/lsif-go/internal/testdata/fixtures 2 | 3 | go 1.16 4 | -------------------------------------------------------------------------------- /internal/testdata/fixtures/internal/shouldvisit/tests/tests.go: -------------------------------------------------------------------------------- 1 | // This package has tests. 2 | package tests 3 | 4 | func foo() bool { return true } 5 | -------------------------------------------------------------------------------- /internal/testdata/fixtures/internal/shouldvisit/notests/notests.go: -------------------------------------------------------------------------------- 1 | // This package has no tests. 2 | package notests 3 | 4 | func foo() bool { return true } 5 | -------------------------------------------------------------------------------- /internal/testdata/fixtures/external_composite.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | import "net/http" 4 | 5 | type NestedHandler struct { 6 | http.Handler 7 | Other int 8 | } 9 | -------------------------------------------------------------------------------- /internal/testdata/fixtures/internal/shouldvisit/tests_separate/pkg_test.go: -------------------------------------------------------------------------------- 1 | package pkg_test 2 | 3 | import "testing" 4 | 5 | func TestFoo(t *testing.T) { 6 | _ = foo() 7 | } 8 | -------------------------------------------------------------------------------- /internal/testdata/fixtures/pkg/pkg.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | type Foo struct{} 4 | 5 | func (f Foo) nonExportedMethod() { 6 | } 7 | 8 | func (f Foo) ExportedMethod() { 9 | } 10 | -------------------------------------------------------------------------------- /internal/testdata/fixtures/internal/shouldvisit/tests_separate/pkg.go: -------------------------------------------------------------------------------- 1 | // This package has tests, but in a separate _test package. 2 | package pkg 3 | 4 | func foo() bool { return true } 5 | -------------------------------------------------------------------------------- /internal/testdata/fixtures/cmd/minimal_main/minimal_main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type User struct { 4 | Id, Name string 5 | } 6 | 7 | type UserResource struct{} 8 | 9 | func main() {} 10 | -------------------------------------------------------------------------------- /internal/testdata/fixtures/main.go: -------------------------------------------------------------------------------- 1 | // testdata is a small package containing sample Go source code used for 2 | // testing the indexing routines of github.com/sourcegraph/lsif-go. 3 | package testdata 4 | -------------------------------------------------------------------------------- /internal/testdata/fixtures/implementations_embedded.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | import "io" 4 | 5 | type I3 interface { 6 | Close() error 7 | } 8 | 9 | type TClose struct { 10 | io.Closer 11 | } 12 | -------------------------------------------------------------------------------- /internal/testdata/fixtures/named_import.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | import ( 4 | . "fmt" 5 | h "net/http" 6 | ) 7 | 8 | func Example() { 9 | Println(h.CanonicalHeaderKey("accept-encoding")) 10 | } 11 | -------------------------------------------------------------------------------- /internal/testdata/fixtures/internal/secret/secret.go: -------------------------------------------------------------------------------- 1 | package secret 2 | 3 | // SecretScore is like score but _secret_. 4 | const SecretScore = uint64(43) 5 | 6 | // Original doc 7 | type Burger struct { 8 | Field int 9 | } 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.18.2@sha256:800d9b4fb6231053473df14d5a7116bfd33500bca5ca4c6d544de739d9a7d302 2 | 3 | RUN curl -L https://sourcegraph.com/.api/src-cli/src_linux_amd64 -o /usr/bin/src && chmod +x /usr/bin/src 4 | 5 | COPY lsif-go /usr/bin/ 6 | -------------------------------------------------------------------------------- /docs/examples/smollest/gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | set -x 5 | 6 | lsif-go 7 | 8 | lsif-visualize dump.lsif \ 9 | --exclude=sourcegraph:documentationResult \ 10 | --exclude=hoverResult \ 11 | | dot -Tsvg > dump.svg 12 | -------------------------------------------------------------------------------- /internal/testdata/fixtures/internal/shouldvisit/tests/tests_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import "testing" 4 | 5 | func TestFoo(t *testing.T) { 6 | _ = foo() 7 | } 8 | 9 | func BenchmarkFoo(b *testing.B) { 10 | _ = foobar() 11 | } 12 | -------------------------------------------------------------------------------- /docs/examples/imports/gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | set -x 5 | 6 | lsif-go-imports 7 | 8 | lsif-visualize dump.lsif \ 9 | --exclude=sourcegraph:documentationResult \ 10 | --exclude=hoverResult \ 11 | | dot -Tsvg > dump.svg 12 | -------------------------------------------------------------------------------- /internal/testdata/fixtures/typealias.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | import ( 4 | "github.com/sourcegraph/lsif-go/internal/testdata/fixtures/internal/secret" 5 | ) 6 | 7 | // Type aliased doc 8 | type SecretBurger = secret.Burger 9 | 10 | type BadBurger = struct { 11 | Field string 12 | } 13 | -------------------------------------------------------------------------------- /internal/gomod/util.go: -------------------------------------------------------------------------------- 1 | package gomod 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | ) 7 | 8 | // isModule returns true if there is a go.mod file in the given directory. 9 | func isModule(dir string) bool { 10 | _, err := os.Stat(filepath.Join(dir, "go.mod")) 11 | return err == nil 12 | } 13 | -------------------------------------------------------------------------------- /internal/testdata/fixtures/typeswitch.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | func Switch(interfaceValue interface{}) bool { 4 | switch concreteValue := interfaceValue.(type) { 5 | case int: 6 | return concreteValue*3 > 10 7 | case bool: 8 | return !concreteValue 9 | default: 10 | return false 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /internal/git/check_git.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "github.com/sourcegraph/lsif-go/internal/command" 5 | ) 6 | 7 | // Check returns true if the current directory is in a git repository. 8 | func Check(dir string) bool { 9 | _, err := command.Run(dir, "git", "rev-parse", "HEAD") 10 | return err == nil 11 | } 12 | -------------------------------------------------------------------------------- /.buildkite/hooks/pre-command: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | pushd "$(dirname "${BASH_SOURCE[0]}")"/../.. 5 | 6 | # Skip the rest if this is pipeline upload or empty 7 | if [[ "${BUILDKITE_COMMAND:-}" =~ "buildkite-agent pipeline upload".* ]]; then 8 | exit 0 9 | fi 10 | 11 | echo "Installing asdf dependencies" 12 | asdf install 13 | -------------------------------------------------------------------------------- /docs/examples/imports/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | . "net/http" 6 | s "sort" 7 | ) 8 | 9 | func Main() { 10 | sortedStrings := []string{"hello", "world", "!"} 11 | 12 | // s -> sort 13 | s.Strings(sortedStrings) 14 | 15 | // http.CanonicalHeaderKey -> CanonicalHeaderKey 16 | fmt.Println(CanonicalHeaderKey(sortedStrings[0])) 17 | } 18 | -------------------------------------------------------------------------------- /.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@v3 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | .DS_Store 15 | /.vscode 16 | /.idea 17 | /lsif-go 18 | /lsif-gomod 19 | /data.lsif 20 | dump.lsif 21 | 22 | # GoReleaser dist 23 | release/ 24 | -------------------------------------------------------------------------------- /cmd/lsif-go/cached_string.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "sync" 4 | 5 | type cachedString struct { 6 | f func() string 7 | value string 8 | once sync.Once 9 | } 10 | 11 | func newCachedString(f func() string) *cachedString { 12 | return &cachedString{f: f} 13 | } 14 | 15 | func (cs *cachedString) Value() string { 16 | cs.once.Do(func() { cs.value = cs.f() }) 17 | return cs.value 18 | } 19 | -------------------------------------------------------------------------------- /internal/testdata/fixtures/composite.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | import "fmt" 4 | 5 | type Inner struct { 6 | X int 7 | Y int 8 | Z int 9 | } 10 | 11 | type Outer struct { 12 | Inner 13 | W int 14 | } 15 | 16 | func useOfCompositeStructs() { 17 | o := Outer{ 18 | Inner: Inner{ 19 | X: 1, 20 | Y: 2, 21 | Z: 3, 22 | }, 23 | W: 4, 24 | } 25 | 26 | fmt.Printf("> %d\n", o.X) 27 | } 28 | -------------------------------------------------------------------------------- /internal/testdata/fixtures/duplicate_path_id/main.go: -------------------------------------------------------------------------------- 1 | package gosrc 2 | 3 | type importMeta struct{} 4 | 5 | type sourceMeta struct{} 6 | 7 | func fetchMeta() (string, *importMeta, *sourceMeta) { 8 | panic("hmm") 9 | } 10 | 11 | func init() { 12 | } 13 | 14 | // two inits in the same file is legal 15 | func init() { 16 | } 17 | 18 | // three inits in the same file is legal 19 | func init() { 20 | } 21 | -------------------------------------------------------------------------------- /internal/indexer/util.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | // union concatenates, flattens, and deduplicates the given identifier slices. 4 | func union(as ...[]uint64) (flattened []uint64) { 5 | m := map[uint64]struct{}{} 6 | for _, a := range as { 7 | for _, v := range a { 8 | m[v] = struct{}{} 9 | } 10 | } 11 | 12 | for v := range m { 13 | flattened = append(flattened, v) 14 | } 15 | 16 | return flattened 17 | } 18 | -------------------------------------------------------------------------------- /internal/testdata/fixtures/implementations_remote.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | import "net/http" 4 | 5 | type implementsWriter struct{} 6 | 7 | func (implementsWriter) Header() http.Header { panic("Just for how") } 8 | func (implementsWriter) Write([]byte) (int, error) { panic("Just for show") } 9 | func (implementsWriter) WriteHeader(statusCode int) {} 10 | 11 | func ShowsInSignature(respWriter http.ResponseWriter) { 12 | respWriter.WriteHeader(1) 13 | } 14 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Test plan 2 | 3 | 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Releasing `lsif-go` 4 | 5 | To release a new version of lsif-go: 6 | 7 | 1. Update the [Changelog](./CHANGELOG.md) with any relevant changes 8 | 2. Create a new tag and push the tag. 9 | - To see the most recent tag, run: `git describe --tags --abbrev=0` 10 | - To push a new tag, run (while substituing your new tag): `git tag v1.7.5 && git push --tags` 11 | 3. Go to [lsif-go-action](https://github.com/sourcegraph/lsif-go-action) and update to the tagged commit. 12 | -------------------------------------------------------------------------------- /internal/git/toplevel.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/sourcegraph/lsif-go/internal/command" 8 | ) 9 | 10 | // TopLevel returns the root of the git project containing the given directory. 11 | func TopLevel(dir string) (string, error) { 12 | output, err := command.Run(dir, "git", "rev-parse", "--show-toplevel") 13 | if err != nil { 14 | return "", fmt.Errorf("failed to get toplevel: %v\n%s", err, output) 15 | } 16 | 17 | return strings.TrimSpace(string(output)), nil 18 | } 19 | -------------------------------------------------------------------------------- /internal/testdata/fixtures/conflicting_test_symbols/sandbox_unsupported_test.go: -------------------------------------------------------------------------------- 1 | // From https://github.com/moby/moby/blob/master/libnetwork/osl/sandbox_unsupported_test.go 2 | // Build tag constraints removed here to ensure this code is tested on CI. 3 | 4 | package osl 5 | 6 | import ( 7 | "errors" 8 | "testing" 9 | ) 10 | 11 | var ErrNotImplemented = errors.New("not implemented") 12 | 13 | func newKey(t *testing.T) (string, error) { 14 | return "", ErrNotImplemented 15 | } 16 | 17 | func verifySandbox(t *testing.T, s Sandbox) { 18 | return 19 | } 20 | -------------------------------------------------------------------------------- /internal/command/command.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "os/exec" 5 | "strings" 6 | ) 7 | 8 | // Run runs the given command using the given working directory. If the command succeeds, 9 | // the value of stdout is returned with trailing whitespace removed. If the command fails, 10 | // the combined stdout/stderr text will also be returned. 11 | func Run(dir, command string, args ...string) (string, error) { 12 | cmd := exec.Command(command, args...) 13 | cmd.Dir = dir 14 | 15 | out, err := cmd.CombinedOutput() 16 | return strings.TrimSpace(string(out)), err 17 | } 18 | -------------------------------------------------------------------------------- /internal/indexer/util_test.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | "sort" 5 | "testing" 6 | 7 | "github.com/google/go-cmp/cmp" 8 | ) 9 | 10 | func TestUnion(t *testing.T) { 11 | u := union( 12 | []uint64{10, 20, 30}, 13 | []uint64{100, 200, 300}, 14 | []uint64{10, 200, 3000}, 15 | ) 16 | sort.Slice(u, func(i, j int) bool { 17 | return u[i] < u[j] 18 | }) 19 | 20 | expected := []uint64{ 21 | 10, 20, 30, 22 | 100, 200, 300, 23 | 3000, 24 | } 25 | 26 | if diff := cmp.Diff(expected, u); diff != "" { 27 | t.Errorf("unexpected union (-want +got): %s", diff) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /internal/testdata/fixtures/implementation_methods.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | type InterfaceWithSingleMethod interface { 4 | SingleMethod() float64 5 | } 6 | 7 | type StructWithMethods struct{} 8 | 9 | func (StructWithMethods) SingleMethod() float64 { return 5.0 } 10 | 11 | type InterfaceWithSingleMethodTwoImplementers interface { 12 | SingleMethodTwoImpl() float64 13 | } 14 | 15 | type TwoImplOne struct{} 16 | 17 | func (TwoImplOne) SingleMethodTwoImpl() float64 { return 5.0 } 18 | 19 | type TwoImplTwo struct{} 20 | 21 | func (TwoImplTwo) SingleMethodTwoImpl() float64 { return 5.0 } 22 | func (TwoImplTwo) RandomThingThatDoesntMatter() float64 { return 5.0 } 23 | -------------------------------------------------------------------------------- /internal/indexer/package_data_cache_test.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import "testing" 4 | 5 | func TestPackageDataCache(t *testing.T) { 6 | packages := getTestPackages(t) 7 | p, obj := findDefinitionByName(t, packages, "ParallelizableFunc") 8 | 9 | expectedText := normalizeDocstring(` 10 | ParallelizableFunc is a function that can be called concurrently with other instances 11 | of this function type. 12 | `) 13 | 14 | if text := normalizeDocstring(NewPackageDataCache().Text(p, obj.Pos())); text != "" { 15 | if text != expectedText { 16 | t.Errorf("unexpected hover text. want=%q have=%q", expectedText, text) 17 | } 18 | 19 | return 20 | } 21 | 22 | t.Fatalf("did not find target name") 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/pr-auditor.yml: -------------------------------------------------------------------------------- 1 | # See https://docs.sourcegraph.com/dev/background-information/ci#pr-auditor 2 | name: pr-auditor 3 | on: 4 | pull_request_target: 5 | types: [ closed, edited, opened, synchronize, ready_for_review ] 6 | 7 | jobs: 8 | check-pr: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | with: 13 | repository: 'sourcegraph/pr-auditor' 14 | - uses: actions/setup-go@v4 15 | with: { go-version: '1.20' } 16 | 17 | - run: './check-pr.sh' 18 | env: 19 | GITHUB_EVENT_PATH: ${{ env.GITHUB_EVENT_PATH }} 20 | GITHUB_TOKEN: ${{ github.token }} 21 | GITHUB_RUN_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} 22 | -------------------------------------------------------------------------------- /internal/testdata/fixtures/illegal_multiple_mains/main.go: -------------------------------------------------------------------------------- 1 | // File copied from: https://github.com/golang/go/blob/master/test/mainsig.go 2 | 3 | // errorcheck 4 | 5 | // Copyright 2020 The Go Authors. All rights reserved. 6 | // Use of this source code is governed by a BSD-style 7 | // license that can be found in the LICENSE file. 8 | 9 | package main 10 | 11 | func main(int) {} // ERROR "func main must have no arguments and no return values" 12 | func main() int { return 1 } // ERROR "func main must have no arguments and no return values" "main redeclared in this block" 13 | 14 | func init(int) {} // ERROR "func init must have no arguments and no return values" 15 | func init() int { return 1 } // ERROR "func init must have no arguments and no return values" 16 | -------------------------------------------------------------------------------- /internal/gomod/stdlib_test.go: -------------------------------------------------------------------------------- 1 | package gomod 2 | 3 | import "testing" 4 | 5 | func TestStdLib(t *testing.T) { 6 | expectedStdlib := []string{ 7 | "fmt", 8 | "database/sql", 9 | "net/http/httptrace", 10 | } 11 | 12 | for _, testCase := range expectedStdlib { 13 | if !isStandardlibPackge(testCase) { 14 | t.Errorf(`"%s" should be marked as a standard library package`, testCase) 15 | } 16 | } 17 | 18 | expectedUserlib := []string{ 19 | "github.com/sourcegraph/lsif-go/internal/command", 20 | "github.com/sourcegraph/lsif-go/internal/output", 21 | "myCustomName/hello", 22 | } 23 | 24 | for _, testCase := range expectedUserlib { 25 | if isStandardlibPackge(testCase) { 26 | t.Errorf(`"%s" should not be marked as a standard library package`, testCase) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /internal/parallel/parallel.go: -------------------------------------------------------------------------------- 1 | package parallel 2 | 3 | import ( 4 | "runtime" 5 | "sync" 6 | "sync/atomic" 7 | ) 8 | 9 | // Run will run the functions read from the given channel concurrently. This function 10 | // returns a wait group synchronized on the invocation functions, a channel on which any error 11 | // values are written, and a pointer to the number of tasks that have completed, which is 12 | // updated atomically. 13 | func Run(ch <-chan func()) (*sync.WaitGroup, *uint64) { 14 | var count uint64 15 | var wg sync.WaitGroup 16 | 17 | for i := 0; i < runtime.GOMAXPROCS(0); i++ { 18 | wg.Add(1) 19 | 20 | go func() { 21 | defer wg.Done() 22 | 23 | for fn := range ch { 24 | fn() 25 | atomic.AddUint64(&count, 1) 26 | } 27 | }() 28 | } 29 | 30 | return &wg, &count 31 | } 32 | -------------------------------------------------------------------------------- /internal/testdata/fixtures/conflicting_test_symbols/sandbox_unsupported.go: -------------------------------------------------------------------------------- 1 | // From https://github.com/moby/moby/blob/master/libnetwork/osl/sandbox_unsupported.go 2 | // Build tag constraints removed here to ensure this code is tested on CI. 3 | 4 | package osl 5 | 6 | import "errors" 7 | 8 | var ( 9 | // ErrNotImplemented is for platforms which don't implement sandbox 10 | ErrNotImplemented = errors.New("not implemented") 11 | ) 12 | 13 | // NewSandbox provides a new sandbox instance created in an os specific way 14 | // provided a key which uniquely identifies the sandbox 15 | func NewSandbox(key string, osCreate, isRestore bool) (Sandbox, error) { 16 | return nil, ErrNotImplemented 17 | } 18 | 19 | // GenerateKey generates a sandbox key based on the passed 20 | // container id. 21 | func GenerateKey(containerID string) string { 22 | return "" 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | goreleaser: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - 13 | name: Checkout 14 | uses: actions/checkout@v3 15 | - 16 | name: Unshallow 17 | run: git fetch --prune --unshallow 18 | - 19 | name: Set up Go 20 | uses: actions/setup-go@v1 21 | with: 22 | go-version: 1.18.x 23 | - uses: azure/docker-login@v1 24 | with: 25 | username: ${{ secrets.DOCKER_USERNAME }} 26 | password: ${{ secrets.DOCKER_PASSWORD }} 27 | - 28 | name: Run GoReleaser 29 | uses: goreleaser/goreleaser-action@v1 30 | with: 31 | version: v1.8.3 32 | args: release --rm-dist 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | -------------------------------------------------------------------------------- /internal/git/infer_module_version.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/sourcegraph/lsif-go/internal/command" 7 | ) 8 | 9 | // InferModuleVersion returns the version of the module declared in the given 10 | // directory. This will be either the work tree commit's tag, or it will be the 11 | // most recent tag with a short revhash appended to it. 12 | func InferModuleVersion(dir string) (string, error) { 13 | version, err := command.Run(dir, "git", "tag", "-l", "--points-at", "HEAD") 14 | if err != nil { 15 | return "", fmt.Errorf("failed to tags for current commit: %v\n%s", err, version) 16 | } 17 | if version != "" { 18 | return version, nil 19 | } 20 | 21 | commit, err := command.Run(dir, "git", "rev-parse", "HEAD") 22 | if err != nil { 23 | return "", fmt.Errorf("failed to get current commit: %v\n%s", err, commit) 24 | } 25 | 26 | return commit[:12], nil 27 | } 28 | -------------------------------------------------------------------------------- /internal/testdata/fixtures/parallel.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | ) 7 | 8 | // ParallelizableFunc is a function that can be called concurrently with other instances 9 | // of this function type. 10 | type ParallelizableFunc func(ctx context.Context) error 11 | 12 | // Parallel invokes each of the given parallelizable functions in their own goroutines and 13 | // returns the first error to occur. This method will block until all goroutines have returned. 14 | func Parallel(ctx context.Context, fns ...ParallelizableFunc) error { 15 | var wg sync.WaitGroup 16 | errs := make(chan error, len(fns)) 17 | 18 | for _, fn := range fns { 19 | wg.Add(1) 20 | 21 | go func(fn ParallelizableFunc) { 22 | errs <- fn(ctx) 23 | wg.Done() 24 | }(fn) 25 | } 26 | 27 | wg.Wait() 28 | 29 | for err := range errs { 30 | if err != nil { 31 | return err 32 | } 33 | } 34 | 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: lsif-go 2 | 3 | dist: release 4 | 5 | env: 6 | - GO111MODULE=on 7 | - CGO_ENABLED=0 8 | 9 | before: 10 | hooks: 11 | - go mod download 12 | - go mod tidy 13 | 14 | builds: 15 | - 16 | main: ./cmd/lsif-go/ 17 | binary: lsif-go 18 | ldflags: 19 | - -X main.version={{.Version}} 20 | goos: 21 | - linux 22 | - windows 23 | - darwin 24 | goarch: 25 | - amd64 26 | - arm64 27 | 28 | archives: 29 | - id: tarball 30 | format: tar.gz 31 | - id: bin 32 | format: binary 33 | wrap_in_directory: false 34 | name_template: "src_{{ .Os }}_{{ .Arch }}" 35 | 36 | dockers: 37 | - ids: 38 | - lsif-go 39 | image_templates: 40 | - "sourcegraph/lsif-go:{{ .Tag }}" 41 | - "sourcegraph/lsif-go:v{{ .Major }}" 42 | - "sourcegraph/lsif-go:v{{ .Major }}.{{ .Minor }}" 43 | - "sourcegraph/lsif-go:latest" 44 | 45 | changelog: 46 | sort: asc 47 | filters: 48 | exclude: 49 | - '^docs:' 50 | - '^test:' 51 | -------------------------------------------------------------------------------- /scripts/gen_stdlib_map.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | cat > ./internal/gomod/stdlib.go << EOT 5 | // THIS FILE IS GENERATED. SEE ./scripts/gen_stdlib_map.sh 6 | EOT 7 | 8 | echo "// Generated by: $(go version)" >> ./internal/gomod/stdlib.go 9 | 10 | cat >> ./internal/gomod/stdlib.go << EOT 11 | package gomod 12 | 13 | // isStandardlibPackge determines whether a package is in the standard library 14 | // or not. At this point, it checks whether the package name is one of those 15 | // that is found from running "go list std" in the latest released go version. 16 | func isStandardlibPackge(pkg string) bool { 17 | _, ok := standardLibraryMap[pkg] 18 | return ok 19 | } 20 | 21 | var contained = struct{}{} 22 | 23 | // This list is calculated from "go list std". 24 | var standardLibraryMap = map[string]interface{}{ 25 | EOT 26 | go list std | awk '{ print "\""$0"\": contained,"}' >> ./internal/gomod/stdlib.go 27 | echo "}" >> ./internal/gomod/stdlib.go 28 | 29 | go fmt ./internal/gomod/stdlib.go 30 | -------------------------------------------------------------------------------- /.github/workflows/project-board.yml: -------------------------------------------------------------------------------- 1 | name: Project Board 2 | on: 3 | issues: 4 | types: 5 | - opened 6 | jobs: 7 | # Uses issues beta API - see https://docs.github.com/en/issues/trying-out-the-new-projects-experience/automating-projects#example-workflow 8 | code-intel-board: 9 | runs-on: ubuntu-latest 10 | env: 11 | PROJECT_ID: MDExOlByb2plY3ROZXh0NDI1MA== # https://github.com/orgs/sourcegraph/projects/211 12 | GITHUB_TOKEN: ${{ secrets.GH_PROJECTS_ACTION_TOKEN }} 13 | steps: 14 | - name: Add to board 15 | env: 16 | NODE_ID: ${{ github.event.issue.node_id }} 17 | run: | 18 | gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query=' 19 | mutation($project:ID!, $node_id:ID!) { 20 | addProjectNextItem(input: {projectId: $project, contentId: $node_id}) { 21 | projectNextItem { 22 | id 23 | } 24 | } 25 | }' -f project=$PROJECT_ID -f node_id=$NODE_ID 26 | -------------------------------------------------------------------------------- /internal/testdata/fixtures/implementations.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | type I0 interface{} 4 | 5 | type I1 interface { 6 | F1() 7 | } 8 | 9 | type I2 interface { 10 | F2() 11 | } 12 | 13 | type T1 int 14 | 15 | func (r T1) F1() {} 16 | 17 | type T2 int 18 | 19 | func (r T2) F1() {} 20 | func (r T2) F2() {} 21 | 22 | type A1 = T1 23 | type A12 = A1 24 | 25 | type InterfaceWithNonExportedMethod interface { 26 | nonExportedMethod() 27 | } 28 | 29 | type InterfaceWithExportedMethod interface { 30 | ExportedMethod() 31 | } 32 | 33 | type Foo int 34 | 35 | func (r Foo) nonExportedMethod() {} 36 | func (r Foo) ExportedMethod() {} 37 | func (r Foo) Close() error { return nil } 38 | 39 | type SharedOne interface { 40 | Shared() 41 | Distinct() 42 | } 43 | 44 | type SharedTwo interface { 45 | Shared() 46 | Unique() 47 | } 48 | 49 | type Between struct{} 50 | 51 | func (Between) Shared() {} 52 | func (Between) Distinct() {} 53 | func (Between) Unique() {} 54 | 55 | func shouldShow(shared SharedOne) { 56 | shared.Shared() 57 | } 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 ᴊ. ᴄʜᴇɴ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cmd/lsif-go/stats.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | "github.com/sourcegraph/lsif-go/internal/indexer" 9 | "github.com/sourcegraph/lsif-go/internal/util" 10 | ) 11 | 12 | func displayStats(indexerStats indexer.IndexerStats, packageDataCacheStats indexer.PackageDataCacheStats, start time.Time) { 13 | stats := []struct { 14 | name string 15 | value string 16 | }{ 17 | {"Wall time elapsed", fmt.Sprintf("%s", util.HumanElapsed(start))}, 18 | {"Packages indexed", fmt.Sprintf("%d", indexerStats.NumPkgs)}, 19 | {"Files indexed", fmt.Sprintf("%d", indexerStats.NumFiles)}, 20 | {"Definitions indexed", fmt.Sprintf("%d", indexerStats.NumDefs)}, 21 | {"Elements emitted", fmt.Sprintf("%d", indexerStats.NumElements)}, 22 | {"Packages traversed", fmt.Sprintf("%d", packageDataCacheStats.NumPks)}, 23 | } 24 | 25 | n := 0 26 | for _, stat := range stats { 27 | if n < len(stat.name) { 28 | n = len(stat.name) 29 | } 30 | } 31 | 32 | fmt.Printf("\nStats:\n") 33 | 34 | for _, stat := range stats { 35 | fmt.Printf("\t%s: %s%s\n", stat.name, strings.Repeat(" ", n-len(stat.name)), stat.value) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /cmd/lsif-go/paths.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strings" 7 | 8 | "github.com/sourcegraph/lsif-go/internal/git" 9 | ) 10 | 11 | var wd = newCachedString(func() string { 12 | if wd, err := os.Getwd(); err == nil { 13 | return wd 14 | } 15 | 16 | return "" 17 | }) 18 | 19 | var toplevel = newCachedString(func() string { 20 | if toplevel, err := git.TopLevel("."); err == nil { 21 | return toplevel 22 | } 23 | 24 | return "" 25 | }) 26 | 27 | func searchForGoMod(path, repositoryRoot string) string { 28 | for ; !strings.HasPrefix(path, repositoryRoot); path = filepath.Dir(path) { 29 | _, err := os.Stat(filepath.Join(path, "go.mod")) 30 | if err == nil { 31 | return rel(path) 32 | } 33 | 34 | if !os.IsNotExist(err) { 35 | // Actual FS error, stop 36 | break 37 | } 38 | 39 | if filepath.Dir(path) == path { 40 | // We just checked the root, prevent infinite loop 41 | break 42 | } 43 | } 44 | 45 | return "." 46 | } 47 | 48 | func rel(path string) string { 49 | relative, err := filepath.Rel(wd.Value(), path) 50 | if err != nil { 51 | return "." 52 | } 53 | 54 | return relative 55 | } 56 | -------------------------------------------------------------------------------- /internal/parallel/parallel_test.go: -------------------------------------------------------------------------------- 1 | package parallel 2 | 3 | import ( 4 | "sync/atomic" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestRun(t *testing.T) { 10 | ch := make(chan func(), 3) 11 | ch <- func() {} 12 | ch <- func() {} 13 | ch <- func() {} 14 | close(ch) 15 | 16 | wg, n := Run(ch) 17 | wg.Wait() 18 | 19 | if *n != 3 { 20 | t.Errorf("unexpected count. want=%d want=%d", 3, *n) 21 | } 22 | } 23 | 24 | func TestRunProgress(t *testing.T) { 25 | sync1 := make(chan struct{}) 26 | sync2 := make(chan struct{}) 27 | sync3 := make(chan struct{}) 28 | 29 | ch := make(chan func(), 3) 30 | ch <- func() { <-sync1 } 31 | ch <- func() { <-sync2 } 32 | ch <- func() { <-sync3 } 33 | close(ch) 34 | 35 | wg, n := Run(ch) 36 | 37 | checkValue := func(expected uint64) { 38 | var v uint64 39 | 40 | for i := 0; i < 10; i++ { 41 | if v = atomic.LoadUint64(n); v == expected { 42 | return 43 | } 44 | 45 | <-time.After(time.Millisecond) 46 | } 47 | 48 | t.Fatalf("unexpected progress value. want=%d have=%d", expected, v) 49 | } 50 | 51 | checkValue(0) 52 | close(sync1) 53 | checkValue(1) 54 | close(sync2) 55 | checkValue(2) 56 | close(sync3) 57 | checkValue(3) 58 | wg.Wait() 59 | } 60 | -------------------------------------------------------------------------------- /internal/git/infer_repo.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "strings" 7 | 8 | "github.com/sourcegraph/lsif-go/internal/command" 9 | ) 10 | 11 | // InferRepo gets a human-readable repository name from the git clone enclosing 12 | // the given directory. 13 | func InferRepo(dir string) (string, error) { 14 | remoteURL, err := command.Run(dir, "git", "remote", "get-url", "origin") 15 | if err != nil { 16 | return "", err 17 | } 18 | 19 | return parseRemote(remoteURL) 20 | } 21 | 22 | // parseRemote converts a git origin url into a Sourcegraph-friendly repo name. 23 | func parseRemote(remoteURL string) (string, error) { 24 | // e.g., git@github.com:sourcegraph/lsif-go.git 25 | if strings.HasPrefix(remoteURL, "git@") { 26 | if parts := strings.Split(remoteURL, ":"); len(parts) == 2 { 27 | return strings.Join([]string{ 28 | strings.TrimPrefix(parts[0], "git@"), 29 | strings.TrimSuffix(parts[1], ".git"), 30 | }, "/"), nil 31 | } 32 | } 33 | 34 | // e.g., https://github.com/sourcegraph/lsif-go.git 35 | if url, err := url.Parse(remoteURL); err == nil { 36 | return url.Hostname() + strings.TrimSuffix(url.Path, ".git"), nil 37 | } 38 | 39 | return "", fmt.Errorf("unrecognized remote URL: %s", remoteURL) 40 | } 41 | -------------------------------------------------------------------------------- /docs/structs.md: -------------------------------------------------------------------------------- 1 | # Structs 2 | 3 | Structs are generally implemented in a relatively straightforward way. 4 | 5 | For example: 6 | 7 | ```go 8 | type MyStruct struct { 9 | Cli http.Client 10 | ^^^----------------- definition MyStruct.Cli 11 | ^^^^------------ reference github.com/golang/go/std/http 12 | ^^^^^^----- reference github.com/golang/go/std/http.Client 13 | } 14 | 15 | ``` 16 | 17 | But, for anonymous fields, it is a little more complicated, and ends up looking something like this. 18 | 19 | ```go 20 | type NestedHandler struct { 21 | LocalItem 22 | ^^^^^^^^^-------- definition MyStruct.LocalItem 23 | ^^^^^^^^^-------- reference LocalItem 24 | } 25 | ``` 26 | 27 | In this case it is possible to have the same ranges overlapping, so `lsif-go` 28 | will re-use the same range. 29 | 30 | However, in the following case, we have three separate ranges that, while they overlap 31 | are not identical, so they cannot be shared and a new range must be created. 32 | 33 | ```go 34 | type Nested struct { 35 | http.Handler 36 | ^^^^^^^^^^^^-------- definition Nested.Handler 37 | ^^^^---------------- reference github.com/golang/go/std/http 38 | ^^^^^^^-------- reference github.com/golang/go/std/http.Handler 39 | } 40 | ``` 41 | -------------------------------------------------------------------------------- /internal/git/infer_repo_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestInferRepo(t *testing.T) { 9 | repo, err := InferRepo("") 10 | if err != nil { 11 | t.Fatalf("unexpected error inferring repo: %s", err) 12 | } 13 | 14 | if repo != "github.com/sourcegraph/lsif-go" { 15 | t.Errorf("unexpected remote repo. want=%q have=%q", "github.com/sourcegraph/lsif-go", repo) 16 | } 17 | } 18 | 19 | func TestParseRemote(t *testing.T) { 20 | testCases := map[string]string{ 21 | "git@github.com:sourcegraph/lsif-go.git": "github.com/sourcegraph/lsif-go", 22 | "https://github.com/sourcegraph/lsif-go": "github.com/sourcegraph/lsif-go", 23 | "ssh://git@phabricator.company.com:2222/diffusion/COMPANY/companay.git": "phabricator.company.com/diffusion/COMPANY/companay", 24 | } 25 | 26 | for input, expectedOutput := range testCases { 27 | t.Run(fmt.Sprintf("input=%q", input), func(t *testing.T) { 28 | output, err := parseRemote(input) 29 | if err != nil { 30 | t.Fatalf("unexpected error parsing remote: %s", err) 31 | } 32 | 33 | if output != expectedOutput { 34 | t.Errorf("unexpected repo name. want=%q have=%q", expectedOutput, output) 35 | } 36 | }) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /internal/util/duration.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | var durations = []time.Duration{ 8 | time.Nanosecond, 9 | time.Microsecond, 10 | time.Millisecond, 11 | time.Second, 12 | time.Minute, 13 | time.Hour, 14 | } 15 | 16 | // HumanElapsed returns the time elapsed since the given start time truncated 17 | // to 100x the highest non-zero duration unit (ns, us, ms, ...). This tends to 18 | // create very short duration strings when printed (e.g. 725.8ms) without having 19 | // to fiddle too much with 20 | func HumanElapsed(start time.Time) time.Duration { 21 | return humanElapsed(time.Since(start)) 22 | } 23 | 24 | func humanElapsed(elapsed time.Duration) time.Duration { 25 | i := 0 26 | for i < len(durations) && elapsed >= durations[i] { 27 | i++ 28 | } 29 | 30 | if i >= 2 { 31 | // Truncate to the next duration unit 32 | resolution := durations[i-2] 33 | 34 | if (durations[i-1] / durations[i-2]) > 100 { 35 | // If we're going from ns -> us, us -> ms, ms -> s, 36 | // then we want to have two decimal points of precision 37 | // here. Not doing this for s -> m or m -> h is fine as 38 | // there will already be this much precision. 39 | resolution *= 10 40 | } 41 | 42 | return elapsed.Truncate(resolution) 43 | } 44 | 45 | return elapsed 46 | } 47 | -------------------------------------------------------------------------------- /internal/gomod/module_name_test.go: -------------------------------------------------------------------------------- 1 | package gomod 2 | 3 | import "testing" 4 | 5 | func TestResolveModuleName(t *testing.T) { 6 | testCases := []struct { 7 | repo string 8 | name string 9 | expected string 10 | }{ 11 | { 12 | repo: "github.com/sourcegraph/sourcegraph", 13 | name: "github.com/sourcegraph/sourcegraph", 14 | expected: "https://github.com/sourcegraph/sourcegraph", 15 | }, 16 | { 17 | repo: "github.com/sourcegraph/zoekt", // forked repo 18 | name: "github.com/google/zoekt", // declared module 19 | expected: "https://github.com/sourcegraph/zoekt", 20 | }, 21 | 22 | { 23 | repo: "github.com/sourcegraph/zoekt", 24 | name: "github.com/google/zoekt/some/sub/path", 25 | expected: "https://github.com/sourcegraph/zoekt/some/sub/path", 26 | }, 27 | 28 | { 29 | repo: "github.com/golang/go", 30 | name: "std", 31 | expected: "https://github.com/golang/go", 32 | }, 33 | } 34 | 35 | for _, testCase := range testCases { 36 | if actual, _, err := resolveModuleName(testCase.repo, testCase.name); err != nil { 37 | t.Fatalf("unexpected error: %s", err) 38 | } else if actual != testCase.expected { 39 | t.Errorf("unexpected module name. want=%q have=%q", testCase.expected, actual) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /internal/util/duration_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestHumanElapsed(t *testing.T) { 9 | testCases := []struct { 10 | input time.Duration 11 | expected time.Duration 12 | }{ 13 | { 14 | input: time.Nanosecond * 123, 15 | expected: time.Nanosecond * 123, 16 | }, 17 | 18 | { 19 | input: time.Microsecond*5 + time.Nanosecond*123, 20 | expected: time.Microsecond*5 + time.Nanosecond*120, 21 | }, 22 | { 23 | input: time.Millisecond*5 + time.Microsecond*123 + time.Nanosecond*123, 24 | expected: time.Millisecond*5 + time.Microsecond*120, 25 | }, 26 | { 27 | input: time.Second*5 + time.Millisecond*123 + time.Microsecond*123, 28 | expected: time.Second*5 + time.Millisecond*120, 29 | }, 30 | { 31 | input: time.Minute*5 + time.Second*12 + time.Millisecond*123, 32 | expected: time.Minute*5 + time.Second*12, 33 | }, 34 | { 35 | input: time.Hour*5 + time.Minute*12 + time.Second*12, 36 | expected: time.Hour*5 + time.Minute*12, 37 | }, 38 | } 39 | 40 | for _, testCase := range testCases { 41 | if actual := humanElapsed(testCase.input); actual != testCase.expected { 42 | t.Errorf("unexpected duration for %s. want=%q have=%q", testCase.input, testCase.expected, actual) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /internal/indexer/striped_mutex.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | const NumLockStripes = 512 8 | 9 | type StripedMutex struct { 10 | mutex sync.RWMutex 11 | keys map[string]uint64 12 | locks []*sync.RWMutex 13 | } 14 | 15 | func newStripedMutex() *StripedMutex { 16 | locks := make([]*sync.RWMutex, NumLockStripes) 17 | for i := range locks { 18 | locks[i] = &sync.RWMutex{} 19 | } 20 | 21 | return &StripedMutex{ 22 | keys: map[string]uint64{}, 23 | locks: locks, 24 | } 25 | } 26 | 27 | func (m *StripedMutex) LockKey(v string) { m.mutexForKey(v).Lock() } 28 | func (m *StripedMutex) UnlockKey(v string) { m.mutexForKey(v).Unlock() } 29 | func (m *StripedMutex) RLockKey(v string) { m.mutexForKey(v).RLock() } 30 | func (m *StripedMutex) RUnlockKey(v string) { m.mutexForKey(v).RUnlock() } 31 | 32 | func (m *StripedMutex) mutexForIndex(v uint64) *sync.RWMutex { 33 | return m.locks[int(v)%len(m.locks)] 34 | } 35 | 36 | func (m *StripedMutex) mutexForKey(v string) *sync.RWMutex { 37 | return m.mutexForIndex(m.indexFor(v)) 38 | } 39 | 40 | func (m *StripedMutex) indexFor(v string) uint64 { 41 | m.mutex.RLock() 42 | key, ok := m.keys[v] 43 | m.mutex.RUnlock() 44 | if ok { 45 | return key 46 | } 47 | 48 | m.mutex.Lock() 49 | defer m.mutex.Unlock() 50 | 51 | if key, ok := m.keys[v]; ok { 52 | return key 53 | } 54 | 55 | key = uint64(len(m.keys)) 56 | m.keys[v] = key 57 | return key 58 | } 59 | -------------------------------------------------------------------------------- /docs/imports.md: -------------------------------------------------------------------------------- 1 | # Imports 2 | 3 | There are two types of imports available in Go. In both cases, we generate the same reference 4 | to the package itself. This is done by creating an importMoniker. This import moniker 5 | 6 | ```go 7 | import "fmt" 8 | // ^^^------ reference github.com/golang/go/std/fmt 9 | 10 | import f "fmt" 11 | // ^--------- local definition 12 | // ^^^---- reference github.com/golang/go/std/fmt 13 | 14 | 15 | // Special Case, "." generates no local def 16 | import . "fmt" 17 | // no local def 18 | // ^^^---- reference github.com/golang/go/std/fmt 19 | ``` 20 | 21 | ## Example 22 | 23 | So given this kind of import, you will see the following. 24 | 25 | ```go 26 | import ( 27 | "fmt" 28 | . "net/http" 29 | s "sort" 30 | ) 31 | ``` 32 | 33 | - Regular `"fmt"` import. Creates only a reference to the moniker 34 | 35 | ![fmt_import](/docs/media/fmt_import.png) 36 | 37 | - Named `s "sort"` import. Creates both a reference and a definition. Any local 38 | references to `s` in this case will link back to the definition of this import. 39 | `"sort"` will still link to the external package. 40 | 41 | ![sort_import](/docs/media/sort_import.png) 42 | 43 | ![s_definition](/docs/media/s_definition.png) 44 | 45 | - `.` import. This will also only create a reference, because `.` does not 46 | create a new definition. It just pulls it into scope. 47 | 48 | ![http_import](/docs/media/http_import.png) 49 | -------------------------------------------------------------------------------- /docs/package_declarations.md: -------------------------------------------------------------------------------- 1 | # Package Declarations 2 | 3 | 4 | In general, we have used `types.*` structs that match the `types.Object` 5 | interface. However there was no struct that represented the statement: 6 | 7 | ```go 8 | package mypkg 9 | ``` 10 | 11 | That's the because the majority of the information is held in `types.Package` 12 | and the corresponding definition in `packages.Package.Syntax`. 13 | 14 | Since there was no types.PkgDeclaration or similar available, we created our own. 15 | See [types.go](/internal/indexer/types.go) 16 | 17 | ## Definition vs. Reference 18 | 19 | We only emit one definition for a package declaration. The way we pick this is detailed 20 | in `findBestPackageDefinitionPath(...)`. For the `package mypkg`, only the "best" is 21 | picked as the defintion, the other are all emitted as references. This makes sure that we 22 | always jump to the best package declaration when jumping between packages. 23 | 24 | For example, if we have a project that contains two files: 25 | - [lib.go](/docs/examples/smollest/lib.go) 26 | - [sub.go](/docs/examples/smollest/sub.go) 27 | 28 | In this case the project is literally just two 29 | package declarations. The lsif graph will look like this (some nodes removed): 30 | 31 | ![smollest_graph](/docs/examples/smollest/dump.svg) 32 | 33 | NOTE: the two ranges point to the same resultSet but only one of the ranges 34 | (the range from the `lib.go` file) is chosen as the result for the definition 35 | request. 36 | 37 | -------------------------------------------------------------------------------- /internal/indexer/info.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import "sync" 4 | 5 | // IndexerStats summarizes the amount of work done by the indexer. 6 | type IndexerStats struct { 7 | NumPkgs uint 8 | NumFiles uint 9 | NumDefs uint 10 | NumElements uint64 11 | } 12 | 13 | // PackageDataCacheStats summarizes the amount of work done by the package data cache. 14 | type PackageDataCacheStats struct { 15 | NumPks uint 16 | } 17 | 18 | // DocumentInfo provides context for constructing the contains relationship between 19 | // a document and the ranges that it contains. 20 | type DocumentInfo struct { 21 | DocumentID uint64 22 | DefinitionRangeIDs []uint64 23 | ReferenceRangeIDs []uint64 24 | m sync.Mutex 25 | } 26 | 27 | func (document *DocumentInfo) appendDefinition(rangeID uint64) { 28 | document.m.Lock() 29 | document.DefinitionRangeIDs = append(document.DefinitionRangeIDs, rangeID) 30 | document.m.Unlock() 31 | } 32 | 33 | func (document *DocumentInfo) appendReference(rangeID uint64) { 34 | document.m.Lock() 35 | document.ReferenceRangeIDs = append(document.ReferenceRangeIDs, rangeID) 36 | document.m.Unlock() 37 | } 38 | 39 | // DefinitionInfo provides context about a range that defines an identifier. An object 40 | // of this shape is keyed by type and identifier in the indexer so that it can be 41 | // re-retrieved for a range that uses the definition. 42 | type DefinitionInfo struct { 43 | DocumentID uint64 44 | RangeID uint64 45 | ResultSetID uint64 46 | DefinitionResultID uint64 47 | ReferenceRangeIDs map[uint64][]uint64 48 | TypeSwitchHeader bool 49 | m sync.Mutex 50 | } 51 | -------------------------------------------------------------------------------- /OLD_README.md: -------------------------------------------------------------------------------- 1 | # Go LSIF indexer ![](https://img.shields.io/badge/status-ready-brightgreen) 2 | 3 | Visit https://lsif.dev/ to learn about LSIF. 4 | 5 | ## Installation 6 | 7 | Binary downloads are available on the [releases tab](https://github.com/sourcegraph/lsif-go/releases). 8 | 9 | ### Installation: Linux 10 | 11 | ``` 12 | curl -L https://github.com/sourcegraph/lsif-go/releases/download/v1.2.0/src_linux_amd64 -o /usr/local/bin/lsif-go 13 | chmod +x /usr/local/bin/lsif-go 14 | ``` 15 | 16 | ### Installation: MacOS 17 | 18 | ``` 19 | curl -L https://github.com/sourcegraph/lsif-go/releases/download/v1.2.0/src_darwin_amd64 -o /usr/local/bin/lsif-go 20 | chmod +x /usr/local/bin/lsif-go 21 | ``` 22 | 23 | ### Installation: Docker 24 | 25 | ``` 26 | docker pull sourcegraph/lsif-go:v1.2.0 27 | ``` 28 | 29 | ## Indexing your repository 30 | 31 | After installing `lsif-go` onto your PATH, run the command in the root where your `go.mod` file is located. 32 | 33 | ``` 34 | $ lsif-go -v 35 | ✔ Loading packages... Done (753.22ms) 36 | ✔ Emitting documents... Done (72.76µs) 37 | ✔ Adding import definitions... Done (86.24µs) 38 | ✔ Indexing definitions... Done (16.83ms) 39 | ✔ Indexing references... Done (93.36ms) 40 | ✔ Linking items to definitions... Done (8.46ms) 41 | ✔ Emitting contains relations... Done (294.13µs) 42 | 43 | Stats: 44 | Wall time elapsed: 873.2ms 45 | Packages indexed: 14 46 | Files indexed: 53 47 | Definitions indexed: 1756 48 | Elements emitted: 35718 49 | Packages traversed: 40 50 | ``` 51 | 52 | If lsif-go is using too much memory, try setting `--dep-batch-size=100` to only load 100 dependencies into memory at once (~1GB overhead). Lowering the batch size will decrease the overhead further, but increase the runtime a lot more because loading a batch has a fixed cost of ~500ms and each additional package loaded within a batch only adds ~10ms. 53 | 54 | Use `lsif-go --help` for more information. 55 | 56 | ## Updating your index 57 | 58 | To keep your index up-to-date, you can add a step to your CI to generate new data when your repository changes. See [our documentation](https://docs.sourcegraph.com/code_intelligence/how-to/adding_lsif_to_workflows) on adding LSIF to your workflows. 59 | -------------------------------------------------------------------------------- /internal/indexer/protocol.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | "bytes" 5 | "go/token" 6 | "go/types" 7 | "strings" 8 | 9 | doc "github.com/slimsag/godocmd" 10 | protocol "github.com/sourcegraph/sourcegraph/lib/codeintel/lsif/protocol" 11 | ) 12 | 13 | const languageGo = "go" 14 | 15 | // rangeForObject transforms the position of the given object (1-indexed) into an LSP range 16 | // (0-indexed). If the object is a quoted package name, the leading and trailing quotes are 17 | // stripped from the resulting range's bounds. 18 | func rangeForObject(obj ObjectLike, pos token.Position) (protocol.Pos, protocol.Pos) { 19 | adjustment := 0 20 | if pkgName, ok := obj.(*types.PkgName); ok && strings.HasPrefix(pkgName.Name(), `"`) { 21 | adjustment = 1 22 | } 23 | 24 | line := pos.Line - 1 25 | column := pos.Column - 1 26 | n := len(obj.Name()) 27 | 28 | start := protocol.Pos{Line: line, Character: column + adjustment} 29 | end := protocol.Pos{Line: line, Character: column + n - adjustment} 30 | return start, end 31 | } 32 | 33 | // toMarkupContent creates a protocol.MarkupContent object from the given content. The signature 34 | // and extra parameters are formatted as code, if supplied. The docstring is formatted as markdown, 35 | // if supplied. 36 | func toMarkupContent(signature, docstring, extra string) (mss protocol.MarkupContent) { 37 | var ss []string 38 | 39 | for _, m := range []string{formatCode(signature), formatMarkdown(docstring), formatCode(extra)} { 40 | if m != "" { 41 | ss = append(ss, m) 42 | } 43 | } 44 | 45 | return protocol.NewMarkupContent(strings.Join(ss, "\n\n---\n\n"), protocol.Markdown) 46 | } 47 | 48 | // formatMarkdown creates a string containing a markdown-formatted version 49 | // of the given string. 50 | func formatMarkdown(v string) string { 51 | if v == "" { 52 | return "" 53 | } 54 | 55 | var buf bytes.Buffer 56 | doc.ToMarkdown(&buf, v, nil) 57 | return buf.String() 58 | } 59 | 60 | // formatCode creates a string containing a code fence-formatted version 61 | // of the given string. 62 | func formatCode(v string) string { 63 | if v == "" { 64 | return "" 65 | } 66 | 67 | // reuse MarkedString here as it takes care of code fencing 68 | return protocol.NewMarkedString(v, languageGo).String() 69 | } 70 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sourcegraph/lsif-go 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/agnivade/levenshtein v1.1.1 7 | github.com/alecthomas/kingpin v2.2.6+incompatible 8 | github.com/efritz/pentimento v0.0.0-20190429011147-ade47d831101 9 | github.com/google/go-cmp v0.5.6 10 | github.com/hashicorp/go-multierror v1.1.1 11 | github.com/hexops/autogold v1.3.0 12 | github.com/pkg/errors v0.9.1 13 | github.com/slimsag/godocmd v0.0.0-20161025000126-a1005ad29fe3 14 | github.com/sourcegraph/lsif-static-doc v0.0.0-20210831232443-e74f711cdf06 15 | github.com/sourcegraph/sourcegraph/lib v0.0.0-20210914223954-cff3e4aaa732 16 | golang.org/x/tools v0.1.3 17 | ) 18 | 19 | require ( 20 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect 21 | github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 // indirect 22 | github.com/cockroachdb/errors v1.8.6 // indirect 23 | github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f // indirect 24 | github.com/cockroachdb/redact v1.1.3 // indirect 25 | github.com/cockroachdb/sentry-go v0.6.1-cockroachdb.2 // indirect 26 | github.com/go-stack/stack v1.8.1 // indirect 27 | github.com/gogo/protobuf v1.3.2 // indirect 28 | github.com/google/uuid v1.3.0 // indirect 29 | github.com/hashicorp/errwrap v1.1.0 // indirect 30 | github.com/hexops/gotextdiff v1.0.3 // indirect 31 | github.com/hexops/valast v1.4.0 // indirect 32 | github.com/inconshreveable/log15 v0.0.0-20201112154412-8562bdadbbac // indirect 33 | github.com/json-iterator/go v1.1.11 // indirect 34 | github.com/kr/pretty v0.3.0 // indirect 35 | github.com/kr/text v0.2.0 // indirect 36 | github.com/mattn/go-colorable v0.1.8 // indirect 37 | github.com/mattn/go-isatty v0.0.13 // indirect 38 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 39 | github.com/modern-go/reflect2 v1.0.1 // indirect 40 | github.com/nightlyone/lockfile v1.0.0 // indirect 41 | github.com/rogpeppe/go-internal v1.8.0 // indirect 42 | github.com/shurcooL/go-goon v0.0.0-20210110234559-7585751d9a17 // indirect 43 | golang.org/x/mod v0.4.2 // indirect 44 | golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e // indirect 45 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 46 | mvdan.cc/gofumpt v0.1.1 // indirect 47 | ) 48 | -------------------------------------------------------------------------------- /cmd/lsif-go/index.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | "github.com/sourcegraph/lsif-go/internal/gomod" 9 | "github.com/sourcegraph/lsif-go/internal/indexer" 10 | "github.com/sourcegraph/lsif-go/internal/output" 11 | protocol "github.com/sourcegraph/sourcegraph/lib/codeintel/lsif/protocol" 12 | "github.com/sourcegraph/sourcegraph/lib/codeintel/lsif/protocol/writer" 13 | ) 14 | 15 | func writeIndex(repositoryRoot, repositoryRemote, projectRoot, moduleName, moduleVersion string, dependencies map[string]gomod.GoModule, projectDependencies []string, outFile string, outputOptions output.Options, generationOptions indexer.GenerationOptions) error { 16 | start := time.Now() 17 | 18 | out, err := os.Create(outFile) 19 | if err != nil { 20 | return fmt.Errorf("failed to create dump file: %v", err) 21 | } 22 | defer out.Close() 23 | 24 | toolInfo := protocol.ToolInfo{ 25 | Name: "lsif-go", 26 | Version: version, 27 | Args: os.Args[1:], 28 | } 29 | 30 | packageDataCache := indexer.NewPackageDataCache() 31 | 32 | // TODO(efritz) - With cgo enabled, the indexer cannot handle packages 33 | // that include assembly (.s) files. To index such a package you need to 34 | // set CGO_ENABLED=0. Consider maybe doing this explicitly, always. 35 | indexer := indexer.New( 36 | repositoryRoot, 37 | repositoryRemote, 38 | projectRoot, 39 | toolInfo, 40 | moduleName, 41 | moduleVersion, 42 | dependencies, 43 | projectDependencies, 44 | writer.NewJSONWriter(out), 45 | packageDataCache, 46 | outputOptions, 47 | generationOptions, 48 | ) 49 | 50 | if err := indexer.Index(); err != nil { 51 | return err 52 | } 53 | 54 | if isVerbose() { 55 | displayStats(indexer.Stats(), packageDataCache.Stats(), start) 56 | } 57 | 58 | return nil 59 | } 60 | 61 | var verbosityLevels = map[int]output.Verbosity{ 62 | 0: output.DefaultOutput, 63 | 1: output.VerboseOutput, 64 | 2: output.VeryVerboseOutput, 65 | 3: output.VeryVeryVerboseOutput, 66 | } 67 | 68 | func getVerbosity() output.Verbosity { 69 | if noOutput { 70 | return output.NoOutput 71 | } 72 | 73 | if verbosity >= len(verbosityLevels) { 74 | verbosity = len(verbosityLevels) - 1 75 | } 76 | 77 | return verbosityLevels[verbosity] 78 | } 79 | 80 | func isVerbose() bool { 81 | return getVerbosity() >= output.VerboseOutput 82 | } 83 | -------------------------------------------------------------------------------- /internal/indexer/types.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | "go/types" 7 | 8 | "golang.org/x/tools/go/packages" 9 | ) 10 | 11 | // ObjectLike is effectively just types.Object. We needed an interface that we could actually implement 12 | // since types.Object has unexported fields, so it is unimplementable for our package. 13 | type ObjectLike interface { 14 | Pos() token.Pos 15 | Pkg() *types.Package 16 | Name() string 17 | Type() types.Type 18 | Exported() bool 19 | Id() string 20 | 21 | String() string 22 | } 23 | 24 | // PkgDeclaration is similar to types.PkgName, except that instead of for _imported_ packages 25 | // it is for _declared_ packages. 26 | // 27 | // Generated for: `package name` 28 | // 29 | // For more information, see : docs/package_declarations.md 30 | type PkgDeclaration struct { 31 | pos token.Pos 32 | pkg *types.Package 33 | name string 34 | } 35 | 36 | func (p PkgDeclaration) Pos() token.Pos { return p.pos } 37 | func (p PkgDeclaration) Pkg() *types.Package { return p.pkg } 38 | func (p PkgDeclaration) Name() string { return p.name } 39 | func (p PkgDeclaration) Type() types.Type { return pkgDeclarationType{p} } 40 | func (p PkgDeclaration) Exported() bool { return true } 41 | func (p PkgDeclaration) Id() string { return "pkg:" + p.pkg.Name() + ":" + p.name } 42 | func (p PkgDeclaration) String() string { return "pkg:" + p.pkg.Name() + ":" + p.name } 43 | 44 | // Fulfills types.Type interface 45 | type pkgDeclarationType struct{ decl PkgDeclaration } 46 | 47 | func (p pkgDeclarationType) Underlying() types.Type { return p } 48 | func (p pkgDeclarationType) String() string { return p.decl.Id() } 49 | 50 | var packageLen = len("package ") 51 | 52 | func newPkgDeclaration(p *packages.Package, f *ast.File) (*PkgDeclaration, token.Position) { 53 | // import mypackage 54 | // ^--------------------- pkgKeywordPosition *types.Position 55 | // ^-------------- pkgDeclarationPos *types.Pos 56 | // ^-------------- pkgPosition *types.Position 57 | pkgKeywordPosition := p.Fset.Position(f.Package) 58 | 59 | pkgDeclarationPos := p.Fset.File(f.Package).Pos(pkgKeywordPosition.Offset + packageLen) 60 | pkgPosition := p.Fset.Position(pkgDeclarationPos) 61 | 62 | name := f.Name.Name 63 | 64 | return &PkgDeclaration{ 65 | pos: pkgDeclarationPos, 66 | pkg: types.NewPackage(p.PkgPath, name), 67 | name: name, 68 | }, pkgPosition 69 | } 70 | -------------------------------------------------------------------------------- /BENCHMARK.md: -------------------------------------------------------------------------------- 1 | # Indexer performance 2 | 3 | We ran lsif-go (v1.0.0) over repositories of various sizes to determine the performance characteristics of the indexer as a function of its input. The machine running this benchmark was a iMac Pro (2017) with a 2.3 GHz 8-Core Intel Xeon W and 64GB of RAM. Performance characteristics may differ with a process of a different speed or a different number of cores (especially as the repository size increases). 4 | 5 | | Repo name | Repo size | SLoC | Comment LoC | Time to index | Index size | 6 | | ----------- | --------- | ---------- | ----------- | ------------- | ---------- | 7 | | monorepo-1 | 268M | 1,314,700 | 1,036,663 | 0m 23.808s | 1.4G | 8 | | monorepo-5 | 633M | 6,220,940 | 5,093,927 | 1m 47.697s | 6.9G | 9 | | monorepo-10 | 1.1G | 12,353,740 | 10,165,507 | 3m 34.579s | 13G | 10 | | monorepo-15 | 1.5G | 18,486,540 | 15,237,087 | 8m 12.479s | 20G | 11 | | monorepo-20 | 2.0G | 24,619,340 | 20,308,667 | 13m 0.855s | 27G | 12 | | monorepo-25 | 2.4G | 30,752,140 | 25,380,247 | 18m 52.822s | 33G | 13 | 14 | Notes: 15 | - SLOC = significant lines of code 16 | - Comment LoC = number of comment lines 17 | - Time to index is the average over 5 runs on an otherwise idle machine 18 | - Index size is the size of the (uncompressed) output of the indexer 19 | 20 | #### Source code generation 21 | 22 | The source code used for indexing was generated from the following script. This will clone the Go AWS SDK (which is already a large-ish repository with many packages and large generated files with many symbols) and replicate the `services` directory a number of times to artificially expand the size of the repository. 23 | 24 | ```bash 25 | #!/bin/bash -exu 26 | 27 | N=${1:-5} 28 | git clone git@github.com:aws/aws-sdk-go.git "monorepo-$N" 29 | pushd "monorepo-$N" 30 | 31 | mv service service1 32 | find . -type f -name '*.go' -exec \ 33 | sed -i '' \ 34 | -e 's/github.com\/aws\/aws-sdk-go\/service/github.com\/aws\/aws-sdk-go\/service1/g' \ 35 | {} \; 36 | 37 | if [ 2 -lt "$N" ]; then 38 | for i in $(seq 2 "$N"); do 39 | cp -r service1 "service$i" 40 | pushd "service$i" 41 | find . -type f -name '*.go' -exec sed -i '' \ 42 | -e "s/service1/service$i/g" \ 43 | {} \; 44 | popd 45 | done 46 | fi 47 | 48 | popd 49 | ``` 50 | 51 | The benchmark results in this document used the commit `20cd465d`. 52 | -------------------------------------------------------------------------------- /internal/gomod/module_name.go: -------------------------------------------------------------------------------- 1 | package gomod 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strings" 7 | 8 | "github.com/sourcegraph/lsif-go/internal/command" 9 | "github.com/sourcegraph/lsif-go/internal/output" 10 | "golang.org/x/tools/go/vcs" 11 | ) 12 | 13 | // ModuleName returns the resolved name of the go module declared in the given 14 | // directory usable for moniker identifiers. Note that this is distinct from the 15 | // declared module as this does not uniquely identify a project via its code host 16 | // coordinates in the presence of forks. 17 | // 18 | // isStdLib is true if dir is pointing to the src directory in the golang/go 19 | // repository. 20 | func ModuleName(dir, repo string, outputOptions output.Options) (moduleName string, isStdLib bool, err error) { 21 | resolve := func() { 22 | name := repo 23 | 24 | if !isModule(dir) { 25 | log.Println("WARNING: No go.mod file found in current directory.") 26 | } else { 27 | if name, err = command.Run(dir, "go", "list", "-mod=readonly", "-m"); err != nil { 28 | err = fmt.Errorf("failed to list modules: %v\n%s", err, name) 29 | return 30 | } 31 | } 32 | 33 | moduleName, isStdLib, err = resolveModuleName(repo, name) 34 | } 35 | 36 | output.WithProgress("Resolving module name", resolve, outputOptions) 37 | return moduleName, isStdLib, err 38 | } 39 | 40 | // resolveModuleName converts the given repository and import path into a canonical 41 | // representation of a module name usable for moniker identifiers. The base of the 42 | // import path will be the resolved repository remote, and the given module name 43 | // is used only to determine the path suffix. 44 | func resolveModuleName(repo, name string) (string, bool, error) { 45 | // Determine path suffix relative to repository root 46 | var suffix string 47 | 48 | if nameRepoRoot, err := vcs.RepoRootForImportPath(name, false); err == nil { 49 | suffix = strings.TrimPrefix(name, nameRepoRoot.Root) 50 | } else { 51 | // A user-visible warning will occur on this path as the declared 52 | // module will be resolved as part of gomod.ListDependencies. 53 | } 54 | 55 | // Determine the canonical code host of the current repository 56 | repoRepoRoot, err := vcs.RepoRootForImportPath(repo, false) 57 | if err != nil { 58 | help := "Make sure your git repo has a remote (git remote add origin git@github.com:owner/repo)" 59 | return "", false, fmt.Errorf("%v\n\n%s", err, help) 60 | } 61 | 62 | return repoRepoRoot.Repo + suffix, name == "std", nil 63 | } 64 | -------------------------------------------------------------------------------- /cmd/lsif-go/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | "github.com/sourcegraph/lsif-go/internal/git" 9 | "github.com/sourcegraph/lsif-go/internal/gomod" 10 | "github.com/sourcegraph/lsif-go/internal/indexer" 11 | "github.com/sourcegraph/lsif-go/internal/output" 12 | ) 13 | 14 | func init() { 15 | log.SetFlags(0) 16 | log.SetPrefix("") 17 | log.SetOutput(os.Stdout) 18 | } 19 | 20 | func main() { 21 | if err := mainErr(); err != nil { 22 | fmt.Fprint(os.Stderr, fmt.Sprintf("error: %v\n", err)) 23 | os.Exit(1) 24 | } 25 | } 26 | 27 | func mainErr() (err error) { 28 | if err := parseArgs(os.Args[1:]); err != nil { 29 | return err 30 | } 31 | 32 | if !git.Check(moduleRoot) { 33 | return fmt.Errorf("module root is not a git repository") 34 | } 35 | 36 | defer func() { 37 | if err != nil { 38 | // Add a new line to all errors except for ones that 39 | // come from parsing invalid command line arguments 40 | // and basic environment sanity checks. 41 | // 42 | // We will print progress unconditionally after this 43 | // point and we want the error text to be clearly 44 | // visible. 45 | fmt.Fprintf(os.Stderr, "\n") 46 | } 47 | }() 48 | 49 | outputOptions := output.Options{ 50 | Verbosity: getVerbosity(), 51 | ShowAnimations: animation, 52 | } 53 | 54 | moduleName, isStdLib, err := gomod.ModuleName(moduleRoot, repositoryRemote, outputOptions) 55 | if err != nil { 56 | return fmt.Errorf("failed to infer module name: %v", err) 57 | } 58 | 59 | dependencies, err := gomod.ListDependencies(moduleRoot, moduleName, moduleVersion, outputOptions) 60 | if err != nil { 61 | return fmt.Errorf("failed to list dependencies: %v", err) 62 | } 63 | 64 | var projectDependencies []string 65 | if !isStdLib { 66 | projectDependencies, err = gomod.ListProjectDependencies(moduleRoot) 67 | if err != nil { 68 | return fmt.Errorf("failed to list project dependencies: %v", err) 69 | } 70 | } 71 | 72 | generationOptions := indexer.NewGenerationOptions() 73 | if enableApiDocs { 74 | return fmt.Errorf("API Docs are no longer supported. To fix this problem, remove the -enable-api-docs flag.") 75 | } 76 | generationOptions.EnableImplementations = enableImplementations 77 | generationOptions.DepBatchSize = depBatchSize 78 | 79 | if err := writeIndex( 80 | repositoryRoot, 81 | repositoryRemote, 82 | projectRoot, 83 | moduleName, 84 | moduleVersion, 85 | dependencies, 86 | projectDependencies, 87 | outFile, 88 | outputOptions, 89 | generationOptions, 90 | ); err != nil { 91 | return fmt.Errorf("failed to index: %v", err) 92 | } 93 | 94 | return nil 95 | } 96 | -------------------------------------------------------------------------------- /internal/indexer/protocol_test.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | "encoding/json" 5 | "go/token" 6 | "go/types" 7 | "testing" 8 | 9 | "github.com/google/go-cmp/cmp" 10 | protocol "github.com/sourcegraph/sourcegraph/lib/codeintel/lsif/protocol" 11 | ) 12 | 13 | func TestRangeForObject(t *testing.T) { 14 | start, end := rangeForObject( 15 | types.NewPkgName(token.Pos(42), nil, "foobar", nil), 16 | token.Position{Line: 10, Column: 25}, 17 | ) 18 | 19 | if diff := cmp.Diff(protocol.Pos{Line: 9, Character: 24}, start); diff != "" { 20 | t.Errorf("unexpected start (-want +got): %s", diff) 21 | } 22 | if diff := cmp.Diff(protocol.Pos{Line: 9, Character: 30}, end); diff != "" { 23 | t.Errorf("unexpected end (-want +got): %s", diff) 24 | } 25 | } 26 | 27 | func TestRangeForObjectWithQuotedNamed(t *testing.T) { 28 | start, end := rangeForObject( 29 | types.NewPkgName(token.Pos(42), nil, `"foobar"`, nil), 30 | token.Position{Line: 10, Column: 25}, 31 | ) 32 | 33 | if diff := cmp.Diff(protocol.Pos{Line: 9, Character: 25}, start); diff != "" { 34 | t.Errorf("unexpected start (-want +got): %s", diff) 35 | } 36 | if diff := cmp.Diff(protocol.Pos{Line: 9, Character: 31}, end); diff != "" { 37 | t.Errorf("unexpected end (-want +got): %s", diff) 38 | } 39 | } 40 | 41 | func TestToMarkedStringSignature(t *testing.T) { 42 | content, err := json.Marshal(toMarkupContent("var score int64", "", "")) 43 | if err != nil { 44 | t.Errorf("unexpected error marshalling hover content: %s", err) 45 | } 46 | 47 | if diff := cmp.Diff("{\"kind\":\"markdown\",\"value\":\"```go\\nvar score int64\\n```\"}", string(content)); diff != "" { 48 | t.Errorf("unexpected hover content (-want +got): %s", diff) 49 | } 50 | } 51 | 52 | func TestToMarkedStringDocstring(t *testing.T) { 53 | content, err := json.Marshal(toMarkupContent("var score int64", "Score tracks the user's score.", "")) 54 | if err != nil { 55 | t.Errorf("unexpected error marshalling hover content: %s", err) 56 | } 57 | 58 | if diff := cmp.Diff("{\"kind\":\"markdown\",\"value\":\"```go\\nvar score int64\\n```\\n\\n---\\n\\nScore tracks the user's score.\\n\\n\"}", string(content)); diff != "" { 59 | t.Errorf("unexpected hover content (-want +got): %s", diff) 60 | } 61 | } 62 | 63 | func TestToMarkedStringExtra(t *testing.T) { 64 | content, err := json.Marshal(toMarkupContent("var score int64", "", "score = 123")) 65 | if err != nil { 66 | t.Errorf("unexpected error marshalling hover content: %s", err) 67 | } 68 | 69 | if diff := cmp.Diff("{\"kind\":\"markdown\",\"value\":\"```go\\nvar score int64\\n```\\n\\n---\\n\\n```go\\nscore = 123\\n```\"}", string(content)); diff != "" { 70 | t.Errorf("unexpected hover content (-want +got): %s", diff) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /internal/indexer/visit.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | "log" 5 | "sync" 6 | "sync/atomic" 7 | 8 | "github.com/sourcegraph/lsif-go/internal/output" 9 | "github.com/sourcegraph/lsif-go/internal/parallel" 10 | "golang.org/x/tools/go/packages" 11 | ) 12 | 13 | // visitEachRawFile invokes the given visitor function on each file reachable from the given set of 14 | // packages. The file info object passed to the given callback function does not have an associated 15 | // document value. This method prints the progress of the traversal to stdout asynchronously. 16 | func (i *Indexer) visitEachRawFile(name string, fn func(filename string)) { 17 | n := uint64(0) 18 | for _, p := range i.packages { 19 | n += uint64(len(p.Syntax)) 20 | } 21 | 22 | var count uint64 23 | var wg sync.WaitGroup 24 | wg.Add(1) 25 | 26 | go func() { 27 | defer wg.Done() 28 | 29 | for _, p := range i.packages { 30 | if i.outputOptions.Verbosity >= output.VeryVerboseOutput { 31 | log.Printf("\tPackage %s", p.ID) 32 | } 33 | 34 | for _, f := range p.Syntax { 35 | filename := p.Fset.Position(f.Package).Filename 36 | 37 | if i.outputOptions.Verbosity >= output.VeryVeryVerboseOutput { 38 | log.Printf("\t\tFile %s", filename) 39 | } 40 | 41 | fn(filename) 42 | atomic.AddUint64(&count, 1) 43 | } 44 | } 45 | }() 46 | 47 | output.WithProgressParallel(&wg, name, i.outputOptions, &count, n) 48 | } 49 | 50 | // visitEachPackage invokes the given visitor function on each indexed package. This method prints the 51 | // progress of the traversal to stdout asynchronously. 52 | func (i *Indexer) visitEachPackage(name string, fn func(p *packages.Package)) { 53 | ch := make(chan func()) 54 | 55 | go func() { 56 | defer close(ch) 57 | 58 | for _, p := range i.packages { 59 | t := p 60 | ch <- func() { 61 | if i.outputOptions.Verbosity >= output.VeryVerboseOutput { 62 | log.Printf("\tPackage %s", p.ID) 63 | } 64 | 65 | fn(t) 66 | } 67 | } 68 | }() 69 | 70 | n := uint64(len(i.packages)) 71 | wg, count := parallel.Run(ch) 72 | output.WithProgressParallel(wg, name, i.outputOptions, count, n) 73 | } 74 | 75 | // visitEachDefinitionInfo invokes the given visitor function on each definition info value. This method 76 | // prints the progress of the traversal to stdout asynchronously. 77 | func (i *Indexer) visitEachDefinitionInfo(name string, fn func(d *DefinitionInfo)) { 78 | maps := []map[interface{}]*DefinitionInfo{ 79 | i.consts, 80 | i.funcs, 81 | i.imports, 82 | i.labels, 83 | i.types, 84 | i.vars, 85 | } 86 | 87 | n := uint64(0) 88 | for _, m := range maps { 89 | n += uint64(len(m)) 90 | } 91 | 92 | ch := make(chan func()) 93 | 94 | go func() { 95 | defer close(ch) 96 | 97 | for _, m := range maps { 98 | for _, d := range m { 99 | t := d 100 | ch <- func() { fn(t) } 101 | } 102 | } 103 | }() 104 | 105 | wg, count := parallel.Run(ch) 106 | output.WithProgressParallel(wg, name, i.outputOptions, count, n) 107 | } 108 | 109 | // visitEachDocument invokes the given visitor function on each document. This method prints the 110 | // progress of the traversal to stdout asynchronously. 111 | func (i *Indexer) visitEachDocument(name string, fn func(d *DocumentInfo)) { 112 | ch := make(chan func()) 113 | 114 | go func() { 115 | defer close(ch) 116 | 117 | for _, d := range i.documents { 118 | t := d 119 | ch <- func() { fn(t) } 120 | } 121 | }() 122 | 123 | n := uint64(len(i.documents)) 124 | wg, count := parallel.Run(ch) 125 | output.WithProgressParallel(wg, name, i.outputOptions, count, n) 126 | } 127 | -------------------------------------------------------------------------------- /internal/testdata/fixtures/child_symbols.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | // Const is a constant equal to 5. It's the best constant I've ever written. 😹 4 | const Const = 5 5 | 6 | // Docs for the const block itself. 7 | const ( 8 | // ConstBlock1 is a constant in a block. 9 | ConstBlock1 = 1 10 | 11 | // ConstBlock2 is a constant in a block. 12 | ConstBlock2 = 2 13 | ) 14 | 15 | // Var is a variable interface. 16 | var Var Interface = &Struct{Field: "bar!"} 17 | 18 | // unexportedVar is an unexported variable interface. 19 | var unexportedVar Interface = &Struct{Field: "bar!"} 20 | 21 | // x has a builtin error type 22 | var x error 23 | 24 | var BigVar Interface = &Struct{ 25 | Field: "bar!", 26 | Anonymous: struct { 27 | FieldA int 28 | FieldB int 29 | FieldC int 30 | }{FieldA: 1337}, 31 | } 32 | 33 | // What are docs, really? 34 | // I can't say for sure, I don't write any. 35 | // But look, a CAT! 36 | // 37 | // |\ _,,,---,,_ 38 | // ZZZzz /,`.-'`' -. ;-;;,_ 39 | // |,4- ) )-,_. ,\ ( `'-' 40 | // '---''(_/--' `-'\_) 41 | // 42 | // It's sleeping! Some people write that as `sleeping` but Markdown 43 | // isn't allowed in Go docstrings, right? right?! 44 | var ( 45 | // This has some docs 46 | VarBlock1 = "if you're reading this" 47 | 48 | VarBlock2 = "hi" 49 | ) 50 | 51 | // Embedded is a struct, to be embedded in another struct. 52 | type Embedded struct { 53 | // EmbeddedField has some docs! 54 | EmbeddedField string 55 | Field string // conflicts with parent "Field" 56 | } 57 | 58 | type Struct struct { 59 | *Embedded 60 | Field string 61 | Anonymous struct { 62 | FieldA int 63 | FieldB int 64 | FieldC int 65 | } 66 | } 67 | 68 | // StructMethod has some docs! 69 | func (s *Struct) StructMethod() {} 70 | 71 | func (s *Struct) ImplementsInterface() string { return "hi!" } 72 | 73 | func (s *Struct) MachineLearning( 74 | param1 float32, // It's ML, I can't describe what this param is. 75 | 76 | // We call the below hyperparameters because, uhh, well: 77 | // 78 | // ,-. _,---._ __ / \ 79 | // / ) .-' `./ / \ 80 | // ( ( ,' `/ /| 81 | // \ `-" \'\ / | 82 | // `. , \ \ / | 83 | // /`. ,'-`----Y | 84 | // ( ; | ' 85 | // | ,-. ,-' | / 86 | // | | ( | hjw | / 87 | // ) | \ `.___________|/ 88 | // `--' `--' 89 | // 90 | hyperparam2 float32, 91 | hyperparam3 float32, 92 | ) float32 { 93 | // varShouldNotHaveDocs is in a function, should not have docs emitted. 94 | var varShouldNotHaveDocs int32 95 | 96 | // constShouldNotHaveDocs is in a function, should not have docs emitted. 97 | const constShouldNotHaveDocs = 5 98 | 99 | // typeShouldNotHaveDocs is in a function, should not have docs emitted. 100 | type typeShouldNotHaveDocs struct{ a string } 101 | 102 | // funcShouldNotHaveDocs is in a function, should not have docs emitted. 103 | funcShouldNotHaveDocs := func(a string) string { return "hello" } 104 | 105 | return param1 + (hyperparam2 * *hyperparam3) // lol is this all ML is? I'm gonna be rich 106 | } 107 | 108 | // Interface has docs too 109 | type Interface interface { 110 | ImplementsInterface() string 111 | } 112 | 113 | func NewInterface() Interface { return nil } 114 | 115 | var SortExportedFirst = 1 116 | 117 | var sortUnexportedSecond = 2 118 | 119 | var _sortUnderscoreLast = 3 120 | 121 | // Yeah this is some Go magic incantation which is common. 122 | // 123 | // ,_ _ 124 | // |\\_,-~/ 125 | // / _ _ | ,--. 126 | // ( @ @ ) / ,-' 127 | // \ _T_/-._( ( 128 | // / `. \ 129 | // | _ \ | 130 | // \ \ , / | 131 | // || |-_\__ / 132 | // ((_/`(____,-' 133 | // 134 | var _ = Interface(&Struct{}) 135 | 136 | type _ = struct{} 137 | 138 | // crypto/tls/common_string.go uses this pattern.. 139 | func _() { 140 | } 141 | 142 | // Go can be fun 143 | type ( 144 | // And confusing 145 | X struct { 146 | bar string 147 | } 148 | 149 | Y struct { 150 | baz float 151 | } 152 | ) 153 | -------------------------------------------------------------------------------- /internal/output/output.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "sync" 8 | "sync/atomic" 9 | "time" 10 | 11 | "github.com/efritz/pentimento" 12 | "github.com/sourcegraph/lsif-go/internal/parallel" 13 | "github.com/sourcegraph/lsif-go/internal/util" 14 | ) 15 | 16 | type Options struct { 17 | Verbosity Verbosity 18 | ShowAnimations bool 19 | } 20 | 21 | type Verbosity int 22 | 23 | const ( 24 | NoOutput Verbosity = iota 25 | DefaultOutput 26 | VerboseOutput 27 | VeryVerboseOutput 28 | VeryVeryVerboseOutput 29 | ) 30 | 31 | // updateInterval is the duration between updates in withProgress. 32 | var updateInterval = time.Second / 4 33 | 34 | // ticker is the animated throbber used in printProgress. 35 | var ticker = pentimento.NewAnimatedString([]string{ 36 | "⠸", "⠼", 37 | "⠴", "⠦", 38 | "⠧", "⠇", 39 | "⠏", "⠋", 40 | "⠙", "⠹", 41 | }, updateInterval) 42 | 43 | // var failurePrefix = "✗" 44 | var successPrefix = "✔" 45 | 46 | // logger is used to log at the level -vv and above from multiple goroutines. 47 | var logger = log.New(os.Stdout, "", 0) 48 | 49 | // WithProgress prints a spinner while the given function is active. 50 | func WithProgress(name string, fn func(), outputOptions Options) { 51 | ch := make(chan func(), 1) 52 | ch <- fn 53 | close(ch) 54 | 55 | wg, count := parallel.Run(ch) 56 | WithProgressParallel(wg, name, outputOptions, count, 1) 57 | } 58 | 59 | // WithProgressParallel will continuously print progress to stdout until the given wait group 60 | // counter goes to zero. Progress is determined by the values of `c` (number of tasks completed) 61 | // and the value `n` (total number of tasks). 62 | func WithProgressParallel(wg *sync.WaitGroup, name string, outputOptions Options, c *uint64, n uint64) { 63 | sync := make(chan struct{}) 64 | go func() { 65 | wg.Wait() 66 | close(sync) 67 | }() 68 | 69 | withTitle(name, outputOptions, func(printer *pentimento.Printer) { 70 | for { 71 | select { 72 | case <-sync: 73 | return 74 | case <-time.After(updateInterval): 75 | } 76 | 77 | printProgress(printer, name, c, n) 78 | } 79 | }) 80 | } 81 | 82 | // withTitle invokes withTitleAnimated withTitleStatic depending on the value of animated. 83 | func withTitle(name string, outputOptions Options, fn func(printer *pentimento.Printer)) { 84 | if outputOptions.Verbosity == NoOutput { 85 | fn(nil) 86 | } else if !outputOptions.ShowAnimations || outputOptions.Verbosity >= VeryVerboseOutput { 87 | withTitleStatic(name, outputOptions.Verbosity, fn) 88 | } else { 89 | withTitleAnimated(name, outputOptions.Verbosity, fn) 90 | } 91 | } 92 | 93 | // withTitleStatic invokes the given function with non-animated output. 94 | func withTitleStatic(name string, verbosity Verbosity, fn func(printer *pentimento.Printer)) { 95 | start := time.Now() 96 | fmt.Printf("%s\n", name) 97 | fn(nil) 98 | 99 | if verbosity > DefaultOutput { 100 | fmt.Printf("Finished in %s.\n\n", util.HumanElapsed(start)) 101 | } 102 | } 103 | 104 | // withTitleStatic invokes the given function with animated output. 105 | func withTitleAnimated(name string, verbosity Verbosity, fn func(printer *pentimento.Printer)) { 106 | start := time.Now() 107 | fmt.Printf("%s %s... ", ticker, name) 108 | 109 | _ = pentimento.PrintProgress(func(printer *pentimento.Printer) error { 110 | defer func() { 111 | _ = printer.Reset() 112 | }() 113 | 114 | fn(printer) 115 | return nil 116 | }) 117 | 118 | if verbosity > DefaultOutput { 119 | fmt.Printf("%s %s... Done (%s)\n", successPrefix, name, util.HumanElapsed(start)) 120 | } else { 121 | fmt.Printf("%s %s... Done\n", successPrefix, name) 122 | } 123 | } 124 | 125 | // printProgress outputs a throbber, the given name, and the given number of tasks completed to 126 | // the given printer. 127 | func printProgress(printer *pentimento.Printer, name string, c *uint64, n uint64) { 128 | if printer == nil { 129 | return 130 | } 131 | 132 | content := pentimento.NewContent() 133 | 134 | if c == nil { 135 | content.AddLine("%s %s...", ticker, name) 136 | } else { 137 | content.AddLine("%s %s... %d/%d\n", ticker, name, atomic.LoadUint64(c), n) 138 | } 139 | 140 | printer.WriteContent(content) 141 | } 142 | -------------------------------------------------------------------------------- /internal/indexer/typestring.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "go/types" 7 | "strings" 8 | ) 9 | 10 | // indent is used to format struct fields. 11 | const indent = " " 12 | 13 | // typeString returns the string representation of the given object's type. 14 | func typeString(obj ObjectLike) (signature string, extra string) { 15 | switch v := obj.(type) { 16 | case *types.PkgName: 17 | return fmt.Sprintf("package %s", v.Name()), "" 18 | 19 | case *types.TypeName: 20 | return formatTypeSignature(v), formatTypeExtra(v) 21 | 22 | case *types.Var: 23 | if v.IsField() { 24 | // TODO(efritz) - make this be "(T).F" instead of "struct field F string" 25 | return fmt.Sprintf("struct %s", obj.String()), "" 26 | } 27 | 28 | case *types.Const: 29 | return fmt.Sprintf("%s = %s", types.ObjectString(v, packageQualifier), v.Val()), "" 30 | 31 | case *PkgDeclaration: 32 | return fmt.Sprintf("package %s", v.name), "" 33 | 34 | } 35 | 36 | // Fall back to types.Object 37 | // All other cases of this should be this type. We only had to implement PkgDeclaration because 38 | // some fields are not exported in types.Object. 39 | // 40 | // We expect any new ObjectLike items to be `types.Object` values. 41 | v, _ := obj.(types.Object) 42 | return types.ObjectString(v, packageQualifier), "" 43 | } 44 | 45 | // packageQualifier returns an empty string in order to remove the leading package 46 | // name from all identifiers in the return value of types.ObjectString. 47 | func packageQualifier(*types.Package) string { return "" } 48 | 49 | // formatTypeSignature returns a brief description of the given struct or interface type. 50 | func formatTypeSignature(obj *types.TypeName) string { 51 | switch obj.Type().Underlying().(type) { 52 | case *types.Struct: 53 | if obj.IsAlias() { 54 | switch obj.Type().(type) { 55 | case *types.Named: 56 | original := obj.Type().(*types.Named).Obj() 57 | var pkg string 58 | if obj.Pkg().Name() != original.Pkg().Name() { 59 | pkg = original.Pkg().Name() + "." 60 | } 61 | return fmt.Sprintf("type %s = %s%s", obj.Name(), pkg, original.Name()) 62 | 63 | case *types.Struct: 64 | return fmt.Sprintf("type %s = struct", obj.Name()) 65 | } 66 | } 67 | 68 | return fmt.Sprintf("type %s struct", obj.Name()) 69 | case *types.Interface: 70 | return fmt.Sprintf("type %s interface", obj.Name()) 71 | } 72 | 73 | return "" 74 | } 75 | 76 | // formatTypeExtra returns the beautified fields of the given struct or interface type. 77 | // 78 | // The output of `types.TypeString` puts fields of structs and interfaces on a single 79 | // line separated by a semicolon. This method simply expands the fields to reside on 80 | // different lines with the appropriate indentation. 81 | func formatTypeExtra(obj *types.TypeName) string { 82 | extra := types.TypeString(obj.Type().Underlying(), packageQualifier) 83 | 84 | depth := 0 85 | buf := bytes.NewBuffer(make([]byte, 0, len(extra))) 86 | 87 | outer: 88 | for i := 0; i < len(extra); i++ { 89 | switch extra[i] { 90 | case '"': 91 | for j := i + 1; j < len(extra); j++ { 92 | if extra[j] == '\\' { 93 | // skip over escaped characters 94 | j++ 95 | continue 96 | } 97 | 98 | if extra[j] == '"' { 99 | // found non-escaped ending quote 100 | // write entire string unchanged, then skip to this 101 | // character adn continue the outer loop, which will 102 | // start the next iteration on the following character 103 | buf.WriteString(extra[i : j+1]) 104 | i = j 105 | continue outer 106 | } 107 | } 108 | 109 | // note: we should never get down here otherwise 110 | // there is some illegal output from types.TypeString. 111 | 112 | case ';': 113 | buf.WriteString("\n") 114 | buf.WriteString(strings.Repeat(indent, depth)) 115 | i++ // Skip following ' ' 116 | 117 | case '{': 118 | // Special case empty fields so we don't insert 119 | // an unnecessary newline. 120 | if i < len(extra)-1 && extra[i+1] == '}' { 121 | buf.WriteString("{}") 122 | i++ // Skip following '}' 123 | } else { 124 | depth++ 125 | buf.WriteString(" {\n") 126 | buf.WriteString(strings.Repeat(indent, depth)) 127 | } 128 | 129 | case '}': 130 | depth-- 131 | buf.WriteString("\n") 132 | buf.WriteString(strings.Repeat(indent, depth)) 133 | buf.WriteString("}") 134 | 135 | default: 136 | buf.WriteByte(extra[i]) 137 | } 138 | } 139 | 140 | return buf.String() 141 | } 142 | -------------------------------------------------------------------------------- /internal/testdata/fixtures/data.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/sourcegraph/lsif-go/internal/testdata/fixtures/internal/secret" 7 | ) 8 | 9 | // TestInterface is an interface used for testing. 10 | type TestInterface interface { 11 | // Do does a test thing. 12 | Do(ctx context.Context, data string) (score int, _ error) 13 | } 14 | 15 | type ( 16 | // TestStruct is a struct used for testing. 17 | TestStruct struct { 18 | // SimpleA docs 19 | SimpleA int 20 | // SimpleB docs 21 | SimpleB int 22 | // SimpleC docs 23 | SimpleC int 24 | 25 | FieldWithTag string `json:"tag"` 26 | FieldWithAnonymousType struct { 27 | NestedA string 28 | NestedB string 29 | // NestedC docs 30 | NestedC string 31 | } 32 | 33 | EmptyStructField struct{} 34 | } 35 | 36 | TestEmptyStruct struct{} 37 | ) 38 | 39 | // Score is just a hardcoded number. 40 | const Score = uint64(42) 41 | const secretScore = secret.SecretScore 42 | 43 | const SomeString = "foobar" 44 | const LongString = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed tincidunt viverra aliquam. Phasellus finibus, arcu eu commodo porta, dui quam dictum ante, nec porta enim leo quis felis. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Curabitur luctus orci tortor, non condimentum arcu bibendum ut. Proin sit amet vulputate lorem, ut egestas arcu. Curabitur quis sagittis mi. Aenean elit sem, imperdiet ut risus eget, varius varius erat.\nNullam lobortis tortor sed sodales consectetur. Aenean condimentum vehicula elit, eget interdum ante finibus nec. Mauris mollis, nulla eu vehicula rhoncus, eros lectus viverra tellus, ac hendrerit quam massa et felis. Nunc vestibulum diam a facilisis sollicitudin. Aenean nec varius metus. Sed nec diam nibh. Ut erat erat, suscipit et ante eget, tincidunt condimentum orci. Aenean nec facilisis augue, ac sodales ex. Nulla dictum hendrerit tempus. Aliquam fringilla tortor in massa molestie, quis bibendum nulla ullamcorper. Suspendisse congue laoreet elit, vitae consectetur orci facilisis non. Aliquam tempus ultricies sapien, rhoncus tincidunt nisl tincidunt eget. Aliquam nisi ante, rutrum eget viverra imperdiet, congue ut nunc. Donec mollis sed tellus vel placerat. Sed mi ex, fringilla a fermentum a, tincidunt eget lectus.\nPellentesque lacus nibh, accumsan eget feugiat nec, gravida eget urna. Donec quam velit, imperdiet in consequat eget, ultricies eget nunc. Curabitur interdum vel sem et euismod. Donec sed vulputate odio, sit amet bibendum tellus. Integer pellentesque nunc eu turpis cursus, vestibulum sodales ipsum posuere. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Ut at vestibulum sapien. In hac habitasse platea dictumst. Nullam sed lobortis urna, non bibendum ipsum. Sed in sapien quis purus semper fringilla. Integer ut egestas nulla, eu ornare lectus. Maecenas quis sapien condimentum, dignissim urna quis, hendrerit neque. Donec cursus sit amet metus eu mollis.\nSed scelerisque vitae odio non egestas. Cras hendrerit tortor mauris. Aenean quis imperdiet nulla, a viverra purus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Praesent finibus faucibus orci, sed ultrices justo iaculis ut. Ut libero massa, condimentum at elit non, fringilla iaculis quam. Sed sit amet ipsum placerat, tincidunt sem in, efficitur lacus. Curabitur ligula orci, tempus ut magna eget, sodales tristique odio.\nPellentesque in libero ac risus pretium ultrices. In hac habitasse platea dictumst. Curabitur a quam sed orci tempus luctus. Integer commodo nec odio quis consequat. Aenean vitae dapibus augue, nec dictum lectus. Etiam sit amet leo diam. Duis eu ligula venenatis, fermentum lacus vel, interdum odio. Vivamus sit amet libero vitae elit interdum cursus et eu erat. Cras interdum augue sit amet ex aliquet tempor. Praesent dolor nisl, convallis bibendum mauris a, euismod commodo ante. Phasellus non ipsum condimentum, molestie dolor quis, pretium nisi. Mauris augue urna, fermentum ut lacinia a, efficitur vitae odio. Praesent finibus nisl et dolor luctus faucibus. Donec eget lectus sed mi porttitor placerat ac eu odio." 45 | const ConstMath = 1 + (2+3)*5 46 | 47 | type StringAlias string 48 | 49 | const AliasedString StringAlias = "foobar" 50 | 51 | // Doer is similar to the test interface (but not the same). 52 | func (ts *TestStruct) Doer(ctx context.Context, data string) (score int, err error) { 53 | return Score, nil 54 | } 55 | 56 | // StructTagRegression is a struct that caused panic in the wild. Added here to 57 | // support a regression test. 58 | // 59 | // See https://github.com/tal-tech/go-zero/blob/11dd3d75ecceaa3f5772024fb3f26dec1ada8e9c/core/mapping/unmarshaler_test.go#L2272. 60 | type StructTagRegression struct { 61 | Value int `key:",range=[:}"` 62 | } 63 | 64 | type TestEqualsStruct = struct { 65 | Value int 66 | } 67 | 68 | type ShellStruct struct { 69 | // Ensure this field comes before the definition 70 | // so that we grab the correct one in our unit 71 | // tests. 72 | InnerStruct 73 | } 74 | 75 | type InnerStruct struct{} 76 | -------------------------------------------------------------------------------- /cmd/lsif-go/args.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/alecthomas/kingpin" 10 | "github.com/sourcegraph/lsif-go/internal/git" 11 | protocol "github.com/sourcegraph/sourcegraph/lib/codeintel/lsif/protocol" 12 | ) 13 | 14 | var app = kingpin.New( 15 | "lsif-go", 16 | "lsif-go is an LSIF indexer for Go.", 17 | ).Version(version + ", protocol version " + protocol.Version) 18 | 19 | var ( 20 | outFile string 21 | projectRoot string 22 | moduleRoot string 23 | repositoryRoot string 24 | repositoryRemote string 25 | moduleVersion string 26 | verbosity int 27 | noOutput bool 28 | animation bool 29 | depBatchSize int 30 | enableApiDocs bool 31 | enableImplementations bool 32 | ) 33 | 34 | func init() { 35 | app.HelpFlag.Short('h') 36 | app.VersionFlag.Short('V') 37 | 38 | // Outfile options 39 | app.Flag("output", "The output file.").Short('o').Default("dump.lsif").StringVar(&outFile) 40 | 41 | // Path options (inferred by presence of go.mod; git) 42 | app.Flag("project-root", "Specifies the directory to index.").Default(".").StringVar(&projectRoot) 43 | app.Flag("module-root", "Specifies the directory containing the go.mod file.").Default(defaultModuleRoot.Value()).StringVar(&moduleRoot) 44 | app.Flag("repository-root", "Specifies the top-level directory of the git repository.").Default(defaultRepositoryRoot.Value()).StringVar(&repositoryRoot) 45 | 46 | // Repository remote and tag options (inferred by git) 47 | app.Flag("repository-remote", "Specifies the canonical name of the repository remote.").Default(defaultRepositoryRemote.Value()).StringVar(&repositoryRemote) 48 | app.Flag("module-version", "Specifies the version of the module defined by module-root.").Default(defaultModuleVersion.Value()).StringVar(&moduleVersion) 49 | 50 | // Verbosity options 51 | app.Flag("quiet", "Do not output to stdout or stderr.").Short('q').Default("false").BoolVar(&noOutput) 52 | app.Flag("verbose", "Output debug logs.").Short('v').CounterVar(&verbosity) 53 | app.Flag("animation", "Do not animate output.").Default("false").BoolVar(&animation) 54 | 55 | app.Flag("dep-batch-size", "How many dependencies to load at once to limit memory usage (e.g. 100). 0 means load all at once.").Default("0").IntVar(&depBatchSize) 56 | 57 | // Feature flags 58 | app.Flag("enable-api-docs", "Enable Sourcegraph API Doc generation").Default("false").BoolVar(&enableApiDocs) 59 | app.Flag("enable-implementations", "Enable textDocument/implementation generation").Default("true").BoolVar(&enableImplementations) 60 | } 61 | 62 | func parseArgs(args []string) (err error) { 63 | if _, err := app.Parse(args); err != nil { 64 | return fmt.Errorf("failed to parse args: %v", err) 65 | } 66 | 67 | sanitizers := []func() error{sanitizeProjectRoot, sanitizeModuleRoot, sanitizeRepositoryRoot} 68 | validators := []func() error{validatePaths} 69 | 70 | for _, f := range append(sanitizers, validators...) { 71 | if err := f(); err != nil { 72 | return fmt.Errorf("failed to parse args: %v", err) 73 | } 74 | } 75 | 76 | return nil 77 | } 78 | 79 | // 80 | // Sanitizers 81 | 82 | func sanitizeProjectRoot() (err error) { 83 | projectRoot, err = filepath.Abs(projectRoot) 84 | if err != nil { 85 | return fmt.Errorf("get abspath of project root: %v", err) 86 | } 87 | 88 | return nil 89 | } 90 | 91 | func sanitizeModuleRoot() (err error) { 92 | moduleRoot, err = filepath.Abs(moduleRoot) 93 | if err != nil { 94 | return fmt.Errorf("get abspath of module root: %v", err) 95 | } 96 | 97 | return nil 98 | } 99 | 100 | func sanitizeRepositoryRoot() (err error) { 101 | repositoryRoot, err = filepath.Abs(repositoryRoot) 102 | if err != nil { 103 | return fmt.Errorf("get abspath of repository root: %v", err) 104 | } 105 | 106 | return nil 107 | } 108 | 109 | // 110 | // Validators 111 | 112 | func validatePaths() error { 113 | if !strings.HasPrefix(projectRoot, repositoryRoot) { 114 | return errors.New("project root is outside the repository") 115 | } 116 | 117 | if !strings.HasPrefix(moduleRoot, repositoryRoot) { 118 | return errors.New("module root is outside the repository") 119 | } 120 | 121 | return nil 122 | } 123 | 124 | // 125 | // Defaults 126 | 127 | var defaultModuleRoot = newCachedString(func() string { 128 | return searchForGoMod(wd.Value(), toplevel.Value()) 129 | }) 130 | 131 | var defaultRepositoryRoot = newCachedString(func() string { 132 | return rel(toplevel.Value()) 133 | }) 134 | 135 | var defaultRepositoryRemote = newCachedString(func() string { 136 | if repo, err := git.InferRepo(defaultModuleRoot.Value()); err == nil { 137 | return repo 138 | } 139 | 140 | return "" 141 | }) 142 | 143 | var defaultModuleVersion = newCachedString(func() string { 144 | if version, err := git.InferModuleVersion(defaultModuleRoot.Value()); err == nil { 145 | return version 146 | } 147 | 148 | return "" 149 | }) 150 | -------------------------------------------------------------------------------- /internal/indexer/hover_test.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestFindDocstringFunc(t *testing.T) { 8 | packages := getTestPackages(t) 9 | p, obj := findDefinitionByName(t, packages, "ParallelizableFunc") 10 | 11 | expectedText := normalizeDocstring(` 12 | ParallelizableFunc is a function that can be called concurrently with other instances 13 | of this function type. 14 | `) 15 | if text := normalizeDocstring(findDocstring(NewPackageDataCache(), packages, p, obj)); text != expectedText { 16 | t.Errorf("unexpected hover text. want=%q have=%q", expectedText, text) 17 | } 18 | } 19 | 20 | func TestFindDocstringInterface(t *testing.T) { 21 | packages := getTestPackages(t) 22 | p, obj := findDefinitionByName(t, packages, "TestInterface") 23 | 24 | expectedText := normalizeDocstring(`TestInterface is an interface used for testing.`) 25 | if text := normalizeDocstring(findDocstring(NewPackageDataCache(), packages, p, obj)); text != expectedText { 26 | t.Errorf("unexpected hover text. want=%q have=%q", expectedText, text) 27 | } 28 | } 29 | 30 | func TestFindDocstringStruct(t *testing.T) { 31 | packages := getTestPackages(t) 32 | p, obj := findDefinitionByName(t, packages, "TestStruct") 33 | 34 | expectedText := normalizeDocstring(`TestStruct is a struct used for testing.`) 35 | if text := normalizeDocstring(findDocstring(NewPackageDataCache(), packages, p, obj)); text != expectedText { 36 | t.Errorf("unexpected hover text. want=%q have=%q", expectedText, text) 37 | } 38 | } 39 | 40 | func TestFindDocstringField(t *testing.T) { 41 | packages := getTestPackages(t) 42 | p, obj := findDefinitionByName(t, packages, "NestedC") 43 | 44 | expectedText := normalizeDocstring(`NestedC docs`) 45 | if text := normalizeDocstring(findDocstring(NewPackageDataCache(), packages, p, obj)); text != expectedText { 46 | t.Errorf("unexpected hover text. want=%q have=%q", expectedText, text) 47 | } 48 | } 49 | 50 | func TestFindDocstringConst(t *testing.T) { 51 | packages := getTestPackages(t) 52 | p, obj := findDefinitionByName(t, packages, "Score") 53 | 54 | expectedText := normalizeDocstring(`Score is just a hardcoded number.`) 55 | if text := normalizeDocstring(findDocstring(NewPackageDataCache(), packages, p, obj)); text != expectedText { 56 | t.Errorf("unexpected hover text. want=%q have=%q", expectedText, text) 57 | } 58 | } 59 | 60 | // TestFindDocstringLocalVariable ensures that local definitions within a function with a 61 | // docstring do not take their parent's docstring as their own. This was a brief (unpublished) 62 | // regression made when switching from storing node paths for hover text extraction to only 63 | // storing a single ancestor node from which hover text is extracted. 64 | func TestFindDocstringLocalVariable(t *testing.T) { 65 | packages := getTestPackages(t) 66 | p, obj := findDefinitionByName(t, packages, "errs") 67 | 68 | expectedText := normalizeDocstring(``) 69 | if text := normalizeDocstring(findDocstring(NewPackageDataCache(), packages, p, obj)); text != expectedText { 70 | t.Errorf("unexpected hover text. want=%q have=%q", expectedText, text) 71 | } 72 | } 73 | 74 | func TestFindDocstringInternalPackageName(t *testing.T) { 75 | packages := getTestPackages(t) 76 | p, obj := findUseByName(t, packages, "secret") 77 | 78 | expectedText := normalizeDocstring(`secret is a package that holds secrets.`) 79 | if text := normalizeDocstring(findDocstring(NewPackageDataCache(), packages, p, obj)); text != expectedText { 80 | t.Errorf("unexpected hover text. want=%q have=%q", expectedText, text) 81 | } 82 | } 83 | 84 | func TestFindDocstringExternalPackageName(t *testing.T) { 85 | packages := getTestPackages(t) 86 | p, obj := findUseByName(t, packages, "sync") 87 | 88 | expectedText := normalizeDocstring(` 89 | Package sync provides basic synchronization primitives such as mutual exclusion locks. 90 | Other than the Once and WaitGroup types, most are intended for use by low-level library routines. 91 | Higher-level synchronization is better done via channels and communication. 92 | Values containing the types defined in this package should not be copied. 93 | `) 94 | if text := normalizeDocstring(findDocstring(NewPackageDataCache(), packages, p, obj)); text != expectedText { 95 | t.Errorf("unexpected hover text. want=%q have=%q", expectedText, text) 96 | } 97 | } 98 | 99 | func TestFindExternalDocstring(t *testing.T) { 100 | packages := getTestPackages(t) 101 | p, obj := findUseByName(t, packages, "WaitGroup") 102 | 103 | expectedText := normalizeDocstring(` 104 | A WaitGroup waits for a collection of goroutines to finish. 105 | The main goroutine calls Add to set the number of goroutines to wait for. 106 | Then each of the goroutines runs and calls Done when finished. 107 | At the same time, Wait can be used to block until all goroutines have finished. 108 | A WaitGroup must not be copied after first use. 109 | `) 110 | if text := normalizeDocstring(findExternalDocstring(NewPackageDataCache(), packages, p, obj)); text != expectedText { 111 | t.Errorf("unexpected hover text. want=%q have=%q", expectedText, text) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /internal/indexer/hover.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | "fmt" 5 | "go/types" 6 | 7 | protocol "github.com/sourcegraph/sourcegraph/lib/codeintel/lsif/protocol" 8 | "golang.org/x/tools/go/packages" 9 | ) 10 | 11 | // findHoverContents returns the hover contents of the given object. This method is not cached 12 | // and should only be called wrapped in a call to makeCachedHoverResult. 13 | func findHoverContents(packageDataCache *PackageDataCache, pkgs []*packages.Package, p *packages.Package, obj ObjectLike) protocol.MarkupContent { 14 | signature, extra := typeString(obj) 15 | docstring := findDocstring(packageDataCache, pkgs, p, obj) 16 | return toMarkupContent(signature, docstring, extra) 17 | } 18 | 19 | // findExternalHoverContents returns the hover contents of the given object defined in the given 20 | // package. This method is not cached and should only be called wrapped in a call to makeCachedHoverResult. 21 | func findExternalHoverContents(packageDataCache *PackageDataCache, pkgs []*packages.Package, p *packages.Package, obj ObjectLike) protocol.MarkupContent { 22 | signature, extra := typeString(obj) 23 | docstring := findExternalDocstring(packageDataCache, pkgs, p, obj) 24 | return toMarkupContent(signature, docstring, extra) 25 | } 26 | 27 | // makeCachedHoverResult returns a hover result vertex identifier. If hover text for the given 28 | // identifier has not already been emitted, a new vertex is created. Identifiers will share the 29 | // same hover result if they refer to the same identifier in the same target package. 30 | func (i *Indexer) makeCachedHoverResult(pkg *types.Package, obj ObjectLike, fn func() protocol.MarkupContent) uint64 { 31 | key := makeCacheKey(pkg, obj) 32 | if key == "" { 33 | // Do not store empty cache keys 34 | return i.emitter.EmitHoverResult(fn()) 35 | } 36 | 37 | i.hoverResultCacheMutex.RLock() 38 | hoverResultID, ok := i.hoverResultCache[key] 39 | i.hoverResultCacheMutex.RUnlock() 40 | if ok { 41 | return hoverResultID 42 | } 43 | 44 | // Note: we calculate this outside of the critical section 45 | contents := fn() 46 | 47 | i.hoverResultCacheMutex.Lock() 48 | defer i.hoverResultCacheMutex.Unlock() 49 | 50 | if hoverResultID, ok := i.hoverResultCache[key]; ok { 51 | return hoverResultID 52 | } 53 | 54 | hoverResultID = i.emitter.EmitHoverResult(contents) 55 | i.hoverResultCache[key] = hoverResultID 56 | return hoverResultID 57 | } 58 | 59 | // makeCacheKey returns a string uniquely representing the given package and object pair. If 60 | // the given package is not nil, the key is the concatenation of the package path and the object 61 | // identifier. Otherwise, the key will be the object identifier if it refers to a package import. 62 | // If the given package is nil and the object is not a package import, the returned cache key is 63 | // the empty string (to force a fresh calculation of each local object's hover text). 64 | func makeCacheKey(pkg *types.Package, obj ObjectLike) string { 65 | if pkg != nil { 66 | return fmt.Sprintf("%s::%d", pkg.Path(), obj.Pos()) 67 | } 68 | 69 | if pkgName, ok := obj.(*types.PkgName); ok { 70 | return pkgName.Imported().Path() 71 | } 72 | 73 | return "" 74 | } 75 | 76 | // findDocstring extracts the comments from the given object. It is assumed that this object is 77 | // declared in an index target (otherwise, findExternalDocstring should be called). 78 | func findDocstring(packageDataCache *PackageDataCache, pkgs []*packages.Package, p *packages.Package, obj ObjectLike) string { 79 | if obj == nil { 80 | return "" 81 | } 82 | 83 | if v, ok := obj.(*types.PkgName); ok { 84 | return findPackageDocstring(pkgs, p, v) 85 | } 86 | 87 | // Resolve the object into its respective ast.Node 88 | return packageDataCache.Text(p, obj.Pos()) 89 | } 90 | 91 | // findExternalDocstring extracts the comments from the given object. It is assumed that this object is 92 | // declared in a dependency. 93 | func findExternalDocstring(packageDataCache *PackageDataCache, pkgs []*packages.Package, p *packages.Package, obj ObjectLike) string { 94 | if obj == nil { 95 | return "" 96 | } 97 | 98 | if v, ok := obj.(*types.PkgName); ok { 99 | return findPackageDocstring(pkgs, p, v) 100 | } 101 | 102 | if target := p.Imports[obj.Pkg().Path()]; target != nil { 103 | // Resolve the object obj into its respective ast.Node 104 | return packageDataCache.Text(target, obj.Pos()) 105 | } 106 | 107 | return "" 108 | } 109 | 110 | // findPackageDocstring searches for the package matching the target package name and returns its 111 | // package-level documentation (the first doc text attached ot a file in the given package). 112 | func findPackageDocstring(pkgs []*packages.Package, p *packages.Package, target *types.PkgName) string { 113 | pkgPath := target.Imported().Path() 114 | 115 | for _, p := range pkgs { 116 | if p.PkgPath == pkgPath { 117 | // The target package is an index target 118 | return extractPackageDocstring(p) 119 | } 120 | } 121 | 122 | if p, ok := p.Imports[pkgPath]; ok { 123 | // The target package is a dependency 124 | return extractPackageDocstring(p) 125 | } 126 | 127 | return "" 128 | } 129 | 130 | // extractPackagedocstring returns the first doc text attached to a file in the given package. 131 | func extractPackageDocstring(p *packages.Package) string { 132 | for _, f := range p.Syntax { 133 | if text := f.Doc.Text(); text != "" { 134 | return text 135 | } 136 | } 137 | 138 | return "" 139 | } 140 | -------------------------------------------------------------------------------- /internal/indexer/typestring_test.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | "go/types" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/google/go-cmp/cmp" 9 | ) 10 | 11 | func TestTypeStringPackage(t *testing.T) { 12 | p := types.NewPkgName(42, nil, "sync", nil) 13 | 14 | if signature, _ := typeString(p); signature != "package sync" { 15 | t.Errorf("unexpected type string. want=%q have=%q", "package sync", signature) 16 | } 17 | } 18 | 19 | func TestTypeStringFunction(t *testing.T) { 20 | _, f := findDefinitionByName(t, getTestPackages(t), "Parallel") 21 | 22 | if signature, _ := typeString(f); signature != "func Parallel(ctx Context, fns ...ParallelizableFunc) error" { 23 | t.Errorf("unexpected type string. want=%q have=%q", "func Parallel(ctx Context, fns ...ParallelizableFunc) error", signature) 24 | } 25 | } 26 | 27 | func TestTypeStringInterface(t *testing.T) { 28 | _, f := findDefinitionByName(t, getTestPackages(t), "TestInterface") 29 | 30 | signature, extra := typeString(f) 31 | if signature != "type TestInterface interface" { 32 | t.Errorf("unexpected type string. want=%q have=%q", "type TestInterface interface", signature) 33 | } 34 | 35 | expectedExtra := strings.TrimSpace(stripIndent(` 36 | interface { 37 | Do(ctx Context, data string) (score int, _ error) 38 | } 39 | `)) 40 | if diff := cmp.Diff(expectedExtra, extra); diff != "" { 41 | t.Errorf("unexpected extra (-want +got): %s", diff) 42 | } 43 | } 44 | 45 | func TestTypeStringStruct(t *testing.T) { 46 | _, f := findDefinitionByName(t, getTestPackages(t), "TestStruct") 47 | 48 | signature, extra := typeString(f) 49 | if signature != "type TestStruct struct" { 50 | t.Errorf("unexpected type string. want=%q have=%q", "type TestStruct struct", signature) 51 | } 52 | 53 | expectedExtra := strings.TrimSpace(stripIndent(` 54 | struct { 55 | SimpleA int 56 | SimpleB int 57 | SimpleC int 58 | FieldWithTag string "json:\"tag\"" 59 | FieldWithAnonymousType struct { 60 | NestedA string 61 | NestedB string 62 | NestedC string 63 | } 64 | EmptyStructField struct{} 65 | } 66 | `)) 67 | if diff := cmp.Diff(expectedExtra, extra); diff != "" { 68 | t.Errorf("unexpected extra (-want +got): %s", diff) 69 | } 70 | } 71 | 72 | func TestTypeStringEmptyStruct(t *testing.T) { 73 | _, f := findDefinitionByName(t, getTestPackages(t), "TestEmptyStruct") 74 | 75 | signature, extra := typeString(f) 76 | if signature != "type TestEmptyStruct struct" { 77 | t.Errorf("unexpected type string. want=%q have=%q", "type TestEmptyStruct struct", signature) 78 | } 79 | 80 | expectedExtra := `struct{}` 81 | if diff := cmp.Diff(expectedExtra, extra); diff != "" { 82 | t.Errorf("unexpected extra (-want +got): %s", diff) 83 | } 84 | } 85 | 86 | func TestTypeStringNameEqualsAnonymousStruct(t *testing.T) { 87 | _, f := findDefinitionByName(t, getTestPackages(t), "TestEqualsStruct") 88 | 89 | signature, extra := typeString(f) 90 | if signature != "type TestEqualsStruct = struct" { 91 | t.Errorf("unexpected type string. want=%q have=%q", "type TestEqualsStruct = struct", signature) 92 | } 93 | 94 | expectedExtra := strings.TrimSpace(stripIndent(` 95 | struct { 96 | Value int 97 | } 98 | `)) 99 | if diff := cmp.Diff(expectedExtra, extra); diff != "" { 100 | t.Errorf("unexpected extra (-want +got): %s", diff) 101 | } 102 | } 103 | 104 | func TestStructTagRegression(t *testing.T) { 105 | _, f := findDefinitionByName(t, getTestPackages(t), "StructTagRegression") 106 | 107 | signature, extra := typeString(f) 108 | if signature != "type StructTagRegression struct" { 109 | t.Errorf("unexpected type string. want=%q have=%q", "type StructTagRegression struct", signature) 110 | } 111 | 112 | expectedExtra := strings.TrimSpace(stripIndent(` 113 | struct { 114 | Value int "key:\",range=[:}\"" 115 | } 116 | `)) 117 | 118 | if diff := cmp.Diff(expectedExtra, extra); diff != "" { 119 | t.Errorf("unexpected extra (-want +got): %s", diff) 120 | } 121 | } 122 | 123 | func TestTypeStringConstNumber(t *testing.T) { 124 | _, obj := findDefinitionByName(t, getTestPackages(t), "Score") 125 | 126 | signature, _ := typeString(obj) 127 | if signature != "const Score uint64 = 42" { 128 | t.Errorf("unexpected type string. want=%q have=%q", "const Score uint64 = 42", signature) 129 | } 130 | } 131 | 132 | func TestTypeStringConstString(t *testing.T) { 133 | _, obj := findDefinitionByName(t, getTestPackages(t), "SomeString") 134 | 135 | signature, _ := typeString(obj) 136 | if signature != `const SomeString untyped string = "foobar"` { 137 | t.Errorf("unexpected type string. want=%q have=%q", `const SomeString string = "foobar"`, signature) 138 | } 139 | } 140 | 141 | func TestTypeStringConstTruncatedString(t *testing.T) { 142 | _, obj := findDefinitionByName(t, getTestPackages(t), "LongString") 143 | 144 | signature, _ := typeString(obj) 145 | if signature != `const LongString untyped string = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed tincidu...` { 146 | t.Errorf("unexpected type string. want=%q have=%q", `const LongString untyped string = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed tincidu...`, signature) 147 | } 148 | } 149 | 150 | func TestTypeStringConstArithmetic(t *testing.T) { 151 | _, obj := findDefinitionByName(t, getTestPackages(t), "ConstMath") 152 | 153 | signature, _ := typeString(obj) 154 | if signature != `const ConstMath untyped int = 26` { 155 | t.Errorf("unexpected type string. want=%q have=%q", `const ConstMath untyped int = 26`, signature) 156 | } 157 | } 158 | 159 | func TestTypeStringAliasedString(t *testing.T) { 160 | _, obj := findDefinitionByName(t, getTestPackages(t), "AliasedString") 161 | 162 | signature, _ := typeString(obj) 163 | if signature != `const AliasedString StringAlias = "foobar"` { 164 | t.Errorf("unexpected type string. want=%q have=%q", `const AliasedString StringAlias = "foobar"`, signature) 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /internal/gomod/dependencies_test.go: -------------------------------------------------------------------------------- 1 | package gomod 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/go-cmp/cmp" 7 | ) 8 | 9 | func TestParseGoListOutput(t *testing.T) { 10 | output := ` 11 | { 12 | "Path": "github.com/gavv/httpexpect", 13 | "Version": "v2.0.0+incompatible", 14 | "Time": "2019-05-23T21:42:28Z", 15 | "Indirect": true, 16 | "GoMod": "/Users/efritz/go/pkg/mod/cache/download/github.com/gavv/httpexpect/@v/v2.0.0+incompatible.mod" 17 | } 18 | { 19 | "Path": "github.com/getsentry/raven-go", 20 | "Version": "v0.2.0", 21 | "Time": "2018-11-28T22:11:06Z", 22 | "Dir": "/Users/efritz/go/pkg/mod/github.com/getsentry/raven-go@v0.2.0", 23 | "GoMod": "/Users/efritz/go/pkg/mod/cache/download/github.com/getsentry/raven-go/@v/v0.2.0.mod" 24 | } 25 | { 26 | "Path": "github.com/gfleury/go-bitbucket-v1", 27 | "Version": "v0.0.0-20200312180434-e5170e3280fb", 28 | "Time": "2020-03-12T18:04:34Z", 29 | "Indirect": true, 30 | "Dir": "/Users/efritz/go/pkg/mod/github.com/gfleury/go-bitbucket-v1@v0.0.0-20200312180434-e5170e3280fb", 31 | "GoMod": "/Users/efritz/go/pkg/mod/cache/download/github.com/gfleury/go-bitbucket-v1/@v/v0.0.0-20200312180434-e5170e3280fb.mod", 32 | "GoVersion": "1.14" 33 | } 34 | { 35 | "Path": "github.com/ghodss/yaml", 36 | "Version": "v1.0.0", 37 | "Replace": { 38 | "Path": "github.com/sourcegraph/yaml", 39 | "Version": "v1.0.1-0.20200714132230-56936252f152", 40 | "Time": "2020-07-14T13:22:30Z", 41 | "Dir": "/Users/efritz/go/pkg/mod/github.com/sourcegraph/yaml@v1.0.1-0.20200714132230-56936252f152", 42 | "GoMod": "/Users/efritz/go/pkg/mod/cache/download/github.com/sourcegraph/yaml/@v/v1.0.1-0.20200714132230-56936252f152.mod" 43 | }, 44 | "Dir": "/Users/efritz/go/pkg/mod/github.com/sourcegraph/yaml@v1.0.1-0.20200714132230-56936252f152", 45 | "GoMod": "/Users/efritz/go/pkg/mod/cache/download/github.com/sourcegraph/yaml/@v/v1.0.1-0.20200714132230-56936252f152.mod" 46 | } 47 | { 48 | "Path": "github.com/sourcegraph/sourcegraph/enterprise/lib", 49 | "Version": "v0.0.0-00010101000000-000000000000", 50 | "Replace": { 51 | "Path": "./enterprise/lib", 52 | "Dir": "/Users/efritz/dev/sourcegraph/sourcegraph/enterprise/lib", 53 | "GoMod": "/Users/efritz/dev/sourcegraph/sourcegraph/lib/go.mod", 54 | "GoVersion": "1.16" 55 | }, 56 | "Dir": "/Users/efritz/dev/sourcegraph/sourcegraph/enterprise/lib", 57 | "GoMod": "/Users/efritz/dev/sourcegraph/sourcegraph/lib/go.mod", 58 | "GoVersion": "1.16" 59 | } 60 | ` 61 | 62 | modOutput := ` 63 | { 64 | "Path": "github.com/sourcegraph/lsif-go", 65 | "Main": true, 66 | "Dir": "/home/tjdevries/sourcegraph/lsif-go.git/asdf", 67 | "GoMod": "/home/tjdevries/sourcegraph/lsif-go.git/asdf/go.mod", 68 | "GoVersion": "1.15" 69 | } 70 | ` 71 | 72 | modules, err := parseGoListOutput(output, modOutput, "v1.2.3") 73 | if err != nil { 74 | t.Fatalf("unexpected error: %s", err) 75 | } 76 | 77 | expected := map[string]GoModule{ 78 | "github.com/golang/go": {Name: "github.com/golang/go", Version: "go1.15"}, 79 | "github.com/gavv/httpexpect": {Name: "github.com/gavv/httpexpect", Version: "v2.0.0"}, 80 | "github.com/getsentry/raven-go": {Name: "github.com/getsentry/raven-go", Version: "v0.2.0"}, 81 | "github.com/gfleury/go-bitbucket-v1": {Name: "github.com/gfleury/go-bitbucket-v1", Version: "e5170e3280fb"}, 82 | "github.com/ghodss/yaml": {Name: "github.com/sourcegraph/yaml", Version: "56936252f152"}, 83 | "github.com/sourcegraph/sourcegraph/enterprise/lib": {Name: "./enterprise/lib", Version: "v1.2.3"}, 84 | } 85 | if diff := cmp.Diff(expected, modules); diff != "" { 86 | t.Errorf("unexpected parsed modules (-want +got): %s", diff) 87 | } 88 | } 89 | 90 | func TestCleanVersion(t *testing.T) { 91 | testCases := []struct { 92 | input string 93 | expected string 94 | }{ 95 | {input: "v2.25.0", expected: "v2.25.0"}, 96 | {input: "v2.25.0+incompatible", expected: "v2.25.0"}, 97 | {input: "v0.0.0-20190905194746-02993c407bfb", expected: "02993c407bfb"}, 98 | } 99 | 100 | for _, testCase := range testCases { 101 | if actual := cleanVersion(testCase.input); actual != testCase.expected { 102 | t.Errorf("unexpected clean version. want=%q have=%q", testCase.expected, actual) 103 | } 104 | } 105 | } 106 | 107 | func TestResolveImportPaths(t *testing.T) { 108 | modules := []string{ 109 | "cloud.google.com/go/pubsub", 110 | "github.com/etcd-io/bbolt", 111 | "gitlab.com/nyarla/go-crypt", 112 | "go.etcd.io/etcd", 113 | "go.uber.org/zap", 114 | "golang.org/x/crypto", 115 | "gopkg.in/inf.v0", 116 | "k8s.io/klog", 117 | "rsc.io/binaryregexp", 118 | "rsc.io/quote/v3", 119 | "./enterprise/lib", 120 | } 121 | 122 | expected := map[string]string{ 123 | "cloud.google.com/go/pubsub": "https://github.com/googleapis/google-cloud-go/pubsub", 124 | "github.com/etcd-io/bbolt": "https://github.com/etcd-io/bbolt", 125 | "gitlab.com/nyarla/go-crypt": "https://gitlab.com/nyarla/go-crypt.git", 126 | "go.etcd.io/etcd": "https://github.com/etcd-io/etcd", 127 | "go.uber.org/zap": "https://github.com/uber-go/zap", 128 | "golang.org/x/crypto": "https://go.googlesource.com/crypto", 129 | "gopkg.in/inf.v0": "https://gopkg.in/inf.v0", 130 | "k8s.io/klog": "https://github.com/kubernetes/klog", 131 | "rsc.io/binaryregexp": "https://github.com/rsc/binaryregexp", 132 | "rsc.io/quote/v3": "https://github.com/rsc/quote/v3", 133 | "./enterprise/lib": "https://github.com/sourcegraph/sourcegraph/enterprise/lib", 134 | } 135 | if diff := cmp.Diff(expected, resolveImportPaths("https://github.com/sourcegraph/sourcegraph", modules)); diff != "" { 136 | t.Errorf("unexpected import paths (-want +got): %s", diff) 137 | } 138 | } 139 | 140 | func TestNormalizeMonikerPackage(t *testing.T) { 141 | testCases := map[string]string{ 142 | "fmt": "github.com/golang/go/std/fmt", 143 | 144 | // This happens sometimes in the standard library, that we have "std/" prefixed. 145 | "std/hash": "github.com/golang/go/std/hash", 146 | 147 | // User libs should be unchanged. 148 | "github.com/sourcegraph/sourcegraph/lib": "github.com/sourcegraph/sourcegraph/lib", 149 | 150 | // Unknown libs should not be changed (for example, custom proxy) 151 | "myCustomPackage": "myCustomPackage", 152 | } 153 | 154 | for path, expected := range testCases { 155 | if diff := cmp.Diff(expected, NormalizeMonikerPackage(path)); diff != "" { 156 | t.Errorf("unexpected normalized moniker package (-want +got): %s", diff) 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /docs/examples/smollest/dump.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | G 11 | 12 | 13 | 14 | v6 15 | 16 | (6) resultSet 17 | 18 | 19 | 20 | v7 21 | 22 | (7) definitionResult 23 | 24 | 25 | 26 | v6->v7 27 | 28 | 29 | (9) textDocument/definition 30 | 31 | 32 | 33 | v13 34 | 35 | (13) moniker {"Kind":"export","Scheme":"gomod","Identifier":"smollest"} 36 | 37 | 38 | 39 | v6->v13 40 | 41 | 42 | (16) moniker 43 | 44 | 45 | 46 | v17 47 | 48 | (17) range {"start":{"line":0,"character":8},"end":{"line":0,"character":16}} 49 | 50 | 51 | 52 | v17->v6 53 | 54 | 55 | (18) next 56 | 57 | 58 | 59 | v2 60 | 61 | (2) project 62 | 63 | 64 | 65 | v3 66 | 67 | (3) document "file:///home/tjdevries/sourcegraph/lsif-go.git/imports_work/docs/examples/smollest/lib.go" 68 | 69 | 70 | 71 | v2->v3 72 | 73 | 74 | (28) contains 75 | 76 | 77 | 78 | v4 79 | 80 | (4) document "file:///home/tjdevries/sourcegraph/lsif-go.git/imports_work/docs/examples/smollest/sub.go" 81 | 82 | 83 | 84 | v2->v4 85 | 86 | 87 | (28) contains 88 | 89 | 90 | 91 | v5 92 | 93 | (5) range {"start":{"line":1,"character":8},"end":{"line":1,"character":16}} 94 | 95 | 96 | 97 | v5->v6 98 | 99 | 100 | (8) next 101 | 102 | 103 | 104 | v7->v5 105 | 106 | 107 | (10) item 108 | 109 | 110 | 111 | v3->v5 112 | 113 | 114 | (26) contains 115 | 116 | 117 | 118 | v14 119 | 120 | (14) packageInformation {"Name":"https://github.com/sourcegraph/lsif-go","Version":"16da5d4980c1"} 121 | 122 | 123 | 124 | v13->v14 125 | 126 | 127 | (15) packageInformation 128 | 129 | 130 | 131 | v4->v17 132 | 133 | 134 | (27) contains 135 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /internal/indexer/moniker_test.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | "go/constant" 5 | "go/token" 6 | "go/types" 7 | "sync" 8 | "testing" 9 | 10 | "github.com/google/go-cmp/cmp" 11 | "github.com/sourcegraph/lsif-go/internal/gomod" 12 | "github.com/sourcegraph/sourcegraph/lib/codeintel/lsif/protocol" 13 | "github.com/sourcegraph/sourcegraph/lib/codeintel/lsif/protocol/writer" 14 | ) 15 | 16 | func TestEmitExportMoniker(t *testing.T) { 17 | w := &capturingWriter{} 18 | 19 | indexer := &Indexer{ 20 | repositoryRemote: "github.com/sourcegraph/lsif-go", 21 | repositoryRoot: "/users/efritz/dev/sourcegraph/lsif-go", 22 | projectRoot: "/users/efritz/dev/sourcegraph/lsif-go", 23 | moduleName: "https://github.com/sourcegraph/lsif-go", 24 | moduleVersion: "3.14.159", 25 | emitter: writer.NewEmitter(w), 26 | importMonikerIDs: map[string]uint64{}, 27 | packageInformationIDs: map[string]uint64{}, 28 | importMonikerReferences: map[uint64]map[uint64]map[uint64]setVal{}, 29 | stripedMutex: newStripedMutex(), 30 | } 31 | 32 | object := types.NewConst( 33 | token.Pos(42), 34 | types.NewPackage("github.com/test/pkg", "pkg"), 35 | "foobar", 36 | &types.Basic{}, 37 | constant.MakeBool(true), 38 | ) 39 | 40 | indexer.emitExportMoniker(123, nil, object) 41 | 42 | monikers := findMonikersByRangeOrReferenceResultID(w, 123) 43 | if monikers == nil || len(monikers) < 1 { 44 | t.Fatalf("could not find moniker") 45 | } 46 | if monikers[0].Kind != "export" { 47 | t.Errorf("incorrect moniker kind. want=%q have=%q", "export", monikers[0].Kind) 48 | } 49 | if monikers[0].Scheme != "gomod" { 50 | t.Errorf("incorrect moniker scheme want=%q have=%q", "gomod", monikers[0].Scheme) 51 | } 52 | if monikers[0].Identifier != "github.com/test/pkg:foobar" { 53 | t.Errorf("incorrect moniker identifier. want=%q have=%q", "github.com/test/pkg:foobar", monikers[0].Identifier) 54 | } 55 | 56 | packageInformation := findPackageInformationByMonikerID(w, monikers[0].ID) 57 | if monikers == nil || len(monikers) < 1 { 58 | t.Fatalf("could not find package information") 59 | } 60 | if packageInformation[0].Name != "https://github.com/sourcegraph/lsif-go" { 61 | t.Errorf("incorrect moniker name. want=%q have=%q", "https://github.com/sourcegraph/lsif-go", monikers[0].Kind) 62 | } 63 | if packageInformation[0].Version != "3.14.159" { 64 | t.Errorf("incorrect moniker scheme want=%q have=%q", "3.14.159", monikers[0].Scheme) 65 | } 66 | } 67 | 68 | func TestEmitExportMonikerPreGoMod(t *testing.T) { 69 | w := &capturingWriter{} 70 | 71 | indexer := &Indexer{ 72 | repositoryRemote: "github.com/sourcegraph/lsif-go", 73 | repositoryRoot: "/users/efritz/dev/sourcegraph/lsif-go", 74 | projectRoot: "/users/efritz/dev/sourcegraph/lsif-go", 75 | moduleName: "https://github.com/sourcegraph/lsif-go", 76 | moduleVersion: "3.14.159", 77 | emitter: writer.NewEmitter(w), 78 | importMonikerIDs: map[string]uint64{}, 79 | packageInformationIDs: map[string]uint64{}, 80 | importMonikerReferences: map[uint64]map[uint64]map[uint64]setVal{}, 81 | stripedMutex: newStripedMutex(), 82 | } 83 | 84 | object := types.NewConst( 85 | token.Pos(42), 86 | types.NewPackage("_/users/efritz/dev/sourcegraph/lsif-go/internal/git", "pkg"), 87 | "InferRemote", 88 | &types.Basic{}, 89 | constant.MakeBool(true), 90 | ) 91 | 92 | indexer.emitExportMoniker(123, nil, object) 93 | 94 | monikers := findMonikersByRangeOrReferenceResultID(w, 123) 95 | if monikers == nil || len(monikers) < 1 { 96 | t.Fatalf("could not find moniker") 97 | } 98 | if monikers[0].Kind != "export" { 99 | t.Errorf("incorrect moniker kind. want=%q have=%q", "export", monikers[0].Kind) 100 | } 101 | if monikers[0].Scheme != "gomod" { 102 | t.Errorf("incorrect moniker scheme want=%q have=%q", "gomod", monikers[0].Scheme) 103 | } 104 | if monikers[0].Identifier != "github.com/sourcegraph/lsif-go/internal/git:InferRemote" { 105 | t.Errorf("incorrect moniker identifier. want=%q have=%q", "github.com/sourcegraph/lsif-go/internal/git:InferRemote", monikers[0].Identifier) 106 | } 107 | 108 | packageInformation := findPackageInformationByMonikerID(w, monikers[0].ID) 109 | if monikers == nil || len(monikers) < 1 { 110 | t.Fatalf("could not find package information") 111 | } 112 | if packageInformation[0].Name != "https://github.com/sourcegraph/lsif-go" { 113 | t.Errorf("incorrect moniker kind. want=%q have=%q", "https://github.com/sourcegraph/lsif-go", monikers[0].Kind) 114 | } 115 | if packageInformation[0].Version != "3.14.159" { 116 | t.Errorf("incorrect moniker scheme want=%q have=%q", "3.14.159", monikers[0].Scheme) 117 | } 118 | } 119 | 120 | func TestEmitImportMoniker(t *testing.T) { 121 | w := &capturingWriter{} 122 | 123 | indexer := &Indexer{ 124 | dependencies: map[string]gomod.GoModule{ 125 | "github.com/test/pkg/sub1": {Name: "github.com/test/pkg/sub1", Version: "1.2.3-deadbeef"}, 126 | }, 127 | emitter: writer.NewEmitter(w), 128 | importMonikerIDs: map[string]uint64{}, 129 | packageInformationIDs: map[string]uint64{}, 130 | stripedMutex: newStripedMutex(), 131 | importMonikerChannel: make(chan importMonikerReference, 1), 132 | importMonikerReferences: map[uint64]map[uint64]map[uint64]setVal{}, 133 | } 134 | 135 | object := types.NewConst( 136 | token.Pos(42), 137 | types.NewPackage("github.com/test/pkg/sub1/sub2/sub3", "sub3"), 138 | "foobar", 139 | &types.Basic{}, 140 | constant.MakeBool(true), 141 | ) 142 | 143 | wg := new(sync.WaitGroup) 144 | indexer.startImportMonikerReferenceTracker(wg) 145 | 146 | if !indexer.emitImportMoniker(123, nil, object, &DocumentInfo{DocumentID: 1}) { 147 | t.Fatalf("Failed to emit import moniker") 148 | } 149 | 150 | // TODO: It might be nice to not hard code the elements... but this test is not super fantastic for anything else. 151 | moniker, ok := w.elements[1].(protocol.Moniker) 152 | if !ok { 153 | t.Fatalf("could not find moniker") 154 | } 155 | if moniker.Kind != "import" { 156 | t.Errorf("incorrect moniker kind. want=%q have=%q", "import", moniker.Kind) 157 | } 158 | if moniker.Scheme != "gomod" { 159 | t.Errorf("incorrect moniker scheme want=%q have=%q", "gomod", moniker.Scheme) 160 | } 161 | if moniker.Identifier != "github.com/test/pkg/sub1/sub2/sub3:foobar" { 162 | t.Errorf("incorrect moniker identifier. want=%q have=%q", "github.com/test/pkg/sub1/sub2/sub3:foobar", moniker.Identifier) 163 | } 164 | 165 | packageInformation, ok := w.elements[0].(protocol.PackageInformation) 166 | if !ok { 167 | t.Fatalf("could not find package information") 168 | } 169 | if packageInformation.Name != "github.com/test/pkg/sub1" { 170 | t.Errorf("incorrect moniker kind. want=%q have=%q", "github.com/test/pkg/sub1", moniker.Kind) 171 | } 172 | if packageInformation.Version != "1.2.3-deadbeef" { 173 | t.Errorf("incorrect moniker scheme want=%q have=%q", "1.2.3-deadbeef", moniker.Scheme) 174 | } 175 | } 176 | 177 | func TestPackagePrefixes(t *testing.T) { 178 | expectedPackages := []string{ 179 | "github.com/foo/bar/baz/bonk/internal/secrets", 180 | "github.com/foo/bar/baz/bonk/internal", 181 | "github.com/foo/bar/baz/bonk", 182 | "github.com/foo/bar/baz", 183 | "github.com/foo/bar", 184 | "github.com/foo", 185 | "github.com", 186 | } 187 | 188 | if diff := cmp.Diff(expectedPackages, packagePrefixes("github.com/foo/bar/baz/bonk/internal/secrets")); diff != "" { 189 | t.Errorf("unexpected package prefixes (-want +got): %s", diff) 190 | } 191 | } 192 | 193 | func TestMonikerIdentifierBasic(t *testing.T) { 194 | packages := getTestPackages(t) 195 | p, obj := findUseByName(t, packages, "Score") 196 | 197 | if identifier := makeMonikerIdentifier(NewPackageDataCache(), p, obj); identifier != "Score" { 198 | t.Errorf("unexpected moniker identifier. want=%q have=%q", "Score", identifier) 199 | } 200 | } 201 | 202 | func TestMonikerIdentifierPackageName(t *testing.T) { 203 | packages := getTestPackages(t) 204 | p, obj := findUseByName(t, packages, "sync") 205 | 206 | if identifier := makeMonikerIdentifier(NewPackageDataCache(), p, obj); identifier != "" { 207 | t.Errorf("unexpected moniker identifier. want=%q have=%q", "", identifier) 208 | } 209 | } 210 | 211 | func TestMonikerIdentifierSignature(t *testing.T) { 212 | packages := getTestPackages(t) 213 | p, obj := findDefinitionByName(t, packages, "Doer") 214 | 215 | if identifier := makeMonikerIdentifier(NewPackageDataCache(), p, obj); identifier != "TestStruct.Doer" { 216 | t.Errorf("unexpected moniker identifier. want=%q have=%q", "TestStruct.Doer", identifier) 217 | } 218 | } 219 | 220 | func TestMonikerIdentifierField(t *testing.T) { 221 | packages := getTestPackages(t) 222 | p, obj := findDefinitionByName(t, packages, "NestedB") 223 | 224 | if identifier := makeMonikerIdentifier(NewPackageDataCache(), p, obj); identifier != "TestStruct.FieldWithAnonymousType.NestedB" { 225 | t.Errorf("unexpected moniker identifier. want=%q have=%q", "TestStruct.FieldWithAnonymousType.NestedB", identifier) 226 | } 227 | } 228 | 229 | func TestMonikerEmbeddedField(t *testing.T) { 230 | packages := getTestPackages(t) 231 | p, obj := findDefinitionByName(t, packages, "InnerStruct") 232 | 233 | if identifier := makeMonikerIdentifier(NewPackageDataCache(), p, obj); identifier != "ShellStruct.InnerStruct" { 234 | t.Errorf("unexpected moniker identifier. want=%q have=%q", "ShellStruct.InnerStruct", identifier) 235 | } 236 | } 237 | 238 | func TestJoinMonikerParts(t *testing.T) { 239 | testCases := []struct { 240 | input []string 241 | expected string 242 | }{ 243 | {input: []string{}, expected: ""}, 244 | {input: []string{"a"}, expected: "a"}, 245 | {input: []string{"a", "", "c"}, expected: "a:c"}, 246 | {input: []string{"a", "b", "c"}, expected: "a:b:c"}, 247 | } 248 | 249 | for _, testCase := range testCases { 250 | if actual := joinMonikerParts(testCase.input...); actual != testCase.expected { 251 | t.Errorf("unexpected moniker identifier. want=%q have=%q", testCase.expected, actual) 252 | } 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /internal/indexer/moniker.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | "fmt" 5 | "go/types" 6 | "strings" 7 | 8 | "github.com/sourcegraph/lsif-go/internal/gomod" 9 | "golang.org/x/tools/go/packages" 10 | ) 11 | 12 | // emitExportMoniker emits an export moniker for the given object linked to the given source 13 | // identifier (either a range or a result set identifier). This will also emit links between 14 | // the moniker vertex and the package information vertex representing the current module. 15 | func (i *Indexer) emitExportMoniker(sourceID uint64, p *packages.Package, obj ObjectLike) { 16 | if i.moduleName == "" { 17 | // Unknown dependencies, skip export monikers 18 | return 19 | } 20 | 21 | packageName := makeMonikerPackage(obj) 22 | if strings.HasPrefix(packageName, "_"+i.projectRoot) { 23 | packageName = i.repositoryRemote + strings.TrimSuffix(packageName[len(i.projectRoot)+1:], "_test") 24 | } 25 | 26 | // Emit export moniker (uncached as these are on unique definitions) 27 | monikerID := i.emitter.EmitMoniker("export", "gomod", joinMonikerParts( 28 | packageName, 29 | makeMonikerIdentifier(i.packageDataCache, p, obj), 30 | )) 31 | 32 | // Lazily emit package information vertex and attach it to moniker 33 | packageInformationID := i.ensurePackageInformation(i.moduleName, i.moduleVersion) 34 | _ = i.emitter.EmitPackageInformationEdge(monikerID, packageInformationID) 35 | 36 | // Attach moniker to source element 37 | _ = i.emitter.EmitMonikerEdge(sourceID, monikerID) 38 | } 39 | 40 | // joinMonikerParts joins the non-empty strings in the given list by a colon. 41 | func joinMonikerParts(parts ...string) string { 42 | nonEmpty := parts[:0] 43 | for _, s := range parts { 44 | if s != "" { 45 | nonEmpty = append(nonEmpty, s) 46 | } 47 | } 48 | 49 | return strings.Join(nonEmpty, ":") 50 | } 51 | 52 | // emitImportMoniker emits an import moniker for the given object linked to the given source 53 | // identifier (either a range or a result set identifier). This will also emit links between 54 | // the moniker vertex and the package information vertex representing the dependency containing 55 | // the identifier. 56 | func (i *Indexer) emitImportMoniker(rangeID uint64, p *packages.Package, obj ObjectLike, document *DocumentInfo) bool { 57 | pkg := makeMonikerPackage(obj) 58 | monikerIdentifier := joinMonikerParts(pkg, makeMonikerIdentifier(i.packageDataCache, p, obj)) 59 | 60 | for _, moduleName := range packagePrefixes(pkg) { 61 | if module, ok := i.dependencies[moduleName]; ok { 62 | // Lazily emit package information vertex 63 | packageInformationID := i.ensurePackageInformation(module.Name, module.Version) 64 | 65 | // Lazily emit moniker vertex 66 | monikerID := i.ensureImportMoniker(monikerIdentifier, packageInformationID) 67 | 68 | // Monikers will be linked during Indexer.linkImportMonikersToRanges 69 | i.addImportMonikerReference(monikerID, rangeID, document.DocumentID) 70 | 71 | return true 72 | } 73 | } 74 | 75 | return false 76 | } 77 | 78 | // emitImplementationMoniker emits an implementation moniker for the given object linked to the given source 79 | // identifier (either a range or a result set identifier). This will also emit links between 80 | // the moniker vertex and the package information vertex representing the dependency containing 81 | // the identifier. 82 | func (i *Indexer) emitImplementationMoniker(resultSet uint64, pkg string, monikerIdentifier string) bool { 83 | for _, moduleName := range packagePrefixes(pkg) { 84 | if module, ok := i.dependencies[moduleName]; ok { 85 | // Lazily emit package information vertex 86 | packageInformationID := i.ensurePackageInformation(module.Name, module.Version) 87 | 88 | // Lazily emit moniker vertex 89 | monikerID := i.ensureImplementationMoniker(monikerIdentifier, packageInformationID) 90 | 91 | // Link the result set to the moniker 92 | i.emitter.EmitMonikerEdge(resultSet, monikerID) 93 | 94 | return true 95 | } 96 | } 97 | 98 | return false 99 | } 100 | 101 | // packagePrefixes returns all prefix of the go package path. For example, the package 102 | // `foo/bar/baz` will return the slice containing `foo/bar/baz`, `foo/bar`, and `foo`. 103 | func packagePrefixes(packageName string) []string { 104 | parts := strings.Split(packageName, "/") 105 | prefixes := make([]string, len(parts)) 106 | 107 | for i := 1; i <= len(parts); i++ { 108 | prefixes[len(parts)-i] = strings.Join(parts[:i], "/") 109 | } 110 | 111 | return prefixes 112 | } 113 | 114 | // ensurePackageInformation returns the identifier of a package information vertex with the 115 | // give name and version. A vertex will be emitted only if one with the same name has not 116 | // yet been emitted. 117 | func (i *Indexer) ensurePackageInformation(name, version string) uint64 { 118 | i.packageInformationIDsMutex.RLock() 119 | packageInformationID, ok := i.packageInformationIDs[name] 120 | i.packageInformationIDsMutex.RUnlock() 121 | if ok { 122 | return packageInformationID 123 | } 124 | 125 | i.packageInformationIDsMutex.Lock() 126 | defer i.packageInformationIDsMutex.Unlock() 127 | 128 | if packageInformationID, ok := i.packageInformationIDs[name]; ok { 129 | return packageInformationID 130 | } 131 | 132 | packageInformationID = i.emitter.EmitPackageInformation(name, "gomod", version) 133 | i.packageInformationIDs[name] = packageInformationID 134 | return packageInformationID 135 | } 136 | 137 | // ensureImportMoniker returns the identifier of a moniker vertex with the give identifier 138 | // attached to the given package information identifier. A vertex will be emitted only if 139 | // one with the same key has not yet been emitted. 140 | func (i *Indexer) ensureImportMoniker(identifier string, packageInformationID uint64) uint64 { 141 | key := fmt.Sprintf("%s:%d", identifier, packageInformationID) 142 | 143 | i.importMonikerIDsMutex.RLock() 144 | monikerID, ok := i.importMonikerIDs[key] 145 | i.importMonikerIDsMutex.RUnlock() 146 | if ok { 147 | return monikerID 148 | } 149 | 150 | i.importMonikerIDsMutex.Lock() 151 | defer i.importMonikerIDsMutex.Unlock() 152 | 153 | if monikerID, ok := i.importMonikerIDs[key]; ok { 154 | return monikerID 155 | } 156 | 157 | monikerID = i.emitter.EmitMoniker("import", "gomod", identifier) 158 | _ = i.emitter.EmitPackageInformationEdge(monikerID, packageInformationID) 159 | i.importMonikerIDs[key] = monikerID 160 | return monikerID 161 | } 162 | 163 | // ensureImplementationMoniker returns the identifier of a moniker vertex with the give identifier 164 | // attached to the given package information identifier. A vertex will be emitted only if 165 | // one with the same key has not yet been emitted. 166 | // 167 | // While other "ensure*Moniker" functions must use locks, Indexer.indexImplementations is single threaded, 168 | // so there is no need to use locks to hold the keys. 169 | func (i *Indexer) ensureImplementationMoniker(identifier string, packageInformationID uint64) uint64 { 170 | key := fmt.Sprintf("%s:%d", identifier, packageInformationID) 171 | 172 | if monikerID, ok := i.implementationMonikerIDs[key]; ok { 173 | return monikerID 174 | } 175 | 176 | monikerID := i.emitter.EmitMoniker("implementation", "gomod", identifier) 177 | _ = i.emitter.EmitPackageInformationEdge(monikerID, packageInformationID) 178 | i.implementationMonikerIDs[key] = monikerID 179 | return monikerID 180 | } 181 | 182 | // makeMonikerPackage returns the package prefix used to construct a unique moniker for the given object. 183 | // A full moniker has the form `{package prefix}:{identifier suffix}`. 184 | func makeMonikerPackage(obj ObjectLike) string { 185 | var pkgName string 186 | if v, ok := obj.(*types.PkgName); ok { 187 | // gets the full path of the package name, rather than just the name. 188 | // So instead of "http", it will return "net/http" 189 | pkgName = v.Imported().Path() 190 | } else { 191 | pkgName = pkgPath(obj) 192 | } 193 | 194 | return gomod.NormalizeMonikerPackage(pkgName) 195 | } 196 | 197 | // makeMonikerIdentifier returns the identifier suffix used to construct a unique moniker for the given object. 198 | // A full moniker has the form `{package prefix}:{identifier suffix}`. The identifier is meant to act as a 199 | // qualified type path to the given object (e.g. `StructName.FieldName` or `StructName.MethodName`). 200 | func makeMonikerIdentifier(packageDataCache *PackageDataCache, p *packages.Package, obj ObjectLike) string { 201 | if _, ok := obj.(*types.PkgName); ok { 202 | // Packages are identified uniquely by their package prefix 203 | return "" 204 | } 205 | 206 | if _, ok := obj.(*PkgDeclaration); ok { 207 | // Package declarations are identified uniquely by their package name 208 | return "" 209 | } 210 | 211 | if v, ok := obj.(*types.Var); ok && v.IsField() { 212 | if target := p.Imports[obj.Pkg().Path()]; target != nil { 213 | p = target 214 | } 215 | 216 | // Qualifiers for fields were populated as pre-load step so we do not need to traverse 217 | // the AST path back up to the root to find the enclosing type specs and fields with an 218 | // anonymous struct type. 219 | return strings.Join(packageDataCache.MonikerPath(p, obj.Pos()), ".") 220 | } 221 | 222 | if signature, ok := obj.Type().(*types.Signature); ok { 223 | if recv := signature.Recv(); recv != nil { 224 | return strings.Join([]string{ 225 | // Qualify function with receiver stripped of a pointer indicator `*` and its package path 226 | strings.TrimPrefix(strings.TrimPrefix(recv.Type().String(), "*"), pkgPath(obj)+"."), 227 | obj.Name(), 228 | }, ".") 229 | } 230 | } 231 | 232 | return obj.Name() 233 | } 234 | 235 | // pkgPath can be used to always return a string for the obj.Pkg().Path() 236 | // 237 | // At this time, I am only aware of objects in the Universe scope that do not 238 | // have `obj.Pkg()` -> nil. When we try and call `obj.Pkg().Path()` on nil, we 239 | // have problems. 240 | // 241 | // This function will attempt to lookup the corresponding obj in the universe 242 | // scope, and if it finds the object, will return "builtin" (which is the location 243 | // in the go standard library where they are defined). 244 | func pkgPath(obj ObjectLike) string { 245 | pkg := obj.Pkg() 246 | 247 | // Handle Universe Scoped objs. 248 | if pkg == nil { 249 | // Here be dragons: 250 | switch v := obj.(type) { 251 | case *types.Func: 252 | switch typ := v.Type().(type) { 253 | case *types.Signature: 254 | recv := typ.Recv() 255 | universeObj := types.Universe.Lookup(recv.Type().String()) 256 | if universeObj != nil { 257 | return "builtin" 258 | } 259 | } 260 | } 261 | 262 | // Do not allow to fall through to returning pkg.Path() 263 | // 264 | // If this becomes a problem more in the future, we can just default to 265 | // returning "builtin" but as of now this handles all the cases that I 266 | // know of. 267 | panic("Unhandled nil obj.Pkg()") 268 | } 269 | 270 | return pkg.Path() 271 | } 272 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | # Changelog 9 | 10 | All notable changes to `lsif-go` are documented in this file. 11 | 12 | ## v1.9.2 13 | 14 | ### Fixed 15 | 16 | - Fixed issues with indexing `golang/go` repo 17 | 18 | ## v1.9.1 19 | 20 | ### Changed 21 | 22 | - The `--no-animation` flag is now enabled by default. Use the flag `--animation` to get the old default behavior. 23 | 24 | ## v1.9.0 25 | 26 | ### Changed 27 | 28 | - Update to Go 1.18.2. This should improve indexing of code that uses generics. (#248) 29 | 30 | ### Removed 31 | 32 | - API Documentation. The experimental `-enable-api-docs` flag has been removed and this functionality is no longer supported. 33 | 34 | ## v1.8.0 35 | 36 | ### Changed 37 | 38 | - Pre-built binary and docker image now support apple silicon. (#245) 39 | 40 | ## v1.7.7 41 | 42 | ### Changed 43 | 44 | - Dropped `CGO_ENABLED=0` so users can decide whether or not to enable cgo. (#233) 45 | 46 | ## v1.7.6 47 | 48 | ### Changed 49 | 50 | - The accompanying Docker image uses Go 1.17.7 and `src-cli` 3.37.0. (#235) 51 | 52 | ### Fixed 53 | 54 | - Fixes a couple of panics (#230, #232). 55 | 56 | ## v1.7.5 57 | 58 | ### Features 59 | 60 | - Added `textDocument/implementation` support. 61 | - Also added Sourcegraph specific cross repository implementation support via monikers. 62 | 63 | ### Changed 64 | 65 | - Add ability to enable/disable different generation features: 66 | - Sourcegraph API Documentation generation (`--no-enable-api-docs` flag) 67 | - Implementation generation (`--no-enable-implemenations` flag) 68 | 69 | ### Fixed 70 | 71 | - Many issues relating to package declarations, imports and structs have been fixed. 72 | - See [Package Declarations](./docs/package_declarations.md) 73 | - See [Imports](./docs/imports.md) 74 | - See [Structs](./docs/structs.md) 75 | - Additionally, package declarations are now indexed. 76 | - No longer emits duplicate `next` edges. 77 | - Prints better help when uncrecognized import path 78 | 79 | ## v1.6.7 80 | 81 | ### Fixed 82 | 83 | - An issue where API docs would not be generated for packages containing multiple `init` functions in the same file. [#195](https://github.com/sourcegraph/lsif-go/pull/195) 84 | - An issue where API docs would not be generated for packages contianing multiple (illegal) `main` functions in the same file ([example](https://raw.githubusercontent.com/golang/go/master/test/mainsig.go)). [#196](https://github.com/sourcegraph/lsif-go/pull/196) 85 | 86 | ## v1.6.6 87 | 88 | ### Fixed 89 | 90 | - An issue where illegal code (conflicting test/non-test symbol names, such as in some moby/moby packages) would fail to index. [#186](https://github.com/sourcegraph/lsif-go/pull/186) 91 | 92 | ## v1.6.5 93 | 94 | ### Fixed 95 | 96 | - Fixed generation of standard library monikers. [#184](https://github.com/sourcegraph/lsif-go/pull/184) 97 | - An issue where indexing would fail if `package main` contained exported data types. [#185](https://github.com/sourcegraph/lsif-go/pull/185) 98 | 99 | (v1.6.4 had issues in our release process, it did not correctly release: v1.6.5 corrects this.) 100 | 101 | ## v1.6.3 102 | 103 | ### Changed 104 | 105 | - Improved error messages. 106 | 107 | ## v1.6.2 108 | 109 | ### Fixed 110 | 111 | - API docs no longer incorrectly tags Functions/Variables/etc sections as a package. 112 | - API docs no longer emits null tag lists in violation of the spec. 113 | 114 | ## v1.6.1 115 | 116 | ### Fixed 117 | 118 | - API docs no longer incorrectly tags some methods as functions and vice-versa. 119 | 120 | ## v1.6.0 121 | 122 | ### Added 123 | 124 | - API docs now emit data linking `resultSet`s to `documentationResult`s, making it possible to go from hover/definition/references to API docs and vice-versa. 125 | - API docs now respect the latest Sourcegraph extension spec. 126 | - API docs now emit search keys for documentation to enable search indexing. 127 | 128 | ### Changed 129 | 130 | - API docs index pages are now directory-structured, instead of a flat list of Go packages. 131 | - API docs symbols are now sorted (exported-first, alphabetical order.) 132 | 133 | ### Fixed 134 | 135 | - API docs no longer include blank const/var declarations (`const _ = ...`) 136 | - API docs now only index top-level declarations, not e.g. variables inside functions. 137 | - API docs do a better job of trimming very long var/const declaration lines. 138 | - API docs no longer emit an empty "Functions" section if there are no functions in a package. 139 | - API docs no longer emit duplicate path IDs, which were forbidden in the spec. 140 | - API docs now emit many more tags for documentation sections: whether something is a function, const, var, public, etc. 141 | - API docs now tag benchmark/test functions as such properly. 142 | 143 | ## v1.5.0 144 | 145 | ### Added 146 | 147 | - API documentation is now emitted as [an extension to LSIF](https://github.com/sourcegraph/sourcegraph/pull/20108) in order to support documentation generation in the form of e.g. https://pkg.go.dev and https://godocs.io. [#150](https://github.com/sourcegraph/lsif-go/pull/150) 148 | 149 | ### Changed 150 | 151 | - :rotating_light: Changed package module version generation to make cross-index queries accurate. Cross-linking may not work with indexes created before v1.5.0. [#152](https://github.com/sourcegraph/lsif-go/pull/152) 152 | - Improve moniker identifiers for exported identifiers in projects with no go.mod file. [#153](https://github.com/sourcegraph/lsif-go/pull/153) 153 | 154 | ### Fixed 155 | 156 | - Fixed moniker identifiers for composite structs and interfaces. [#135](https://github.com/sourcegraph/lsif-go/pull/135) 157 | - Fixed definition relationship with composite structs and interfaces. [#156](https://github.com/sourcegraph/lsif-go/pull/156) 158 | - Fixed error-on-startup caused by unresolvable module name in go.mod file. [#157](https://github.com/sourcegraph/lsif-go/pull/157) 159 | 160 | ## v1.4.0 161 | 162 | ### Added 163 | 164 | - Added const values to hover text. [#144](https://github.com/sourcegraph/lsif-go/pull/144) 165 | - Support replace directives in go.mod. [#145](https://github.com/sourcegraph/lsif-go/pull/145) 166 | - Infer package name from git upstream when go.mod file is absent. [#149](https://github.com/sourcegraph/lsif-go/pull/149) 167 | 168 | ### Changed 169 | 170 | - :rotating_light: Changed moniker identifier generation to support replace directives and vanity imports. Cross-index linking will work only for indexes created on or after v1.4.0. [#145](https://github.com/sourcegraph/lsif-go/pull/145) 171 | - Deduplicated import moniker vertices. [#146](https://github.com/sourcegraph/lsif-go/pull/146) 172 | - Update lsif-protocol dependency. [#136](https://github.com/sourcegraph/lsif-go/pull/136) 173 | - Avoid scanning duplicate test packages. [#138](https://github.com/sourcegraph/lsif-go/pull/138) 174 | 175 | ### Fixed 176 | 177 | - Fix bad moniker generation for cross-index fields. [#148](https://github.com/sourcegraph/lsif-go/pull/148) 178 | 179 | ## v1.3.1 180 | 181 | ### Fixed 182 | 183 | - Fixed type assertion panic with aliases to anonymous structs. [#134](https://github.com/sourcegraph/lsif-go/pull/134) 184 | 185 | ## v1.3.0 186 | 187 | ### Changed 188 | 189 | - Type alias hovers now name the aliased type e.g. `type Alias = pkg.Original`. [#131](https://github.com/sourcegraph/lsif-go/pull/131) 190 | 191 | ### Fixed 192 | 193 | - Definition of the RHS type symbol in a type alias is no longer the type alias itself but the type being aliased. [#131](https://github.com/sourcegraph/lsif-go/pull/131) 194 | 195 | ## v1.2.0 196 | 197 | ### Changed 198 | 199 | - :rotating_light: The `go mod download` step is no longer performed implicitly prior to loading packages. [#115](https://github.com/sourcegraph/lsif-go/pull/115) 200 | - :rotating_light: Application flags have been updated. [#115](https://github.com/sourcegraph/lsif-go/pull/115), [#118](https://github.com/sourcegraph/lsif-go/pull/118) 201 | - `-v` is now for verbosity, not `--version` (use `-V` instead for version) 202 | - `-vv` and `-vvv` increase verbosity levels 203 | - `--module-root` validation is fixed and can now correctly point to a directory containing a go.mod file outside of the project root 204 | - Renamed flags for consistent casing: 205 | 206 | | Previous | Current | 207 | | ---------------- | ----------------- | 208 | | `out` | `output` | 209 | | `projectRoot` | `project-root` | 210 | | `moduleRoot` | `module-root` | 211 | | `repositoryRoot` | `repository-root` | 212 | | `noOutput` | `quiet` | 213 | | `noProgress` | `no-animation` | 214 | 215 | 216 | 217 | ### Fixed 218 | 219 | - Fixed a panic that occurs when a struct field contains certain structtag content. [#116](https://github.com/sourcegraph/lsif-go/pull/116) 220 | - Packages with no documentation no longer have the hover text `'`. [#120](https://github.com/sourcegraph/lsif-go/pull/120) 221 | - Fixed incorrect indexing of typeswitch. The symbolic variable in the type switch header and all it occurrences in the case clauses are now properly linked, and the hover text of each occurrence contains the refined type. [#122](https://github.com/sourcegraph/lsif-go/pull/122) 222 | 223 | ## v1.1.4 224 | 225 | ### Changed 226 | 227 | - Replaced "Preloading hover text and moniker paths" step with on-demand processing of packages. This should give a small index time speed boost and is likely to lower resident memory in some environments. [#104](https://github.com/sourcegraph/lsif-go/pull/104) 228 | 229 | ## v1.1.3 230 | 231 | ### Changed 232 | 233 | - Additional updates to lower resident memory. [#109](https://github.com/sourcegraph/lsif-go/pull/109) 234 | 235 | ## v1.1.2 236 | 237 | ### Fixed 238 | 239 | - Downgraded go1.15 to go1.14 in Dockerfile to help diagnose customer build issues. [5d8865d](https://github.com/sourcegraph/lsif-go/commit/5d8865d6feacb4fce3313cade2c61dc29c6271e6) 240 | 241 | ## v1.1.1 242 | 243 | ### Fixed 244 | 245 | - Replaced the digest of the golang base image. [ae1cd6e](https://github.com/sourcegraph/lsif-go/commit/ae1cd6e97cf6551e68da9f010a3d86f438552bdb) 246 | 247 | ## v1.1.0 248 | 249 | ### Added 250 | 251 | - Added `--verbose` flag. [#101](https://github.com/sourcegraph/lsif-go/pull/101) 252 | 253 | ### Fixed 254 | 255 | - Fix slice out of bounds error when processing references. [#103](https://github.com/sourcegraph/lsif-go/pull/103) 256 | - Misc updates to lower resident memory. [#105](https://github.com/sourcegraph/lsif-go/pull/105), [#106](https://github.com/sourcegraph/lsif-go/pull/106) 257 | 258 | ## v1.0.0 259 | 260 | - Initial stable release. 261 | -------------------------------------------------------------------------------- /internal/indexer/package_data_cache.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | "go/types" 7 | "strings" 8 | "sync" 9 | 10 | "golang.org/x/tools/go/packages" 11 | ) 12 | 13 | // PackageDataCache is a cache of hover text and enclosing type identifiers by file and token position. 14 | type PackageDataCache struct { 15 | m sync.RWMutex 16 | packageData map[*packages.Package]*PackageData 17 | } 18 | 19 | // NewPackageDataCache creates a new empty PackageDataCache. 20 | func NewPackageDataCache() *PackageDataCache { 21 | return &PackageDataCache{ 22 | packageData: map[*packages.Package]*PackageData{}, 23 | } 24 | } 25 | 26 | // Text will return the hover text extracted from the given package for the symbol at the given position. 27 | // This method will parse the package if the package results haven't been previously calculated or have been 28 | // evicted from the cache. 29 | func (l *PackageDataCache) Text(p *packages.Package, position token.Pos) string { 30 | return extractHoverText(l.getPackageData(p).HoverText[position]) 31 | } 32 | 33 | // MonikerPath will return the names of enclosing nodes extracted form the given package for the symbol at 34 | // the given position. This method will parse the package if the package results haven't been previously 35 | // calculated or have been evicted from the cache. 36 | func (l *PackageDataCache) MonikerPath(p *packages.Package, position token.Pos) []string { 37 | return l.getPackageData(p).MonikerPaths[position] 38 | } 39 | 40 | // Stats returns a PackageDataCacheStats object with the number of unique packages traversed. 41 | func (l *PackageDataCache) Stats() PackageDataCacheStats { 42 | return PackageDataCacheStats{ 43 | NumPks: uint(len(l.packageData)), 44 | } 45 | } 46 | 47 | // getPackageData will return a package data value for the given package. If the data for this package has not 48 | // already been loaded, it will be loaded immediately. This method will block until the package data has been 49 | // completely loaded before returning to the caller. 50 | func (l *PackageDataCache) getPackageData(p *packages.Package) *PackageData { 51 | data := l.getPackageDataRaw(p) 52 | data.load(p) 53 | return data 54 | } 55 | 56 | // getPackageDataRaw will return the package data value for the given package or create one of it doesn't exist. 57 | // It is not guaranteed that the value has bene loaded, so load (which is idempotent) should be called before use. 58 | func (l *PackageDataCache) getPackageDataRaw(p *packages.Package) *PackageData { 59 | l.m.RLock() 60 | data, ok := l.packageData[p] 61 | l.m.RUnlock() 62 | if ok { 63 | return data 64 | } 65 | 66 | l.m.Lock() 67 | defer l.m.Unlock() 68 | if data, ok = l.packageData[p]; ok { 69 | return data 70 | } 71 | 72 | data = &PackageData{ 73 | HoverText: map[token.Pos]ast.Node{}, 74 | MonikerPaths: map[token.Pos][]string{}, 75 | } 76 | l.packageData[p] = data 77 | return data 78 | } 79 | 80 | // PackageData is a cache of hover text and moniker paths by token position within a package. 81 | type PackageData struct { 82 | once sync.Once 83 | HoverText map[token.Pos]ast.Node 84 | MonikerPaths map[token.Pos][]string 85 | } 86 | 87 | // load will parse the package and populate the maps of hover text and moniker paths. This method is 88 | // idempotent. All calls to this method will block until the first call has completed. 89 | func (data *PackageData) load(p *packages.Package) { 90 | data.once.Do(func() { 91 | definitionPositions, fieldPositions := interestingPositions(p) 92 | 93 | for _, root := range p.Syntax { 94 | visit(root, definitionPositions, fieldPositions, data.HoverText, data.MonikerPaths, nil, nil) 95 | } 96 | }) 97 | } 98 | 99 | // interestingPositions returns a pair of maps whose keys are token positions for which we want values 100 | // in the package data cache's hoverText and monikerPaths maps. Determining which types of types we will 101 | // query for this data and populating values only for those nodes saves a lot of resident memory. 102 | func interestingPositions(p *packages.Package) (map[token.Pos]struct{}, map[token.Pos]struct{}) { 103 | hoverTextPositions := map[token.Pos]struct{}{} 104 | monikerPathPositions := map[token.Pos]struct{}{} 105 | 106 | for _, obj := range p.TypesInfo.Defs { 107 | if shouldHaveHoverText(obj) { 108 | hoverTextPositions[obj.Pos()] = struct{}{} 109 | } 110 | if isField(obj) { 111 | monikerPathPositions[obj.Pos()] = struct{}{} 112 | } 113 | } 114 | 115 | for _, obj := range p.TypesInfo.Uses { 116 | if isField(obj) { 117 | monikerPathPositions[obj.Pos()] = struct{}{} 118 | } 119 | } 120 | 121 | return hoverTextPositions, monikerPathPositions 122 | } 123 | 124 | // visit walks the AST for a file and assigns hover text and a moniker path to interesting positions. 125 | // A position's hover text is the comment associated with the deepest node that encloses the position. 126 | // A position's moniker path is the name of the object prefixed with the names of the containers that 127 | // enclose that position. 128 | func visit( 129 | node ast.Node, // Current node 130 | hoverTextPositions map[token.Pos]struct{}, // Positions for hover text assignment 131 | monikerPathPositions map[token.Pos]struct{}, // Positions for moniker paths assignment 132 | hoverTextMap map[token.Pos]ast.Node, // Target hover text map 133 | monikerPathMap map[token.Pos][]string, // Target moniker path map 134 | nodeWithHoverText ast.Node, // The ancestor node with non-empty hover text (if any) 135 | monikerPath []string, // The moniker path constructed up to this node 136 | ) { 137 | if canExtractHoverText(node) { 138 | // If we have hover text replace whatever ancestor node we might 139 | // have. We have more relevant text on this node, so just use that. 140 | nodeWithHoverText = node 141 | } 142 | 143 | // If we're a field or type, update our moniker path 144 | newMonikerPath := updateMonikerPath(monikerPath, node) 145 | 146 | for _, child := range childrenOf(node) { 147 | visit( 148 | child, 149 | hoverTextPositions, 150 | monikerPathPositions, 151 | hoverTextMap, 152 | monikerPathMap, 153 | chooseNodeWithHoverText(node, child), 154 | newMonikerPath, 155 | ) 156 | } 157 | 158 | if _, ok := hoverTextPositions[node.Pos()]; ok { 159 | hoverTextMap[node.Pos()] = nodeWithHoverText 160 | } 161 | if _, ok := monikerPathPositions[node.Pos()]; ok { 162 | monikerPathMap[node.Pos()] = newMonikerPath 163 | } 164 | } 165 | 166 | // updateMonikerPath returns the given slice plus the name of the given node if it has a name that 167 | // can uniquely identify it along a path of nodes to the root of the file (an enclosing type). 168 | // Otherwise, the given slice is returned unchanged. This function does not modify the input slice. 169 | func updateMonikerPath(monikerPath []string, node ast.Node) []string { 170 | switch q := node.(type) { 171 | case *ast.Field: 172 | // Handle field name/names 173 | if len(q.Names) > 0 { 174 | // Handle things like `a, b, c T`. If there are multiple names we just default to the first 175 | // one as each field must belong on at most one moniker path. This is sub-optimal and 176 | // should be addressed in https://github.com/sourcegraph/lsif-go/issues/154. 177 | return addString(monikerPath, q.Names[0].String()) 178 | } 179 | 180 | // Handle embedded types 181 | if name, ok := q.Type.(*ast.Ident); ok { 182 | return addString(monikerPath, name.Name) 183 | } 184 | 185 | // Handle embedded types that are selectors, like http.Client 186 | if selector, ok := q.Type.(*ast.SelectorExpr); ok { 187 | return addString(monikerPath, selector.Sel.Name) 188 | } 189 | 190 | case *ast.TypeSpec: 191 | // Add the top-level type spec (e.g. `type X struct` and `type Y interface`) 192 | return addString(monikerPath, q.Name.String()) 193 | } 194 | 195 | return monikerPath 196 | } 197 | 198 | // addString creates a new slice composed of the element of slice plus the given value. 199 | // This function does not modify the input slice. 200 | func addString(slice []string, value string) []string { 201 | newSlice := make([]string, len(slice), len(slice)+1) 202 | copy(newSlice, slice) 203 | newSlice = append(newSlice, value) 204 | return newSlice 205 | } 206 | 207 | // childrenOf returns the direct non-nil children of ast.Node n. 208 | func childrenOf(n ast.Node) (children []ast.Node) { 209 | ast.Inspect(n, func(node ast.Node) bool { 210 | if node == n { 211 | return true 212 | } 213 | if node != nil { 214 | children = append(children, node) 215 | } 216 | return false 217 | }) 218 | 219 | return children 220 | } 221 | 222 | // isField returns true if the given object is a field. 223 | func isField(obj ObjectLike) bool { 224 | if v, ok := obj.(*types.Var); ok && v.IsField() { 225 | return true 226 | } 227 | return false 228 | } 229 | 230 | // shouldHaveHoverText returns true if the object is a type for which we should store hover text. This 231 | // is similar but distinct from the set of types from which we _extract_ hover text. See canExtractHoverText 232 | // for those types. This function returns true for the set of objects for which we actually call the methods 233 | // findHoverContents or findExternalHoverContents (see hover.go). 234 | func shouldHaveHoverText(obj ObjectLike) bool { 235 | switch obj.(type) { 236 | case *types.Const: 237 | return true 238 | case *types.Func: 239 | return true 240 | case *types.Label: 241 | return true 242 | case *types.TypeName: 243 | return true 244 | case *types.Var: 245 | return true 246 | } 247 | 248 | return false 249 | } 250 | 251 | // extractHoverText returns the comments attached to the given node. 252 | func extractHoverText(node ast.Node) string { 253 | switch v := node.(type) { 254 | case *ast.FuncDecl: 255 | return v.Doc.Text() 256 | case *ast.GenDecl: 257 | return v.Doc.Text() 258 | case *ast.TypeSpec: 259 | return v.Doc.Text() 260 | case *ast.ValueSpec: 261 | return v.Doc.Text() 262 | case *ast.Field: 263 | return strings.TrimSpace(v.Doc.Text() + "\n" + v.Comment.Text()) 264 | } 265 | 266 | return "" 267 | } 268 | 269 | // canExtractHoverText returns true if the node has non-empty comments extractable by extractHoverText. 270 | func canExtractHoverText(node ast.Node) bool { 271 | switch v := node.(type) { 272 | case *ast.FuncDecl: 273 | return !commentGroupsEmpty(v.Doc) 274 | case *ast.GenDecl: 275 | return !commentGroupsEmpty(v.Doc) 276 | case *ast.TypeSpec: 277 | return !commentGroupsEmpty(v.Doc) 278 | case *ast.ValueSpec: 279 | return !commentGroupsEmpty(v.Doc) 280 | case *ast.Field: 281 | return !commentGroupsEmpty(v.Doc, v.Comment) 282 | } 283 | 284 | return false 285 | } 286 | 287 | // commentGroupsEmpty returns true if all of the given comments groups are empty. 288 | func commentGroupsEmpty(gs ...*ast.CommentGroup) bool { 289 | for _, g := range gs { 290 | if g != nil && len(g.List) > 0 { 291 | return false 292 | } 293 | } 294 | 295 | return true 296 | } 297 | 298 | // chooseNodeWithHoverText returns the parent node if the relationship between the parent and child is 299 | // one in which comments can be reasonably shared. This will return a nil node for most relationships, 300 | // except things like (1) FuncDecl -> Ident, in which case we want to store the function's comment 301 | // in the ident, or (2) GenDecl -> TypeSpec, in which case we want to store the generic declaration's 302 | // comments if the type node doesn't have any directly attached to it. 303 | func chooseNodeWithHoverText(parent, child ast.Node) ast.Node { 304 | if _, ok := parent.(*ast.GenDecl); ok { 305 | return parent 306 | } 307 | if _, ok := child.(*ast.Ident); ok { 308 | return parent 309 | } 310 | 311 | return nil 312 | } 313 | -------------------------------------------------------------------------------- /internal/gomod/dependencies.go: -------------------------------------------------------------------------------- 1 | package gomod 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "log" 9 | "net/url" 10 | "path" 11 | "regexp" 12 | "runtime" 13 | "strings" 14 | "sync" 15 | 16 | "github.com/sourcegraph/lsif-go/internal/command" 17 | "github.com/sourcegraph/lsif-go/internal/output" 18 | "golang.org/x/tools/go/vcs" 19 | ) 20 | 21 | type GoModule struct { 22 | Name string 23 | Version string 24 | } 25 | 26 | // ListDependencies returns a map from dependency import paths to the imported module's name 27 | // and version as declared by the go.mod file in the current directory. The given root module 28 | // and version are used to resolve replace directives with local file paths. The root module 29 | // is expected to be a resolved import path (a valid URL, including a scheme). 30 | func ListDependencies(dir, rootModule, rootVersion string, outputOptions output.Options) (dependencies map[string]GoModule, err error) { 31 | if !isModule(dir) { 32 | log.Println("WARNING: No go.mod file found in current directory.") 33 | return nil, nil 34 | } 35 | 36 | resolve := func() { 37 | var output, modOutput string 38 | 39 | output, err = command.Run(dir, "go", "list", "-mod=readonly", "-m", "-json", "all") 40 | if err != nil { 41 | err = fmt.Errorf("failed to list modules: %v\n%s", err, output) 42 | return 43 | } 44 | 45 | // The reason we run this command separate is because we want the 46 | // information about this package specifically. Currently, it seems 47 | // that "go list all" will place the current modules information first 48 | // in the list, but we don't know that that is guaranteed. 49 | // 50 | // Because of that, we do a separate execution to guarantee we get only 51 | // this package information to use to determine the corresponding 52 | // goVersion. 53 | modOutput, err = command.Run(dir, "go", "list", "-mod=readonly", "-m", "-json") 54 | if err != nil { 55 | err = fmt.Errorf("failed to list module info: %v\n%s", err, output) 56 | return 57 | } 58 | 59 | dependencies, err = parseGoListOutput(output, modOutput, rootVersion) 60 | if err != nil { 61 | return 62 | } 63 | 64 | modules := make([]string, 0, len(dependencies)) 65 | for _, module := range dependencies { 66 | modules = append(modules, module.Name) 67 | } 68 | 69 | resolvedImportPaths := resolveImportPaths(rootModule, modules) 70 | mapImportPaths(dependencies, resolvedImportPaths) 71 | } 72 | 73 | output.WithProgress("Listing dependencies", resolve, outputOptions) 74 | return dependencies, err 75 | } 76 | 77 | // listProjectDependencies finds any packages from "$ go list all" that are NOT declared 78 | // as part of the current project. 79 | // 80 | // NOTE: This is different from the other dependencies stored in the indexer because it 81 | // does not modules, but packages. 82 | func ListProjectDependencies(projectRoot string) ([]string, error) { 83 | projectPackageOutput, err := command.Run(projectRoot, "go", "list", "./...") 84 | if err != nil { 85 | return nil, fmt.Errorf("failed to list project packages: %v\n%s", err, projectPackageOutput) 86 | } 87 | 88 | projectPackages := map[string]struct{}{} 89 | for _, pkg := range strings.Split(projectPackageOutput, "\n") { 90 | projectPackages[pkg] = struct{}{} 91 | } 92 | 93 | output, err := command.Run(projectRoot, "go", "list", "all") 94 | if err != nil { 95 | return nil, fmt.Errorf("failed to list dependency packages: %v\n%s", err, output) 96 | } 97 | 98 | dependencyPackages := []string{"std"} 99 | for _, dep := range strings.Split(output, "\n") { 100 | // It's a dependency if it's not in the projectPackages 101 | if _, ok := projectPackages[dep]; !ok { 102 | dependencyPackages = append(dependencyPackages, dep) 103 | } 104 | } 105 | 106 | return dependencyPackages, nil 107 | } 108 | 109 | type jsonModule struct { 110 | Name string `json:"Path"` 111 | Version string `json:"Version"` 112 | Replace *jsonModule `json:"Replace"` 113 | 114 | // The Golang version required for this module 115 | GoVersion string `json:"GoVersion"` 116 | } 117 | 118 | // parseGoListOutput parse the JSON output of `go list -m`. This method returns a map from 119 | // import paths to pairs of declared (unresolved) module names and version pairs that respect 120 | // replacement directives specified in go.mod. Replace directives indicating a local file path 121 | // will create a module with the given root version, which is expected to be the same version 122 | // as the module being indexed. 123 | func parseGoListOutput(output, modOutput, rootVersion string) (map[string]GoModule, error) { 124 | dependencies := map[string]GoModule{} 125 | decoder := json.NewDecoder(strings.NewReader(output)) 126 | 127 | for { 128 | var module jsonModule 129 | if err := decoder.Decode(&module); err != nil { 130 | if err == io.EOF { 131 | break 132 | } 133 | 134 | return nil, err 135 | } 136 | 137 | // Stash original name before applying replacement 138 | importPath := module.Name 139 | 140 | // If there's a replace directive, use that module instead 141 | if module.Replace != nil { 142 | module = *module.Replace 143 | } 144 | 145 | // Local file paths and root modules 146 | if module.Version == "" { 147 | module.Version = rootVersion 148 | } 149 | 150 | dependencies[importPath] = GoModule{ 151 | Name: module.Name, 152 | Version: cleanVersion(module.Version), 153 | } 154 | } 155 | 156 | var thisModule jsonModule 157 | if err := json.NewDecoder(strings.NewReader(modOutput)).Decode(&thisModule); err != nil { 158 | return nil, err 159 | } 160 | 161 | if thisModule.GoVersion == "" { 162 | return nil, errors.New("could not find GoVersion for current module") 163 | } 164 | 165 | setGolangDependency(dependencies, thisModule.GoVersion) 166 | 167 | return dependencies, nil 168 | } 169 | 170 | // The repository to find the source code for golang. 171 | var golangRepository = "github.com/golang/go" 172 | 173 | func setGolangDependency(dependencies map[string]GoModule, goVersion string) { 174 | dependencies[golangRepository] = GoModule{ 175 | Name: golangRepository, 176 | 177 | // The reason we prefix version with "go" is because in golang/go, all the release 178 | // tags are prefixed with "go". So turn "1.15" -> "go1.15" 179 | Version: fmt.Sprintf("go%s", goVersion), 180 | } 181 | } 182 | 183 | func GetGolangDependency(dependencies map[string]GoModule) GoModule { 184 | return dependencies[golangRepository] 185 | } 186 | 187 | // NormalizeMonikerPackage returns a normalized path to ensure that all 188 | // standard library paths are handled the same. Primarily to make sure 189 | // that both the golangRepository and "std/" paths are normalized. 190 | func NormalizeMonikerPackage(path string) string { 191 | // When indexing _within_ the golang/go repository, `std/` is prefixed 192 | // to packages. So we trim that here just to be sure that we keep 193 | // consistent names. 194 | normalizedPath := strings.TrimPrefix(path, "std/") 195 | 196 | if !isStandardlibPackge(normalizedPath) { 197 | return path 198 | } 199 | 200 | // Make sure we don't see double "std/" in the package for the moniker 201 | return fmt.Sprintf("%s/std/%s", golangRepository, normalizedPath) 202 | } 203 | 204 | // versionPattern matches a versioning ending in a 12-digit sha, e.g., vX.Y.Z.-yyyymmddhhmmss-abcdefabcdef 205 | var versionPattern = regexp.MustCompile(`^.*-([a-f0-9]{12})$`) 206 | 207 | // cleanVersion normalizes a module version string. 208 | func cleanVersion(version string) string { 209 | version = strings.TrimSpace(strings.TrimSuffix(version, "// indirect")) 210 | version = strings.TrimSpace(strings.TrimSuffix(version, "+incompatible")) 211 | 212 | if matches := versionPattern.FindStringSubmatch(version); len(matches) > 0 { 213 | return matches[1] 214 | } 215 | 216 | return version 217 | } 218 | 219 | // resolveImportPaths returns a map of import paths to resolved code host and path 220 | // suffix usable for moniker identifiers. The given root module is used to resolve 221 | // replace directives with local file paths and is expected to be a resolved import 222 | // path (a valid URL, including a scheme). 223 | func resolveImportPaths(rootModule string, modules []string) map[string]string { 224 | ch := make(chan string, len(modules)) 225 | for _, module := range modules { 226 | ch <- module 227 | } 228 | close(ch) 229 | 230 | var m sync.Mutex 231 | namesToResolve := map[string]string{} 232 | var wg sync.WaitGroup 233 | 234 | for i := 0; i < runtime.GOMAXPROCS(0); i++ { 235 | wg.Add(1) 236 | 237 | go func() { 238 | defer wg.Done() 239 | 240 | for name := range ch { 241 | // Stash original name before applying replacement 242 | originalName := name 243 | 244 | // Try to resolve the import path if it looks like a local path 245 | name, err := resolveLocalPath(name, rootModule) 246 | if err != nil { 247 | log.Println(fmt.Sprintf("WARNING: Failed to resolve local %s (%s).", name, err)) 248 | continue 249 | } 250 | 251 | // Determine path suffix relative to the import path 252 | resolved, ok := resolveRepoRootForImportPath(name) 253 | if !ok { 254 | continue 255 | } 256 | 257 | m.Lock() 258 | namesToResolve[originalName] = resolved 259 | m.Unlock() 260 | } 261 | }() 262 | } 263 | 264 | wg.Wait() 265 | return namesToResolve 266 | } 267 | 268 | // resolveRepoRootForImportPath will get the resolved name after handling vsc RepoRoots and any 269 | // necessary handling of the standard library 270 | func resolveRepoRootForImportPath(name string) (string, bool) { 271 | // When indexing golang/go, there are some references to the package "std" itself. 272 | // Generally, "std/" is not referenced directly (it is just assumed when you have "fmt" or similar 273 | // in your imports), but inside of golang/go, it is directly referenced. 274 | // 275 | // In that case, we just return it directly, there is no other resolving to do. 276 | if name == "std" { 277 | return name, true 278 | } 279 | 280 | repoRoot, err := vcs.RepoRootForImportPath(name, false) 281 | if err != nil { 282 | log.Println(fmt.Sprintf("WARNING: Failed to resolve repo %s (%s) %s.", name, err, repoRoot)) 283 | return "", false 284 | } 285 | 286 | suffix := strings.TrimPrefix(name, repoRoot.Root) 287 | return repoRoot.Repo + suffix, true 288 | } 289 | 290 | // resolveLocalPath converts the given name to an import path if it looks like a local path based on 291 | // the given root module. The root module, if non-empty, is expected to be a resolved import path 292 | // (a valid URL, including a scheme). If the name does not look like a local path, it will be returned 293 | // unchanged. 294 | func resolveLocalPath(name, rootModule string) (string, error) { 295 | if rootModule == "" || !strings.HasPrefix(name, ".") { 296 | return name, nil 297 | } 298 | 299 | parsedRootModule, err := url.Parse(rootModule) 300 | if err != nil { 301 | return "", err 302 | } 303 | 304 | // Join path relative to the root to the parsed module 305 | parsedRootModule.Path = path.Join(parsedRootModule.Path, name) 306 | 307 | // Remove scheme so it's resolvable again as an import path 308 | return strings.TrimPrefix(parsedRootModule.String(), parsedRootModule.Scheme+"://"), nil 309 | } 310 | 311 | // mapImportPaths replace each module name with the value in the given resolved import paths 312 | // map. If the module name is not present in the map, no change is made to the module value. 313 | func mapImportPaths(dependencies map[string]GoModule, resolvedImportPaths map[string]string) { 314 | for importPath, module := range dependencies { 315 | if name, ok := resolvedImportPaths[module.Name]; ok { 316 | dependencies[importPath] = GoModule{ 317 | Name: name, 318 | Version: module.Version, 319 | } 320 | } 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /internal/gomod/stdlib.go: -------------------------------------------------------------------------------- 1 | // THIS FILE IS GENERATED. SEE ./scripts/gen_stdlib_map.sh 2 | // Generated by: go version go1.18.2 darwin/amd64 3 | package gomod 4 | 5 | // isStandardlibPackge determines whether a package is in the standard library 6 | // or not. At this point, it checks whether the package name is one of those 7 | // that is found from running "go list std" in the latest released go version. 8 | func isStandardlibPackge(pkg string) bool { 9 | _, ok := standardLibraryMap[pkg] 10 | return ok 11 | } 12 | 13 | var contained = struct{}{} 14 | 15 | // This list is calculated from "go list std". 16 | var standardLibraryMap = map[string]interface{}{ 17 | "archive/tar": contained, 18 | "archive/zip": contained, 19 | "bufio": contained, 20 | "bytes": contained, 21 | "compress/bzip2": contained, 22 | "compress/flate": contained, 23 | "compress/gzip": contained, 24 | "compress/lzw": contained, 25 | "compress/zlib": contained, 26 | "container/heap": contained, 27 | "container/list": contained, 28 | "container/ring": contained, 29 | "context": contained, 30 | "crypto": contained, 31 | "crypto/aes": contained, 32 | "crypto/cipher": contained, 33 | "crypto/des": contained, 34 | "crypto/dsa": contained, 35 | "crypto/ecdsa": contained, 36 | "crypto/ed25519": contained, 37 | "crypto/ed25519/internal/edwards25519": contained, 38 | "crypto/ed25519/internal/edwards25519/field": contained, 39 | "crypto/elliptic": contained, 40 | "crypto/elliptic/internal/fiat": contained, 41 | "crypto/elliptic/internal/nistec": contained, 42 | "crypto/hmac": contained, 43 | "crypto/internal/randutil": contained, 44 | "crypto/internal/subtle": contained, 45 | "crypto/md5": contained, 46 | "crypto/rand": contained, 47 | "crypto/rc4": contained, 48 | "crypto/rsa": contained, 49 | "crypto/sha1": contained, 50 | "crypto/sha256": contained, 51 | "crypto/sha512": contained, 52 | "crypto/subtle": contained, 53 | "crypto/tls": contained, 54 | "crypto/x509": contained, 55 | "crypto/x509/internal/macos": contained, 56 | "crypto/x509/pkix": contained, 57 | "database/sql": contained, 58 | "database/sql/driver": contained, 59 | "debug/buildinfo": contained, 60 | "debug/dwarf": contained, 61 | "debug/elf": contained, 62 | "debug/gosym": contained, 63 | "debug/macho": contained, 64 | "debug/pe": contained, 65 | "debug/plan9obj": contained, 66 | "embed": contained, 67 | "embed/internal/embedtest": contained, 68 | "encoding": contained, 69 | "encoding/ascii85": contained, 70 | "encoding/asn1": contained, 71 | "encoding/base32": contained, 72 | "encoding/base64": contained, 73 | "encoding/binary": contained, 74 | "encoding/csv": contained, 75 | "encoding/gob": contained, 76 | "encoding/hex": contained, 77 | "encoding/json": contained, 78 | "encoding/pem": contained, 79 | "encoding/xml": contained, 80 | "errors": contained, 81 | "expvar": contained, 82 | "flag": contained, 83 | "fmt": contained, 84 | "go/ast": contained, 85 | "go/build": contained, 86 | "go/build/constraint": contained, 87 | "go/constant": contained, 88 | "go/doc": contained, 89 | "go/format": contained, 90 | "go/importer": contained, 91 | "go/internal/gccgoimporter": contained, 92 | "go/internal/gcimporter": contained, 93 | "go/internal/srcimporter": contained, 94 | "go/internal/typeparams": contained, 95 | "go/parser": contained, 96 | "go/printer": contained, 97 | "go/scanner": contained, 98 | "go/token": contained, 99 | "go/types": contained, 100 | "hash": contained, 101 | "hash/adler32": contained, 102 | "hash/crc32": contained, 103 | "hash/crc64": contained, 104 | "hash/fnv": contained, 105 | "hash/maphash": contained, 106 | "html": contained, 107 | "html/template": contained, 108 | "image": contained, 109 | "image/color": contained, 110 | "image/color/palette": contained, 111 | "image/draw": contained, 112 | "image/gif": contained, 113 | "image/internal/imageutil": contained, 114 | "image/jpeg": contained, 115 | "image/png": contained, 116 | "index/suffixarray": contained, 117 | "internal/abi": contained, 118 | "internal/buildcfg": contained, 119 | "internal/bytealg": contained, 120 | "internal/cfg": contained, 121 | "internal/cpu": contained, 122 | "internal/execabs": contained, 123 | "internal/fmtsort": contained, 124 | "internal/fuzz": contained, 125 | "internal/goarch": contained, 126 | "internal/godebug": contained, 127 | "internal/goexperiment": contained, 128 | "internal/goos": contained, 129 | "internal/goroot": contained, 130 | "internal/goversion": contained, 131 | "internal/intern": contained, 132 | "internal/itoa": contained, 133 | "internal/lazyregexp": contained, 134 | "internal/lazytemplate": contained, 135 | "internal/nettrace": contained, 136 | "internal/obscuretestdata": contained, 137 | "internal/oserror": contained, 138 | "internal/poll": contained, 139 | "internal/profile": contained, 140 | "internal/race": contained, 141 | "internal/reflectlite": contained, 142 | "internal/singleflight": contained, 143 | "internal/syscall/execenv": contained, 144 | "internal/syscall/unix": contained, 145 | "internal/sysinfo": contained, 146 | "internal/testenv": contained, 147 | "internal/testlog": contained, 148 | "internal/trace": contained, 149 | "internal/unsafeheader": contained, 150 | "internal/xcoff": contained, 151 | "io": contained, 152 | "io/fs": contained, 153 | "io/ioutil": contained, 154 | "log": contained, 155 | "log/syslog": contained, 156 | "math": contained, 157 | "math/big": contained, 158 | "math/bits": contained, 159 | "math/cmplx": contained, 160 | "math/rand": contained, 161 | "mime": contained, 162 | "mime/multipart": contained, 163 | "mime/quotedprintable": contained, 164 | "net": contained, 165 | "net/http": contained, 166 | "net/http/cgi": contained, 167 | "net/http/cookiejar": contained, 168 | "net/http/fcgi": contained, 169 | "net/http/httptest": contained, 170 | "net/http/httptrace": contained, 171 | "net/http/httputil": contained, 172 | "net/http/internal": contained, 173 | "net/http/internal/ascii": contained, 174 | "net/http/internal/testcert": contained, 175 | "net/http/pprof": contained, 176 | "net/internal/socktest": contained, 177 | "net/mail": contained, 178 | "net/netip": contained, 179 | "net/rpc": contained, 180 | "net/rpc/jsonrpc": contained, 181 | "net/smtp": contained, 182 | "net/textproto": contained, 183 | "net/url": contained, 184 | "os": contained, 185 | "os/exec": contained, 186 | "os/exec/internal/fdtest": contained, 187 | "os/signal": contained, 188 | "os/signal/internal/pty": contained, 189 | "os/user": contained, 190 | "path": contained, 191 | "path/filepath": contained, 192 | "plugin": contained, 193 | "reflect": contained, 194 | "reflect/internal/example1": contained, 195 | "reflect/internal/example2": contained, 196 | "regexp": contained, 197 | "regexp/syntax": contained, 198 | "runtime": contained, 199 | "runtime/cgo": contained, 200 | "runtime/debug": contained, 201 | "runtime/internal/atomic": contained, 202 | "runtime/internal/math": contained, 203 | "runtime/internal/sys": contained, 204 | "runtime/metrics": contained, 205 | "runtime/pprof": contained, 206 | "runtime/race": contained, 207 | "runtime/trace": contained, 208 | "sort": contained, 209 | "strconv": contained, 210 | "strings": contained, 211 | "sync": contained, 212 | "sync/atomic": contained, 213 | "syscall": contained, 214 | "testing": contained, 215 | "testing/fstest": contained, 216 | "testing/internal/testdeps": contained, 217 | "testing/iotest": contained, 218 | "testing/quick": contained, 219 | "text/scanner": contained, 220 | "text/tabwriter": contained, 221 | "text/template": contained, 222 | "text/template/parse": contained, 223 | "time": contained, 224 | "time/tzdata": contained, 225 | "unicode": contained, 226 | "unicode/utf16": contained, 227 | "unicode/utf8": contained, 228 | "unsafe": contained, 229 | "vendor/golang.org/x/crypto/chacha20": contained, 230 | "vendor/golang.org/x/crypto/chacha20poly1305": contained, 231 | "vendor/golang.org/x/crypto/cryptobyte": contained, 232 | "vendor/golang.org/x/crypto/cryptobyte/asn1": contained, 233 | "vendor/golang.org/x/crypto/curve25519": contained, 234 | "vendor/golang.org/x/crypto/curve25519/internal/field": contained, 235 | "vendor/golang.org/x/crypto/hkdf": contained, 236 | "vendor/golang.org/x/crypto/internal/poly1305": contained, 237 | "vendor/golang.org/x/crypto/internal/subtle": contained, 238 | "vendor/golang.org/x/net/dns/dnsmessage": contained, 239 | "vendor/golang.org/x/net/http/httpguts": contained, 240 | "vendor/golang.org/x/net/http/httpproxy": contained, 241 | "vendor/golang.org/x/net/http2/hpack": contained, 242 | "vendor/golang.org/x/net/idna": contained, 243 | "vendor/golang.org/x/net/nettest": contained, 244 | "vendor/golang.org/x/net/route": contained, 245 | "vendor/golang.org/x/sys/cpu": contained, 246 | "vendor/golang.org/x/text/secure/bidirule": contained, 247 | "vendor/golang.org/x/text/transform": contained, 248 | "vendor/golang.org/x/text/unicode/bidi": contained, 249 | "vendor/golang.org/x/text/unicode/norm": contained, 250 | } 251 | -------------------------------------------------------------------------------- /internal/indexer/implementation.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | "go/types" 5 | "runtime" 6 | "strings" 7 | 8 | "github.com/sourcegraph/lsif-go/internal/output" 9 | "golang.org/x/tools/container/intsets" 10 | "golang.org/x/tools/go/packages" 11 | ) 12 | 13 | type implDef struct { 14 | defInfo *DefinitionInfo 15 | identIsExported bool 16 | methods []string 17 | methodsByName map[string]methodInfo 18 | monikerPackage string 19 | monikerIdentifier string 20 | typeNameIsExported bool 21 | typeNameIsAlias bool 22 | } 23 | 24 | type methodInfo struct { 25 | definition *DefinitionInfo 26 | monikerIdentifier string 27 | } 28 | 29 | func (def implDef) Exported() bool { 30 | return def.typeNameIsExported || def.identIsExported 31 | } 32 | 33 | type implEdge struct { 34 | from int 35 | to int 36 | } 37 | 38 | type implRelation struct { 39 | edges []implEdge 40 | 41 | // concatenated list of (concreteTypes..., interfaces...) 42 | // this gives every type and interface a unique index. 43 | nodes []implDef 44 | 45 | // offset index for where interfaces start 46 | ifaceOffset int 47 | } 48 | 49 | func (rel implRelation) forEachImplementation(f func(from implDef, to []implDef)) { 50 | m := map[int][]implDef{} 51 | for _, e := range rel.edges { 52 | if _, ok := m[e.from]; !ok { 53 | m[e.from] = []implDef{} 54 | } 55 | m[e.from] = append(m[e.from], rel.nodes[e.to]) 56 | } 57 | 58 | for fromi, tos := range m { 59 | f(rel.nodes[fromi], tos) 60 | } 61 | } 62 | 63 | // invert reverses the links for edges for a given implRelation 64 | func (rel implRelation) invert() implRelation { 65 | inverse := implRelation{ 66 | edges: []implEdge{}, 67 | nodes: rel.nodes, 68 | ifaceOffset: rel.ifaceOffset, 69 | } 70 | 71 | for _, e := range rel.edges { 72 | inverse.edges = append(inverse.edges, implEdge{from: e.to, to: e.from}) 73 | } 74 | return inverse 75 | } 76 | 77 | // Translates a `concreteTypes` index into a `nodes` index 78 | func (rel implRelation) concreteTypeIxToNodeIx(idx int) int { 79 | // Concrete type nodes come first 80 | return idx 81 | } 82 | 83 | // Translates an `interfaces` index into a `nodes` index 84 | func (rel implRelation) interfaceIxToNodeIx(idx int) int { 85 | // Interface nodes come after the concrete types 86 | return rel.ifaceOffset + idx 87 | } 88 | 89 | func (rel *implRelation) linkInterfaceToReceivers(idx int, interfaceMethods []string, methodToReceivers map[string]*intsets.Sparse) { 90 | // Empty interface - skip it. 91 | if len(interfaceMethods) == 0 { 92 | return 93 | } 94 | 95 | // Find all the concrete types that implement this interface. 96 | // Types that implement this interface are the intersection 97 | // of all sets of receivers of all methods in this interface. 98 | candidateTypes := &intsets.Sparse{} 99 | 100 | // The rest of this function is effectively "fold" (for those CS PhDs out there). 101 | // 102 | // > I think the underlying logic here is really beautiful but the syntax 103 | // > makes it a bit messy and really obscures the intent and simplicity 104 | // > behind it 105 | // 106 | // - Dr. Fritz 107 | 108 | // If it doesn't match on the first method, then we can immediately quit. 109 | // Concrete types must _always_ implement all the methods 110 | if initialReceivers, ok := methodToReceivers[interfaceMethods[0]]; !ok { 111 | return 112 | } else { 113 | candidateTypes.Copy(initialReceivers) 114 | } 115 | 116 | // Loop over the rest of the methods and find all the types that intersect 117 | // every method of the interface. 118 | for _, method := range interfaceMethods[1:] { 119 | receivers, ok := methodToReceivers[method] 120 | if !ok { 121 | return 122 | } 123 | 124 | candidateTypes.IntersectionWith(receivers) 125 | if candidateTypes.IsEmpty() { 126 | return 127 | } 128 | } 129 | 130 | // Add the implementations to the relation. 131 | for _, ty := range candidateTypes.AppendTo(nil) { 132 | rel.edges = append(rel.edges, implEdge{rel.concreteTypeIxToNodeIx(ty), rel.interfaceIxToNodeIx(idx)}) 133 | } 134 | } 135 | 136 | // indexImplementations emits data for each implementation of an interface. 137 | // 138 | // NOTE: if indexImplementations becomes multi-threaded then we would need to update 139 | // Indexer.ensureImplementationMoniker to ensure that it uses appropriate locking. 140 | func (i *Indexer) indexImplementations() error { 141 | if !i.generationOptions.EnableImplementations { 142 | return nil 143 | } 144 | 145 | var implErr error 146 | 147 | output.WithProgress("Indexing implementations", func() { 148 | // When considering the connections we want to draw between the following four categories: 149 | // - LocalInterfaces: Interfaces created in the currently project 150 | // - LocalTypes: Concrete Types created in the currently project 151 | // 152 | // - RemoteTypes: Concrete Types created in one of the dependencies of the current project 153 | // - RemoteInterfaces: Interfaces created in one of the dependencies of the current project 154 | // 155 | // We want to connect the four categories like this: 156 | // 157 | // ```ascii_art 158 | // LocalInterfaces <------------------> LocalConcreteTypes 159 | // | | 160 | // | | 161 | // v v 162 | // RemoteConcreteTypes X RemoteInterfaces 163 | // ``` 164 | // 165 | // NOTES: 166 | // - We do not need to connect RemoteTypes and RemoteInterfaces because those connections will 167 | // be made when we index those projects. 168 | // - We do not connect Interfaces w/ Interfaces or Types w/ Types, so there is no need to make those 169 | // connectsion between the local and remote interfaces/types. 170 | 171 | // ========================= 172 | // Local Implementations 173 | localInterfaces, localConcreteTypes, err := i.extractInterfacesAndConcreteTypes([]string{"./..."}) 174 | if err != nil { 175 | implErr = err 176 | return 177 | } 178 | 179 | // LocalConcreteTypes -> LocalInterfaces 180 | localRelation := buildImplementationRelation(localConcreteTypes, localInterfaces) 181 | localRelation.forEachImplementation(i.emitLocalImplementation) 182 | 183 | // LocalInterfaces -> LocalConcreteTypes 184 | invertedLocalRelation := localRelation.invert() 185 | invertedLocalRelation.forEachImplementation(i.emitLocalImplementation) 186 | 187 | // ========================= 188 | // Remote Implementations 189 | remoteInterfaces, remoteConcreteTypes, err := i.extractInterfacesAndConcreteTypes(i.projectDependencies) 190 | if err != nil { 191 | implErr = err 192 | return 193 | } 194 | 195 | // LocalConcreteTypes -> RemoteInterfaces (exported only) 196 | localTypesToRemoteInterfaces := buildImplementationRelation(localConcreteTypes, filterToExported(remoteInterfaces)) 197 | localTypesToRemoteInterfaces.forEachImplementation(i.emitRemoteImplementation) 198 | 199 | // RemoteConcreteTypes (exported only) -> LocalInterfaces 200 | localInterfacesToRemoteTypes := buildImplementationRelation(filterToExported(remoteConcreteTypes), localInterfaces).invert() 201 | localInterfacesToRemoteTypes.forEachImplementation(i.emitRemoteImplementation) 202 | 203 | }, i.outputOptions) 204 | 205 | return implErr 206 | } 207 | 208 | // emitLocalImplementation correlates implementations for both structs/interfaces (refered to as typeDefs) and methods. 209 | func (i *Indexer) emitLocalImplementation(from implDef, tos []implDef) { 210 | typeDefDocToInVs := map[uint64][]uint64{} 211 | for _, to := range tos { 212 | if to.defInfo == nil { 213 | continue 214 | } 215 | 216 | documentID := to.defInfo.DocumentID 217 | 218 | if _, ok := typeDefDocToInVs[documentID]; !ok { 219 | typeDefDocToInVs[documentID] = []uint64{} 220 | } 221 | typeDefDocToInVs[documentID] = append(typeDefDocToInVs[documentID], to.defInfo.RangeID) 222 | } 223 | 224 | if from.defInfo != nil { 225 | // Emit implementation for the typeDefs directly 226 | i.emitLocalImplementationRelation(from.defInfo.ResultSetID, typeDefDocToInVs) 227 | } 228 | 229 | // Emit implementation for each of the methods on typeDefs 230 | for fromName, fromMethod := range from.methodsByName { 231 | methodDocToInvs := map[uint64][]uint64{} 232 | 233 | fromMethodDef := i.forEachMethodImplementation(tos, fromName, fromMethod, func(to implDef, _ *DefinitionInfo) { 234 | toMethod := to.methodsByName[fromName] 235 | 236 | // This method is from an embedded type defined in some dependency. 237 | if toMethod.definition == nil { 238 | return 239 | } 240 | 241 | toDocument := toMethod.definition.DocumentID 242 | if _, ok := methodDocToInvs[toDocument]; !ok { 243 | methodDocToInvs[toDocument] = []uint64{} 244 | } 245 | methodDocToInvs[toDocument] = append(methodDocToInvs[toDocument], toMethod.definition.RangeID) 246 | }) 247 | 248 | if fromMethodDef == nil { 249 | continue 250 | } 251 | 252 | i.emitLocalImplementationRelation(fromMethodDef.ResultSetID, methodDocToInvs) 253 | } 254 | } 255 | 256 | // emitLocalImplementationRelation emits the required LSIF nodes for an implementation 257 | func (i *Indexer) emitLocalImplementationRelation(defResultSetID uint64, documentToInVs map[uint64][]uint64) { 258 | implResultID := i.emitter.EmitImplementationResult() 259 | i.emitter.EmitTextDocumentImplementation(defResultSetID, implResultID) 260 | 261 | for documentID, inVs := range documentToInVs { 262 | i.emitter.EmitItem(implResultID, inVs, documentID) 263 | } 264 | } 265 | 266 | // emitRemoteImplementation emits implementation monikers 267 | // (kind: "implementation") to connect remote implementations 268 | func (i *Indexer) emitRemoteImplementation(from implDef, tos []implDef) { 269 | for _, to := range tos { 270 | if from.defInfo == nil { 271 | continue 272 | } 273 | i.emitImplementationMoniker(from.defInfo.ResultSetID, to.monikerPackage, to.monikerIdentifier) 274 | } 275 | 276 | for fromName, fromMethod := range from.methodsByName { 277 | i.forEachMethodImplementation(tos, fromName, fromMethod, func(to implDef, fromDef *DefinitionInfo) { 278 | toMethod := to.methodsByName[fromName] 279 | i.emitImplementationMoniker(fromDef.ResultSetID, to.monikerPackage, toMethod.monikerIdentifier) 280 | }) 281 | } 282 | } 283 | 284 | // forEachMethodImplementation will call callback for each to in tos when the 285 | // method is a method that is properly implemented. 286 | // 287 | // It returns the definition of the method that can be linked for each of the 288 | // associated tos 289 | func (i *Indexer) forEachMethodImplementation( 290 | tos []implDef, 291 | fromName string, 292 | fromMethod methodInfo, 293 | callback func(to implDef, fromDef *DefinitionInfo), 294 | ) *DefinitionInfo { 295 | // This method is from an embedded type defined in some dependency. 296 | if fromMethod.definition == nil { 297 | return nil 298 | } 299 | 300 | // if any of the `to` implementations do not have this method, 301 | // that means this method is NOT part of the required set of 302 | // methods to be considered an implementation. 303 | for _, to := range tos { 304 | if _, ok := to.methodsByName[fromName]; !ok { 305 | return fromMethod.definition 306 | } 307 | } 308 | 309 | for _, to := range tos { 310 | // Skip aliases because their methods are redundant with 311 | // the underlying concrete type's methods. 312 | if to.typeNameIsAlias { 313 | continue 314 | } 315 | 316 | callback(to, fromMethod.definition) 317 | } 318 | 319 | return fromMethod.definition 320 | } 321 | 322 | // extractInterfacesAndConcreteTypes constructs a list of interfaces and 323 | // concrete types from the list of given packages. 324 | func (i *Indexer) extractInterfacesAndConcreteTypes(pkgNames []string) (interfaces []implDef, concreteTypes []implDef, err error) { 325 | visit := func(pkg *packages.Package) { 326 | for ident, obj := range pkg.TypesInfo.Defs { 327 | if obj == nil { 328 | continue 329 | } 330 | 331 | // We ignore aliases 'type M = N' to avoid duplicate reporting 332 | // of the Named type N. 333 | typeName, ok := obj.(*types.TypeName) 334 | if !ok { 335 | continue 336 | } 337 | 338 | if _, ok := obj.Type().(*types.Named); !ok { 339 | continue 340 | } 341 | 342 | methods := listMethods(obj.Type().(*types.Named)) 343 | 344 | canonicalizedMethods := []string{} 345 | for _, m := range methods { 346 | canonicalizedMethods = append(canonicalizedMethods, canonicalize(m)) 347 | } 348 | 349 | // ignore interfaces that are empty. they are too 350 | // plentiful and don't provide useful intelligence. 351 | if len(methods) == 0 { 352 | continue 353 | } 354 | 355 | methodsByName := map[string]methodInfo{} 356 | for _, m := range methods { 357 | methodsByName[m.Obj().Name()] = methodInfo{ 358 | definition: i.getDefinitionInfo(m.Obj(), nil), 359 | monikerIdentifier: joinMonikerParts(makeMonikerPackage(m.Obj()), makeMonikerIdentifier(i.packageDataCache, pkg, m.Obj())), 360 | } 361 | } 362 | 363 | monikerPackage := makeMonikerPackage(obj) 364 | 365 | d := implDef{ 366 | monikerPackage: monikerPackage, 367 | monikerIdentifier: joinMonikerParts(monikerPackage, makeMonikerIdentifier(i.packageDataCache, pkg, obj)), 368 | typeNameIsExported: typeName.Exported(), 369 | typeNameIsAlias: typeName.IsAlias(), 370 | identIsExported: ident.IsExported(), 371 | defInfo: i.getDefinitionInfo(typeName, ident), 372 | methods: canonicalizedMethods, 373 | methodsByName: methodsByName, 374 | } 375 | if types.IsInterface(obj.Type()) { 376 | interfaces = append(interfaces, d) 377 | } else { 378 | concreteTypes = append(concreteTypes, d) 379 | } 380 | } 381 | } 382 | 383 | batch := func(pkgBatch []string) error { 384 | pkgs, err := i.loadPackage(true, pkgBatch...) 385 | if err != nil { 386 | return err 387 | } 388 | 389 | for _, pkg := range pkgs { 390 | visit(pkg) 391 | } 392 | return nil 393 | } 394 | 395 | pkgBatch := []string{} 396 | for ix, pkgName := range pkgNames { 397 | pkgBatch = append(pkgBatch, pkgName) 398 | 399 | if i.generationOptions.DepBatchSize != 0 && ix%i.generationOptions.DepBatchSize == 0 { 400 | err := batch(pkgBatch) 401 | runtime.GC() // Prevent a garbage pile 402 | if err != nil { 403 | return nil, nil, err 404 | } 405 | pkgBatch = pkgBatch[:0] 406 | } 407 | } 408 | if err := batch(pkgBatch); err != nil { 409 | return nil, nil, err 410 | } 411 | 412 | return interfaces, concreteTypes, nil 413 | } 414 | 415 | // buildImplementationRelation builds a map from concrete types to all the interfaces that they implement. 416 | func buildImplementationRelation(concreteTypes, interfaces []implDef) implRelation { 417 | rel := implRelation{ 418 | edges: []implEdge{}, 419 | nodes: append(concreteTypes, interfaces...), 420 | ifaceOffset: len(concreteTypes), 421 | } 422 | 423 | // Build a map from methods to all their receivers (concrete types that define those methods). 424 | methodToReceivers := map[string]*intsets.Sparse{} 425 | for idx, t := range concreteTypes { 426 | for _, method := range t.methods { 427 | if _, ok := methodToReceivers[method]; !ok { 428 | methodToReceivers[method] = &intsets.Sparse{} 429 | } 430 | methodToReceivers[method].Insert(idx) 431 | } 432 | } 433 | 434 | // Loop over all the interfaces and find the concrete types that implement them. 435 | for idx, interfase := range interfaces { 436 | rel.linkInterfaceToReceivers(idx, interfase.methods, methodToReceivers) 437 | } 438 | 439 | return rel 440 | } 441 | 442 | // listMethods returns the method set for a named type T 443 | // merged with all the methods of *T that have different names than 444 | // the methods of T. 445 | // 446 | // Copied from https://github.com/golang/tools/blob/1a7ca93429f83e087f7d44d35c0e9ea088fc722e/cmd/godex/print.go#L355 447 | func listMethods(T *types.Named) []*types.Selection { 448 | // method set for T 449 | mset := types.NewMethodSet(T) 450 | var res []*types.Selection 451 | for i, n := 0, mset.Len(); i < n; i++ { 452 | res = append(res, mset.At(i)) 453 | } 454 | 455 | // add all *T methods with names different from T methods 456 | pmset := types.NewMethodSet(types.NewPointer(T)) 457 | for i, n := 0, pmset.Len(); i < n; i++ { 458 | pm := pmset.At(i) 459 | if obj := pm.Obj(); mset.Lookup(obj.Pkg(), obj.Name()) == nil { 460 | res = append(res, pm) 461 | } 462 | } 463 | 464 | return res 465 | } 466 | 467 | // Returns a string representation of a method that can be used as a key for finding matches in interfaces. 468 | func canonicalize(m *types.Selection) string { 469 | builder := strings.Builder{} 470 | 471 | writeTuple := func(t *types.Tuple) { 472 | for i := 0; i < t.Len(); i++ { 473 | builder.WriteString(t.At(i).Type().String()) 474 | } 475 | } 476 | 477 | signature := m.Type().(*types.Signature) 478 | 479 | // if an object is not exported, then we need to make the canonical 480 | // representation of the object not able to match any other representations 481 | if !m.Obj().Exported() { 482 | builder.WriteString(pkgPath(m.Obj())) 483 | builder.WriteString(":") 484 | } 485 | 486 | builder.WriteString(m.Obj().Name()) 487 | builder.WriteString("(") 488 | writeTuple(signature.Params()) 489 | builder.WriteString(")") 490 | 491 | returnTypes := signature.Results() 492 | returnLen := returnTypes.Len() 493 | if returnLen == 0 { 494 | // Don't add anything 495 | } else if returnLen == 1 { 496 | builder.WriteString(" ") 497 | writeTuple(returnTypes) 498 | } else { 499 | builder.WriteString(" (") 500 | writeTuple(returnTypes) 501 | builder.WriteString(")") 502 | } 503 | 504 | // fmt.Println(builder.String()) 505 | return builder.String() 506 | } 507 | 508 | // filterToExported removes any nonExported types or identifiers from a list of []implDef 509 | // NOTE: defs is modified in place by this function. 510 | func filterToExported(defs []implDef) []implDef { 511 | // filter in place. 512 | filtered := defs[:0] 513 | 514 | for _, def := range defs { 515 | if def.Exported() { 516 | filtered = append(filtered, def) 517 | } 518 | } 519 | 520 | return filtered 521 | } 522 | --------------------------------------------------------------------------------