Skip to content
This repository has been archived by the owner on Sep 26, 2023. It is now read-only.

Add support for find implementations #199

Merged
merged 66 commits into from
Nov 4, 2021
Merged

Add support for find implementations #199

merged 66 commits into from
Nov 4, 2021

Conversation

chrismwendt
Copy link
Contributor

@chrismwendt chrismwendt commented Sep 9, 2021

This makes lsif-go emit interface/type implementation data necessary to support find-implementations.

  • List all the interfaces and concrete types for the stdlib and current project (but not other dependencies - we keep hitting an error "missing go.sum entry")
  • Emit a textDocument/implementation for each pair (interface, concrete type) that matches
  • Emit "interface monikers" (new term) for cross-repo find implementations
  • Also list other dependencies
  • Emit package information
  • Emit textDocumentation/implementations for methods + cross repo

Benchmarks

Indexing sourcegraph/sourcegraph:

  • lsif-go@master: took 20s wall 45s user 336MB dump
  • lsif-go@implementations: took 29s wall 69s user 339MB dump (8s to load deps, 0.5s to index implementations)

Indexing aws/aws-sdk-go:

  • lsif-go@master: took 38s wall 106s user 2.347GB dump
  • lsif-go@implementations: took 45s wall 123s user 2.386GB dump (1.5s to load deps, 2.7s to index implementations)

Conclusion: ~25% slower, ~1% bigger dumps

name := dep.Name
name = strings.TrimPrefix(name, "https://")
name = strings.TrimPrefix(name, "https:/")
pkgs, err := packages.Load(config, name)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol @tjdevries you were right

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

of course, but about what? 😆 We saw this while we were testing stuff out today and figured we had to do something like this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I gave you crap about us only calling package.Load once and future proofing with that cache 😃


start1 := time.Now()
localInterfaces, localConcreteTypes := extractTypes(i.packages)
remoteInterfaces, remoteConcreteTypes := extractTypes(deps)
Copy link
Contributor

@tjdevries tjdevries Sep 21, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'd still be somewhat interested in us seeing if we can calculate the implementations on a per-dependency basis (and then parallelize that via go funcs) and see if we get any noticeable performance improvements on large repos.

Comment on lines 960 to 958
if shouldInvert {
relation = invert(relation)
leftDefs, rightDefs = rightDefs, leftDefs
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have been wondering a bit about whether we should teach the backend that this language is reciprocal or whether we should actually emit both ways.

For example, if we say "struct X implements interface Y", is it always true that "interface Y is implemented by X"?

If so, then we do not need to emit everything for both directions. We can simply have:

X --textDocument/implement --> implementationResult --item--> Y

And the backend must handle the face that the reverse relationship is true as well.

cc @efritz so we can chat about this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The relationship should be symmetric (imo, just by intuition). I was chatting with @chrismwendt about this and I think it might have some tendrils in the fact that we need to emit relationships for both:

  • interfaces defined in a package and a conforming struct defined in a dependency, and
  • structs defined in a package and a descriptive interface of that struct defined in a dependency

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are defs/refs symmetric, too? (I think so) If so, should indexers only emit one direction and the backend fill in the other? If not, there are probably some practical or principled reason(s) why that can help us decide whether or not implementations should follow the same model.

This question applies to implementation relationships within an LSIF dump, but not across dumps because cross-dump find-implementations uses monikers rather than textDocument/implementation et al.

if err != nil {
errs <- errors.Wrap(err, "packages.Load")
return
load := func(cache map[string][]*packages.Package, patterns []string) ([]*packages.Package, bool) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
load := func(cache map[string][]*packages.Package, patterns []string) ([]*packages.Package, bool) {
load := func(cache map[string][]*packages.Package, patterns ...string) ([]*packages.Package, bool) {

this would be way cooler 😎

internal/indexer/indexer.go Outdated Show resolved Hide resolved

// listProjectDependencies finds any packages from "$ go list all" that are NOT declared
// as part of the current project.
func (i *Indexer) listProjectDependencies() ([]string, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple comments related to this method (but not exactly about this method):

  • this might be able to go away if dependencies fits your use-case here (it should)
  • this should be called pre-indexing if it stays because all shell commands should happen in main; indexer shouldn't have to know much about the OS

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did anything here change regarding the dependencies thing from Chris's other comment?

I can move this to the pre-indexing stage regardless

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah so it looks like we shouldn't use -m for these in particular, but it would be cool to have this moved into the gomod package and called from main and passed to the indexer instead.

// universeParent := toMethod.Obj().(*types.Func).Type().(*types.Signature).Recv().Type().String()

pkg := obj.Pkg()
if pkg == nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments here would be real neat :)

errs <- errors.Wrap(err, "failed to list dependencies")
return
}
i.depPackages, hasError = load(cachedDepPackages, deps)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this implicitly done by loading ./...?


// filterToExported removes any nonExported types or identifiers from a list of []implDef
func filterToExported(defs []implDef) []implDef {
filtered := []implDef{}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
filtered := []implDef{}
filtered := defs[:0]

If this can be done in place:

localTypesToRemoteInterfaces := i.buildImplementationRelation(localConcreteTypes, filterToExported(remoteInterfaces))
localTypesToRemoteInterfaces.forEachImplementation(i.emitRemoteImplementation)

localInterfacesToRemoteTypes := invert(i.buildImplementationRelation(filterToExported(remoteConcreteTypes), localInterfaces))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can each of these blocks have a short comment?

Comment on lines 291 to 312
// Find all the concrete types that implement this interface.
// Types that implement this interface are the intersection
// of all sets of receivers of all methods in this interface.
candidateTypes := &intsets.Sparse{}

if initialReceivers, ok := methodToReceivers[canonical(methods[0])]; !ok {
continue
} else {
candidateTypes.Copy(initialReceivers)
}

for _, method := range methods[1:] {
receivers, ok := methodToReceivers[canonical(method)]
if !ok {
continue interfaceLoop
}

candidateTypes.IntersectionWith(receivers)
if candidateTypes.IsEmpty() {
continue interfaceLoop
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wish there was a cleaner way to write this as an obvious fold. I think the underlying logic here is really beautiful but the syntax makes it a bit messy and really obscures the intent and simplicity behind it 😞

@tjdevries
Copy link
Contributor

tjdevries commented Nov 2, 2021

image

image

So this is one thing I'm not sure about -- putting here to talk about w/ @efritz and/or @chrismwendt at some point.

@tjdevries
Copy link
Contributor

This PR creates the issue: #208

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants