aboutsummaryrefslogtreecommitdiff
path: root/gopls/internal/lsp/source/highlight.go
diff options
context:
space:
mode:
Diffstat (limited to 'gopls/internal/lsp/source/highlight.go')
-rw-r--r--gopls/internal/lsp/source/highlight.go484
1 files changed, 484 insertions, 0 deletions
diff --git a/gopls/internal/lsp/source/highlight.go b/gopls/internal/lsp/source/highlight.go
new file mode 100644
index 000000000..a190f4896
--- /dev/null
+++ b/gopls/internal/lsp/source/highlight.go
@@ -0,0 +1,484 @@
+// Copyright 2019 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package source
+
+import (
+ "context"
+ "fmt"
+ "go/ast"
+ "go/token"
+ "go/types"
+ "strings"
+
+ "golang.org/x/tools/go/ast/astutil"
+ "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/internal/event"
+)
+
+func Highlight(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) ([]protocol.Range, error) {
+ ctx, done := event.Start(ctx, "source.Highlight")
+ defer done()
+
+ // We always want fully parsed files for highlight, regardless
+ // of whether the file belongs to a workspace package.
+ pkg, pgf, err := PackageForFile(ctx, snapshot, fh.URI(), NarrowestPackage)
+ if err != nil {
+ return nil, fmt.Errorf("getting package for Highlight: %w", err)
+ }
+
+ pos, err := pgf.PositionPos(position)
+ if err != nil {
+ return nil, err
+ }
+ path, _ := astutil.PathEnclosingInterval(pgf.File, pos, pos)
+ if len(path) == 0 {
+ return nil, fmt.Errorf("no enclosing position found for %v:%v", position.Line, position.Character)
+ }
+ // If start == end for astutil.PathEnclosingInterval, the 1-char interval
+ // following start is used instead. As a result, we might not get an exact
+ // match so we should check the 1-char interval to the left of the passed
+ // in position to see if that is an exact match.
+ if _, ok := path[0].(*ast.Ident); !ok {
+ if p, _ := astutil.PathEnclosingInterval(pgf.File, pos-1, pos-1); p != nil {
+ switch p[0].(type) {
+ case *ast.Ident, *ast.SelectorExpr:
+ path = p // use preceding ident/selector
+ }
+ }
+ }
+ result, err := highlightPath(path, pgf.File, pkg.GetTypesInfo())
+ if err != nil {
+ return nil, err
+ }
+ var ranges []protocol.Range
+ for rng := range result {
+ rng, err := pgf.PosRange(rng.start, rng.end)
+ if err != nil {
+ return nil, err
+ }
+ ranges = append(ranges, rng)
+ }
+ return ranges, nil
+}
+
+func highlightPath(path []ast.Node, file *ast.File, info *types.Info) (map[posRange]struct{}, error) {
+ result := make(map[posRange]struct{})
+ switch node := path[0].(type) {
+ case *ast.BasicLit:
+ if len(path) > 1 {
+ if _, ok := path[1].(*ast.ImportSpec); ok {
+ err := highlightImportUses(path, info, result)
+ return result, err
+ }
+ }
+ highlightFuncControlFlow(path, result)
+ case *ast.ReturnStmt, *ast.FuncDecl, *ast.FuncType:
+ highlightFuncControlFlow(path, result)
+ case *ast.Ident:
+ // Check if ident is inside return or func decl.
+ highlightFuncControlFlow(path, result)
+ highlightIdentifier(node, file, info, result)
+ case *ast.ForStmt, *ast.RangeStmt:
+ highlightLoopControlFlow(path, info, result)
+ case *ast.SwitchStmt:
+ highlightSwitchFlow(path, info, result)
+ case *ast.BranchStmt:
+ // BREAK can exit a loop, switch or select, while CONTINUE exit a loop so
+ // these need to be handled separately. They can also be embedded in any
+ // other loop/switch/select if they have a label. TODO: add support for
+ // GOTO and FALLTHROUGH as well.
+ switch node.Tok {
+ case token.BREAK:
+ if node.Label != nil {
+ highlightLabeledFlow(path, info, node, result)
+ } else {
+ highlightUnlabeledBreakFlow(path, info, result)
+ }
+ case token.CONTINUE:
+ if node.Label != nil {
+ highlightLabeledFlow(path, info, node, result)
+ } else {
+ highlightLoopControlFlow(path, info, result)
+ }
+ }
+ default:
+ // If the cursor is in an unidentified area, return empty results.
+ return nil, nil
+ }
+ return result, nil
+}
+
+type posRange struct {
+ start, end token.Pos
+}
+
+func highlightFuncControlFlow(path []ast.Node, result map[posRange]struct{}) {
+ var enclosingFunc ast.Node
+ var returnStmt *ast.ReturnStmt
+ var resultsList *ast.FieldList
+ inReturnList := false
+
+Outer:
+ // Reverse walk the path till we get to the func block.
+ for i, n := range path {
+ switch node := n.(type) {
+ case *ast.KeyValueExpr:
+ // If cursor is in a key: value expr, we don't want control flow highlighting
+ return
+ case *ast.CallExpr:
+ // If cursor is an arg in a callExpr, we don't want control flow highlighting.
+ if i > 0 {
+ for _, arg := range node.Args {
+ if arg == path[i-1] {
+ return
+ }
+ }
+ }
+ case *ast.Field:
+ inReturnList = true
+ case *ast.FuncLit:
+ enclosingFunc = n
+ resultsList = node.Type.Results
+ break Outer
+ case *ast.FuncDecl:
+ enclosingFunc = n
+ resultsList = node.Type.Results
+ break Outer
+ case *ast.ReturnStmt:
+ returnStmt = node
+ // If the cursor is not directly in a *ast.ReturnStmt, then
+ // we need to know if it is within one of the values that is being returned.
+ inReturnList = inReturnList || path[0] != returnStmt
+ }
+ }
+ // Cursor is not in a function.
+ if enclosingFunc == nil {
+ return
+ }
+ // If the cursor is on a "return" or "func" keyword, we should highlight all of the exit
+ // points of the function, including the "return" and "func" keywords.
+ highlightAllReturnsAndFunc := path[0] == returnStmt || path[0] == enclosingFunc
+ switch path[0].(type) {
+ case *ast.Ident, *ast.BasicLit:
+ // Cursor is in an identifier and not in a return statement or in the results list.
+ if returnStmt == nil && !inReturnList {
+ return
+ }
+ case *ast.FuncType:
+ highlightAllReturnsAndFunc = true
+ }
+ // The user's cursor may be within the return statement of a function,
+ // or within the result section of a function's signature.
+ // index := -1
+ var nodes []ast.Node
+ if returnStmt != nil {
+ for _, n := range returnStmt.Results {
+ nodes = append(nodes, n)
+ }
+ } else if resultsList != nil {
+ for _, n := range resultsList.List {
+ nodes = append(nodes, n)
+ }
+ }
+ _, index := nodeAtPos(nodes, path[0].Pos())
+
+ // Highlight the correct argument in the function declaration return types.
+ if resultsList != nil && -1 < index && index < len(resultsList.List) {
+ rng := posRange{
+ start: resultsList.List[index].Pos(),
+ end: resultsList.List[index].End(),
+ }
+ result[rng] = struct{}{}
+ }
+ // Add the "func" part of the func declaration.
+ if highlightAllReturnsAndFunc {
+ r := posRange{
+ start: enclosingFunc.Pos(),
+ end: enclosingFunc.Pos() + token.Pos(len("func")),
+ }
+ result[r] = struct{}{}
+ }
+ ast.Inspect(enclosingFunc, func(n ast.Node) bool {
+ // Don't traverse any other functions.
+ switch n.(type) {
+ case *ast.FuncDecl, *ast.FuncLit:
+ return enclosingFunc == n
+ }
+ ret, ok := n.(*ast.ReturnStmt)
+ if !ok {
+ return true
+ }
+ var toAdd ast.Node
+ // Add the entire return statement, applies when highlight the word "return" or "func".
+ if highlightAllReturnsAndFunc {
+ toAdd = n
+ }
+ // Add the relevant field within the entire return statement.
+ if -1 < index && index < len(ret.Results) {
+ toAdd = ret.Results[index]
+ }
+ if toAdd != nil {
+ result[posRange{start: toAdd.Pos(), end: toAdd.End()}] = struct{}{}
+ }
+ return false
+ })
+}
+
+// highlightUnlabeledBreakFlow highlights the innermost enclosing for/range/switch or swlect
+func highlightUnlabeledBreakFlow(path []ast.Node, info *types.Info, result map[posRange]struct{}) {
+ // Reverse walk the path until we find closest loop, select, or switch.
+ for _, n := range path {
+ switch n.(type) {
+ case *ast.ForStmt, *ast.RangeStmt:
+ highlightLoopControlFlow(path, info, result)
+ return // only highlight the innermost statement
+ case *ast.SwitchStmt:
+ highlightSwitchFlow(path, info, result)
+ return
+ case *ast.SelectStmt:
+ // TODO: add highlight when breaking a select.
+ return
+ }
+ }
+}
+
+// highlightLabeledFlow highlights the enclosing labeled for, range,
+// or switch statement denoted by a labeled break or continue stmt.
+func highlightLabeledFlow(path []ast.Node, info *types.Info, stmt *ast.BranchStmt, result map[posRange]struct{}) {
+ use := info.Uses[stmt.Label]
+ if use == nil {
+ return
+ }
+ for _, n := range path {
+ if label, ok := n.(*ast.LabeledStmt); ok && info.Defs[label.Label] == use {
+ switch label.Stmt.(type) {
+ case *ast.ForStmt, *ast.RangeStmt:
+ highlightLoopControlFlow([]ast.Node{label.Stmt, label}, info, result)
+ case *ast.SwitchStmt:
+ highlightSwitchFlow([]ast.Node{label.Stmt, label}, info, result)
+ }
+ return
+ }
+ }
+}
+
+func labelFor(path []ast.Node) *ast.Ident {
+ if len(path) > 1 {
+ if n, ok := path[1].(*ast.LabeledStmt); ok {
+ return n.Label
+ }
+ }
+ return nil
+}
+
+func highlightLoopControlFlow(path []ast.Node, info *types.Info, result map[posRange]struct{}) {
+ var loop ast.Node
+ var loopLabel *ast.Ident
+ stmtLabel := labelFor(path)
+Outer:
+ // Reverse walk the path till we get to the for loop.
+ for i := range path {
+ switch n := path[i].(type) {
+ case *ast.ForStmt, *ast.RangeStmt:
+ loopLabel = labelFor(path[i:])
+
+ if stmtLabel == nil || loopLabel == stmtLabel {
+ loop = n
+ break Outer
+ }
+ }
+ }
+ if loop == nil {
+ return
+ }
+
+ // Add the for statement.
+ rng := posRange{
+ start: loop.Pos(),
+ end: loop.Pos() + token.Pos(len("for")),
+ }
+ result[rng] = struct{}{}
+
+ // Traverse AST to find branch statements within the same for-loop.
+ ast.Inspect(loop, func(n ast.Node) bool {
+ switch n.(type) {
+ case *ast.ForStmt, *ast.RangeStmt:
+ return loop == n
+ case *ast.SwitchStmt, *ast.SelectStmt:
+ return false
+ }
+ b, ok := n.(*ast.BranchStmt)
+ if !ok {
+ return true
+ }
+ if b.Label == nil || info.Uses[b.Label] == info.Defs[loopLabel] {
+ result[posRange{start: b.Pos(), end: b.End()}] = struct{}{}
+ }
+ return true
+ })
+
+ // Find continue statements in the same loop or switches/selects.
+ ast.Inspect(loop, func(n ast.Node) bool {
+ switch n.(type) {
+ case *ast.ForStmt, *ast.RangeStmt:
+ return loop == n
+ }
+
+ if n, ok := n.(*ast.BranchStmt); ok && n.Tok == token.CONTINUE {
+ result[posRange{start: n.Pos(), end: n.End()}] = struct{}{}
+ }
+ return true
+ })
+
+ // We don't need to check other for loops if we aren't looking for labeled statements.
+ if loopLabel == nil {
+ return
+ }
+
+ // Find labeled branch statements in any loop.
+ ast.Inspect(loop, func(n ast.Node) bool {
+ b, ok := n.(*ast.BranchStmt)
+ if !ok {
+ return true
+ }
+ // statement with labels that matches the loop
+ if b.Label != nil && info.Uses[b.Label] == info.Defs[loopLabel] {
+ result[posRange{start: b.Pos(), end: b.End()}] = struct{}{}
+ }
+ return true
+ })
+}
+
+func highlightSwitchFlow(path []ast.Node, info *types.Info, result map[posRange]struct{}) {
+ var switchNode ast.Node
+ var switchNodeLabel *ast.Ident
+ stmtLabel := labelFor(path)
+Outer:
+ // Reverse walk the path till we get to the switch statement.
+ for i := range path {
+ switch n := path[i].(type) {
+ case *ast.SwitchStmt:
+ switchNodeLabel = labelFor(path[i:])
+ if stmtLabel == nil || switchNodeLabel == stmtLabel {
+ switchNode = n
+ break Outer
+ }
+ }
+ }
+ // Cursor is not in a switch statement
+ if switchNode == nil {
+ return
+ }
+
+ // Add the switch statement.
+ rng := posRange{
+ start: switchNode.Pos(),
+ end: switchNode.Pos() + token.Pos(len("switch")),
+ }
+ result[rng] = struct{}{}
+
+ // Traverse AST to find break statements within the same switch.
+ ast.Inspect(switchNode, func(n ast.Node) bool {
+ switch n.(type) {
+ case *ast.SwitchStmt:
+ return switchNode == n
+ case *ast.ForStmt, *ast.RangeStmt, *ast.SelectStmt:
+ return false
+ }
+
+ b, ok := n.(*ast.BranchStmt)
+ if !ok || b.Tok != token.BREAK {
+ return true
+ }
+
+ if b.Label == nil || info.Uses[b.Label] == info.Defs[switchNodeLabel] {
+ result[posRange{start: b.Pos(), end: b.End()}] = struct{}{}
+ }
+ return true
+ })
+
+ // We don't need to check other switches if we aren't looking for labeled statements.
+ if switchNodeLabel == nil {
+ return
+ }
+
+ // Find labeled break statements in any switch
+ ast.Inspect(switchNode, func(n ast.Node) bool {
+ b, ok := n.(*ast.BranchStmt)
+ if !ok || b.Tok != token.BREAK {
+ return true
+ }
+
+ if b.Label != nil && info.Uses[b.Label] == info.Defs[switchNodeLabel] {
+ result[posRange{start: b.Pos(), end: b.End()}] = struct{}{}
+ }
+
+ return true
+ })
+}
+
+func highlightImportUses(path []ast.Node, info *types.Info, result map[posRange]struct{}) error {
+ basicLit, ok := path[0].(*ast.BasicLit)
+ if !ok {
+ return fmt.Errorf("highlightImportUses called with an ast.Node of type %T", basicLit)
+ }
+ ast.Inspect(path[len(path)-1], func(node ast.Node) bool {
+ if imp, ok := node.(*ast.ImportSpec); ok && imp.Path == basicLit {
+ result[posRange{start: node.Pos(), end: node.End()}] = struct{}{}
+ return false
+ }
+ n, ok := node.(*ast.Ident)
+ if !ok {
+ return true
+ }
+ obj, ok := info.ObjectOf(n).(*types.PkgName)
+ if !ok {
+ return true
+ }
+ if !strings.Contains(basicLit.Value, obj.Name()) {
+ return true
+ }
+ result[posRange{start: n.Pos(), end: n.End()}] = struct{}{}
+ return false
+ })
+ return nil
+}
+
+func highlightIdentifier(id *ast.Ident, file *ast.File, info *types.Info, result map[posRange]struct{}) {
+ // TODO(rfindley): idObj may be nil. Note that returning early in this case
+ // causes tests to fail (because the nObj == idObj check below was succeeded
+ // for nil == nil!)
+ //
+ // Revisit this. If ObjectOf is nil, there are type errors, and it seems
+ // reasonable for identifier highlighting not to work.
+ idObj := info.ObjectOf(id)
+ pkgObj, isImported := idObj.(*types.PkgName)
+ ast.Inspect(file, func(node ast.Node) bool {
+ if imp, ok := node.(*ast.ImportSpec); ok && isImported {
+ highlightImport(pkgObj, imp, result)
+ }
+ n, ok := node.(*ast.Ident)
+ if !ok {
+ return true
+ }
+ if n.Name != id.Name {
+ return false
+ }
+ if nObj := info.ObjectOf(n); nObj == idObj {
+ result[posRange{start: n.Pos(), end: n.End()}] = struct{}{}
+ }
+ return false
+ })
+}
+
+func highlightImport(obj *types.PkgName, imp *ast.ImportSpec, result map[posRange]struct{}) {
+ if imp.Name != nil || imp.Path == nil {
+ return
+ }
+ if !strings.Contains(imp.Path.Value, obj.Name()) {
+ return
+ }
+ result[posRange{start: imp.Path.Pos(), end: imp.Path.End()}] = struct{}{}
+}