-
Notifications
You must be signed in to change notification settings - Fork 212
feat(langserver): find references and tests #3494
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
0e71541
57b77f3
ecb3ea6
18d846d
5581877
059a91b
580a4fa
f1f8ae4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,287 @@ | ||
| package lsp | ||
|
|
||
| import ( | ||
| "path/filepath" | ||
| "slices" | ||
| "strings" | ||
|
|
||
| "github.com/sourcegraph/go-lsp" | ||
|
|
||
| "github.com/thought-machine/please/src/core" | ||
| "github.com/thought-machine/please/src/parse/asp" | ||
| "github.com/thought-machine/please/src/query" | ||
| "github.com/thought-machine/please/tools/build_langserver/lsp/astutils" | ||
| ) | ||
|
|
||
| // references implements 'find all references' support. | ||
| func (h *Handler) references(params *lsp.ReferenceParams) ([]lsp.Location, error) { | ||
| doc := h.doc(params.TextDocument.URI) | ||
| ast := h.parseIfNeeded(doc) | ||
| f := doc.AspFile() | ||
| pos := aspPos(params.Position) | ||
|
|
||
| // Check if cursor is on a function definition (def funcname(...)) | ||
| var funcName string | ||
| asp.WalkAST(ast, func(stmt *asp.Statement) bool { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. General comment across this entire file: I'm concerned that we're calling asp.WalkAST too many times, making this rather inefficient. I would like to see us traverse the AST as few times as possible - this may involve adding a variant of |
||
| if stmt.FuncDef != nil { | ||
| stmtStart := f.Pos(stmt.Pos) | ||
| // Check if cursor is on the function name | ||
| nameEnd := stmtStart | ||
| nameEnd.Column += len("def ") + len(stmt.FuncDef.Name) | ||
| if asp.WithinRange(pos, stmtStart, nameEnd) { | ||
| funcName = stmt.FuncDef.Name | ||
| return false | ||
| } | ||
| } | ||
| return true | ||
| }) | ||
|
|
||
| // If we found a function definition, find all calls to it | ||
| if funcName != "" { | ||
| // Pass the current filename so we can find the labels for THIS definition | ||
| return h.findFunctionReferences(funcName, f.Name, params.Context.IncludeDeclaration) | ||
| } | ||
|
|
||
| // Check if cursor is on a function call (e.g., go_library(...)) | ||
| asp.WalkAST(ast, func(stmt *asp.Statement) bool { | ||
| if stmt.Ident != nil { | ||
| stmtStart := f.Pos(stmt.Pos) | ||
| nameEnd := stmtStart | ||
| nameEnd.Column += len(stmt.Ident.Name) | ||
| if asp.WithinRange(pos, stmtStart, nameEnd) { | ||
| funcName = stmt.Ident.Name | ||
| return false | ||
| } | ||
| } | ||
| return true | ||
| }) | ||
|
|
||
| if funcName != "" { | ||
| // For function calls, we don't have a specific definition file - use empty string | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's add a TODO here for making this better - @DuBento is potentially going to be adding more data to Asp that will help us understand where globals come from (in aid of making |
||
| return h.findFunctionReferences(funcName, "", params.Context.IncludeDeclaration) | ||
| } | ||
|
|
||
| // Otherwise, look for build label references | ||
| return h.findLabelReferences(doc, ast, f, pos, params.Context.IncludeDeclaration) | ||
| } | ||
|
|
||
| // findFunctionReferences finds all calls to a function across all BUILD files. | ||
| // sourceFile is the file where the definition we're searching from is located (empty if from a call site). | ||
| func (h *Handler) findFunctionReferences(funcName string, sourceFile string, includeDeclaration bool) ([]lsp.Location, error) { | ||
| locs := []lsp.Location{} | ||
|
|
||
| // Get the labels that can provide this function (a file may be exposed by multiple targets) | ||
| h.mutex.Lock() | ||
| builtins := h.builtins[funcName] | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we unlock here after we've read from the map? |
||
| var defLabels []core.BuildLabel | ||
| // If we know the source file, find the labels for THAT specific definition | ||
| // This prevents false positives when multiple files define functions with the same name | ||
| for _, b := range builtins { | ||
| if sourceFile != "" && pathsMatch(sourceFile, b.Pos.Filename, h.root) { | ||
| defLabels = b.Labels | ||
| break | ||
| } | ||
| } | ||
| // If no source file specified or not found, fall back to first definition's labels | ||
| if len(defLabels) == 0 && len(builtins) > 0 { | ||
| defLabels = builtins[0].Labels | ||
| } | ||
| h.mutex.Unlock() | ||
|
|
||
| // Search all packages for calls to this function | ||
| for _, pkg := range h.state.Graph.PackageMap() { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again, I'm wondering if we should have some caching here |
||
| // If we know which labels define this function, only search packages that subinclude at least one | ||
| if len(defLabels) > 0 { | ||
| allSubincludes := pkg.AllSubincludes(h.state.Graph) | ||
| hasSubinclude := false | ||
| for _, defLabel := range defLabels { | ||
| if slices.Contains(allSubincludes, defLabel) { | ||
| hasSubinclude = true | ||
| break | ||
| } | ||
| } | ||
| if !hasSubinclude { | ||
| continue | ||
| } | ||
| } | ||
|
|
||
| uri := lsp.DocumentURI("file://" + filepath.Join(h.root, pkg.Filename)) | ||
| refDoc, err := h.maybeOpenDoc(uri) | ||
| if err != nil { | ||
| continue | ||
| } | ||
| refAst := h.parseIfNeeded(refDoc) | ||
| refFile := refDoc.AspFile() | ||
|
|
||
| // Find all statement calls to the function (e.g., go_library(...)) | ||
| asp.WalkAST(refAst, func(stmt *asp.Statement) bool { | ||
| if stmt.Ident != nil && stmt.Ident.Name == funcName && stmt.Ident.Action != nil && stmt.Ident.Action.Call != nil { | ||
| start := refFile.Pos(stmt.Pos) | ||
| end := start | ||
| end.Column += len(funcName) | ||
| locs = append(locs, lsp.Location{ | ||
| URI: uri, | ||
| Range: lsp.Range{ | ||
| Start: lsp.Position{Line: start.Line - 1, Character: start.Column - 1}, | ||
| End: lsp.Position{Line: end.Line - 1, Character: end.Column - 1}, | ||
| }, | ||
| }) | ||
| } | ||
| return true | ||
| }) | ||
|
|
||
| // Find expression calls (e.g., x = go_library(...)) | ||
| asp.WalkAST(refAst, func(expr *asp.Expression) bool { | ||
| if expr.Val.Ident != nil && expr.Val.Ident.Name == funcName && len(expr.Val.Ident.Action) > 0 && expr.Val.Ident.Action[0].Call != nil { | ||
| start := refFile.Pos(expr.Pos) | ||
| end := start | ||
| end.Column += len(funcName) | ||
| locs = append(locs, lsp.Location{ | ||
| URI: uri, | ||
| Range: lsp.Range{ | ||
| Start: lsp.Position{Line: start.Line - 1, Character: start.Column - 1}, | ||
| End: lsp.Position{Line: end.Line - 1, Character: end.Column - 1}, | ||
| }, | ||
| }) | ||
| } | ||
| return true | ||
| }) | ||
| } | ||
|
|
||
| // Include the definition itself if requested | ||
| if includeDeclaration { | ||
| h.mutex.Lock() | ||
| if builtins, ok := h.builtins[funcName]; ok && len(builtins) > 0 { | ||
| b := builtins[0] | ||
| filename := b.Pos.Filename | ||
| if !filepath.IsAbs(filename) { | ||
| filename = filepath.Join(h.root, filename) | ||
| } | ||
| locs = append(locs, lsp.Location{ | ||
| URI: lsp.DocumentURI("file://" + filename), | ||
| Range: rng(b.Pos, b.EndPos), | ||
| }) | ||
| } | ||
| h.mutex.Unlock() | ||
| } | ||
|
|
||
| return locs, nil | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need to limit the total number of references we return? Find reference on |
||
| } | ||
|
|
||
| // findLabelReferences finds all references to a build label. | ||
| func (h *Handler) findLabelReferences(doc *doc, ast []*asp.Statement, f *asp.File, pos asp.FilePosition, includeDeclaration bool) ([]lsp.Location, error) { | ||
| var targetLabel core.BuildLabel | ||
| var targetName string | ||
|
|
||
| // Check if cursor is on a string (build label) | ||
| asp.WalkAST(ast, func(expr *asp.Expression) bool { | ||
| exprStart := f.Pos(expr.Pos) | ||
| exprEnd := f.Pos(expr.EndPos) | ||
| if !asp.WithinRange(pos, exprStart, exprEnd) { | ||
| return false | ||
| } | ||
| if expr.Val.String != "" { | ||
| label := astutils.TrimStrLit(expr.Val.String) | ||
| if l, err := core.TryParseBuildLabel(label, doc.PkgName, ""); err == nil { | ||
| targetLabel = l | ||
| } | ||
| return false | ||
| } | ||
| return true | ||
| }) | ||
|
|
||
| // Check if cursor is on a target definition (name = "...") | ||
| if targetLabel.IsEmpty() { | ||
| asp.WalkAST(ast, func(stmt *asp.Statement) bool { | ||
| if stmt.Ident != nil && stmt.Ident.Action != nil && stmt.Ident.Action.Call != nil { | ||
| stmtStart := f.Pos(stmt.Pos) | ||
| stmtEnd := f.Pos(stmt.EndPos) | ||
| if asp.WithinRange(pos, stmtStart, stmtEnd) { | ||
| if name := findName(stmt.Ident.Action.Call.Arguments); name != "" { | ||
| targetLabel = core.BuildLabel{PackageName: doc.PkgName, Name: name} | ||
| targetName = name | ||
| } | ||
| } | ||
| return false | ||
| } | ||
| return true | ||
| }) | ||
| } | ||
|
|
||
| if targetLabel.IsEmpty() { | ||
| return []lsp.Location{}, nil | ||
| } | ||
|
|
||
| // Verify the target exists in the graph before querying revdeps | ||
| // This prevents panics when the label has an invalid package name | ||
| if h.state.Graph.Package(targetLabel.PackageName, "") == nil { | ||
| log.Warning("references: package %q not found in build graph for label %s", targetLabel.PackageName, targetLabel) | ||
| return []lsp.Location{}, nil | ||
| } | ||
|
|
||
| // Use query.FindRevdeps to find all reverse dependencies | ||
| // Parameters: hidden=false, followSubincludes=true, includeSubrepos=true, depth=-1 (unlimited) | ||
| revdeps := query.FindRevdeps(h.state, core.BuildLabels{targetLabel}, false, true, true, -1) | ||
|
|
||
| locs := []lsp.Location{} | ||
|
|
||
| // For each reverse dependency, find the exact location of the reference in its BUILD file | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @DuBento does any of the work you're doing to tag build targets with the statement that generates them help us here? I think what you have here won't work when the dependency is "implicit" through use of a build def (not that we should let perfect be the enemy of good) |
||
| for target := range revdeps { | ||
| pkg := h.state.Graph.PackageByLabel(target.Label) | ||
| if pkg == nil { | ||
| continue | ||
| } | ||
|
|
||
| uri := lsp.DocumentURI("file://" + filepath.Join(h.root, pkg.Filename)) | ||
| refDoc, err := h.maybeOpenDoc(uri) | ||
| if err != nil { | ||
| continue | ||
| } | ||
| refAst := h.parseIfNeeded(refDoc) | ||
| refFile := refDoc.AspFile() | ||
|
|
||
| // Find all string literals that reference our target | ||
| labelStr := targetLabel.String() | ||
| shortLabelStr := ":" + targetLabel.Name // For same-package references | ||
|
|
||
| asp.WalkAST(refAst, func(expr *asp.Expression) bool { | ||
| if expr.Val.String != "" { | ||
| str := astutils.TrimStrLit(expr.Val.String) | ||
| // Check if this string matches our target label | ||
| if str == labelStr || (refDoc.PkgName == targetLabel.PackageName && str == shortLabelStr) { | ||
| // Also try parsing it as a label to handle relative references | ||
| if l, err := core.TryParseBuildLabel(str, refDoc.PkgName, ""); err == nil && l == targetLabel { | ||
| start := refFile.Pos(expr.Pos) | ||
| end := refFile.Pos(expr.EndPos) | ||
| locs = append(locs, lsp.Location{ | ||
| URI: uri, | ||
| Range: lsp.Range{ | ||
| Start: lsp.Position{Line: start.Line - 1, Character: start.Column - 1}, | ||
| End: lsp.Position{Line: end.Line - 1, Character: end.Column - 1}, | ||
| }, | ||
| }) | ||
| } | ||
| } | ||
| } | ||
| return true | ||
| }) | ||
| } | ||
|
|
||
| // Optionally include the definition itself if requested | ||
| if includeDeclaration && targetName != "" { | ||
| if defLoc := h.findLabel(doc.PkgName, targetLabel.String()); defLoc.URI != "" { | ||
| locs = append(locs, defLoc) | ||
| } | ||
| } | ||
|
|
||
| return locs, nil | ||
| } | ||
|
|
||
| // pathsMatch checks if two file paths refer to the same file, handling plz-out/gen paths. | ||
| func pathsMatch(path1, path2, root string) bool { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| // Strip plz-out/gen/ prefix if present | ||
| norm1 := strings.TrimPrefix(path1, "plz-out/gen/") | ||
| norm2 := strings.TrimPrefix(path2, "plz-out/gen/") | ||
|
|
||
| return norm1 == norm2 | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This feels like it's going to be expensive (iterating over every output of every target in the entire repository) - at a minimum we should be caching the result of this function, but we should consider precomputing the full map from build_defs files to the build_labels which output them so that we only have to do this loop once