blob: f53e73f3053e4175bf5d3e39387774339bb12b4a [file] [log] [blame]
// 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 golang
import (
"context"
"fmt"
"go/ast"
"go/token"
"go/types"
"golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/file"
"golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/internal/event"
)
func Highlight(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, position protocol.Position) ([]protocol.DocumentHighlight, error) {
ctx, done := event.Start(ctx, "golang.Highlight")
defer done()
// We always want fully parsed files for highlight, regardless
// of whether the file belongs to a workspace package.
pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI())
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.TypesInfo())
if err != nil {
return nil, err
}
var ranges []protocol.DocumentHighlight
for rng, kind := range result {
rng, err := pgf.PosRange(rng.start, rng.end)
if err != nil {
return nil, err
}
ranges = append(ranges, protocol.DocumentHighlight{
Range: rng,
Kind: kind,
})
}
return ranges, nil
}
// highlightPath returns ranges to highlight for the given enclosing path,
// which should be the result of astutil.PathEnclosingInterval.
func highlightPath(path []ast.Node, file *ast.File, info *types.Info) (map[posRange]protocol.DocumentHighlightKind, error) {
result := make(map[posRange]protocol.DocumentHighlightKind)
switch node := path[0].(type) {
case *ast.BasicLit:
// Import path string literal?
if len(path) > 1 {
if imp, ok := path[1].(*ast.ImportSpec); ok {
highlight := func(n ast.Node) {
highlightNode(result, n, protocol.Text)
}
// Highlight the import itself...
highlight(imp)
// ...and all references to it in the file.
if pkgname := info.PkgNameOf(imp); pkgname != nil {
ast.Inspect(file, func(n ast.Node) bool {
if id, ok := n.(*ast.Ident); ok &&
info.Uses[id] == pkgname {
highlight(id)
}
return true
})
}
return result, nil
}
}
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, *ast.TypeSwitchStmt:
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)
}
}
}
return result, nil
}
type posRange struct {
start, end token.Pos
}
// highlightFuncControlFlow adds highlight ranges to the result map to
// associate results and result parameters.
//
// Specifically, if the cursor is in a result or result parameter, all
// results and result parameters with the same index are highlighted. If the
// cursor is in a 'func' or 'return' keyword, the func keyword as well as all
// returns from that func are highlighted.
//
// As a special case, if the cursor is within a complicated expression, control
// flow highlighting is disabled, as it would highlight too much.
func highlightFuncControlFlow(path []ast.Node, result map[posRange]protocol.DocumentHighlightKind) {
var (
funcType *ast.FuncType // type of enclosing func, or nil
funcBody *ast.BlockStmt // body of enclosing func, or nil
returnStmt *ast.ReturnStmt // enclosing ReturnStmt within the func, or nil
)
findEnclosingFunc:
for i, n := range path {
switch n := n.(type) {
// TODO(rfindley, low priority): these pre-existing cases for KeyValueExpr
// and CallExpr appear to avoid highlighting when the cursor is in a
// complicated expression. However, the basis for this heuristic is
// unclear. Can we formalize a rationale?
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 n.Args {
if arg == path[i-1] {
return
}
}
}
case *ast.FuncLit:
funcType = n.Type
funcBody = n.Body
break findEnclosingFunc
case *ast.FuncDecl:
funcType = n.Type
funcBody = n.Body
break findEnclosingFunc
case *ast.ReturnStmt:
returnStmt = n
}
}
if funcType == nil {
return // cursor is not in a function
}
// Helper functions for inspecting the current location.
var (
pos = path[0].Pos()
inSpan = func(start, end token.Pos) bool { return start <= pos && pos < end }
inNode = func(n ast.Node) bool { return inSpan(n.Pos(), n.End()) }
)
inResults := funcType.Results != nil && inNode(funcType.Results)
// If the cursor is on a "return" or "func" keyword, but not highlighting any
// specific field or expression, we should highlight all of the exit points
// of the function, including the "return" and "func" keywords.
funcEnd := funcType.Func + token.Pos(len("func"))
highlightAll := path[0] == returnStmt || inSpan(funcType.Func, funcEnd)
var highlightIndexes map[int]bool
if highlightAll {
// Add the "func" part of the func declaration.
highlightRange(result, funcType.Func, funcEnd, protocol.Text)
} else if returnStmt == nil && !inResults {
return // nothing to highlight
} else {
// If we're not highighting the entire return statement, we need to collect
// specific result indexes to highlight. This may be more than one index if
// the cursor is on a multi-name result field, but not in any specific name.
if !highlightAll {
highlightIndexes = make(map[int]bool)
if returnStmt != nil {
for i, n := range returnStmt.Results {
if inNode(n) {
highlightIndexes[i] = true
break
}
}
}
if funcType.Results != nil {
// Scan fields, either adding highlights according to the highlightIndexes
// computed above, or accounting for the cursor position within the result
// list.
// (We do both at once to avoid repeating the cumbersome field traversal.)
i := 0
findField:
for _, field := range funcType.Results.List {
for j, name := range field.Names {
if inNode(name) || highlightIndexes[i+j] {
highlightNode(result, name, protocol.Text)
highlightIndexes[i+j] = true
break findField // found/highlighted the specific name
}
}
// If the cursor is in a field but not in a name (e.g. in the space, or
// the type), highlight the whole field.
//
// Note that this may not be ideal if we're at e.g.
//
// (x,‸y int, z int8)
//
// ...where it would make more sense to highlight only y. But we don't
// reach this function if not in a func, return, ident, or basiclit.
if inNode(field) || highlightIndexes[i] {
highlightNode(result, field, protocol.Text)
highlightIndexes[i] = true
if inNode(field) {
for j := range field.Names {
highlightIndexes[i+j] = true
}
}
break findField // found/highlighted the field
}
n := len(field.Names)
if n == 0 {
n = 1
}
i += n
}
}
}
}
if funcBody != nil {
ast.Inspect(funcBody, func(n ast.Node) bool {
switch n := n.(type) {
case *ast.FuncDecl, *ast.FuncLit:
// Don't traverse into any functions other than enclosingFunc.
return false
case *ast.ReturnStmt:
if highlightAll {
// Add the entire return statement.
highlightNode(result, n, protocol.Text)
} else {
// Add the highlighted indexes.
for i, expr := range n.Results {
if highlightIndexes[i] {
highlightNode(result, expr, protocol.Text)
}
}
}
return false
}
return true
})
}
}
// highlightUnlabeledBreakFlow highlights the innermost enclosing for/range/switch or swlect
func highlightUnlabeledBreakFlow(path []ast.Node, info *types.Info, result map[posRange]protocol.DocumentHighlightKind) {
// 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, *ast.TypeSwitchStmt:
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]protocol.DocumentHighlightKind) {
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, *ast.TypeSwitchStmt:
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]protocol.DocumentHighlightKind) {
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.
rngStart := loop.Pos()
rngEnd := loop.Pos() + token.Pos(len("for"))
highlightRange(result, rngStart, rngEnd, protocol.Text)
// 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.TypeSwitchStmt, *ast.SelectStmt:
return false
}
b, ok := n.(*ast.BranchStmt)
if !ok {
return true
}
if b.Label == nil || info.Uses[b.Label] == info.Defs[loopLabel] {
highlightNode(result, b, protocol.Text)
}
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 {
highlightNode(result, n, protocol.Text)
}
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] {
highlightNode(result, b, protocol.Text)
}
return true
})
}
func highlightSwitchFlow(path []ast.Node, info *types.Info, result map[posRange]protocol.DocumentHighlightKind) {
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, *ast.TypeSwitchStmt:
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.
rngStart := switchNode.Pos()
rngEnd := switchNode.Pos() + token.Pos(len("switch"))
highlightRange(result, rngStart, rngEnd, protocol.Text)
// Traverse AST to find break statements within the same switch.
ast.Inspect(switchNode, func(n ast.Node) bool {
switch n.(type) {
case *ast.SwitchStmt, *ast.TypeSwitchStmt:
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] {
highlightNode(result, b, protocol.Text)
}
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] {
highlightNode(result, b, protocol.Text)
}
return true
})
}
func highlightNode(result map[posRange]protocol.DocumentHighlightKind, n ast.Node, kind protocol.DocumentHighlightKind) {
highlightRange(result, n.Pos(), n.End(), kind)
}
func highlightRange(result map[posRange]protocol.DocumentHighlightKind, pos, end token.Pos, kind protocol.DocumentHighlightKind) {
rng := posRange{pos, end}
// Order of traversal is important: some nodes (e.g. identifiers) are
// visited more than once, but the kind set during the first visitation "wins".
if _, exists := result[rng]; !exists {
result[rng] = kind
}
}
func highlightIdentifier(id *ast.Ident, file *ast.File, info *types.Info, result map[posRange]protocol.DocumentHighlightKind) {
// obj may be nil if the Ident is undefined.
// In this case, the behavior expected by tests is
// to match other undefined Idents of the same name.
obj := info.ObjectOf(id)
highlightIdent := func(n *ast.Ident, kind protocol.DocumentHighlightKind) {
if n.Name == id.Name && info.ObjectOf(n) == obj {
highlightNode(result, n, kind)
}
}
// highlightWriteInExpr is called for expressions that are
// logically on the left side of an assignment.
// We follow the behavior of VSCode+Rust and GoLand, which differs
// slightly from types.TypeAndValue.Assignable:
// *ptr = 1 // ptr write
// *ptr.field = 1 // ptr read, field write
// s.field = 1 // s read, field write
// array[i] = 1 // array read
var highlightWriteInExpr func(expr ast.Expr)
highlightWriteInExpr = func(expr ast.Expr) {
switch expr := expr.(type) {
case *ast.Ident:
highlightIdent(expr, protocol.Write)
case *ast.SelectorExpr:
highlightIdent(expr.Sel, protocol.Write)
case *ast.StarExpr:
highlightWriteInExpr(expr.X)
case *ast.ParenExpr:
highlightWriteInExpr(expr.X)
}
}
ast.Inspect(file, func(n ast.Node) bool {
switch n := n.(type) {
case *ast.AssignStmt:
for _, s := range n.Lhs {
highlightWriteInExpr(s)
}
case *ast.GenDecl:
if n.Tok == token.CONST || n.Tok == token.VAR {
for _, spec := range n.Specs {
if spec, ok := spec.(*ast.ValueSpec); ok {
for _, ele := range spec.Names {
highlightWriteInExpr(ele)
}
}
}
}
case *ast.IncDecStmt:
highlightWriteInExpr(n.X)
case *ast.SendStmt:
highlightWriteInExpr(n.Chan)
case *ast.CompositeLit:
t := info.TypeOf(n)
// Every expression should have a type;
// work around https://github.com/golang/go/issues/69092.
if t == nil {
t = types.Typ[types.Invalid]
}
if ptr, ok := t.Underlying().(*types.Pointer); ok {
t = ptr.Elem()
}
if _, ok := t.Underlying().(*types.Struct); ok {
for _, expr := range n.Elts {
if expr, ok := (expr).(*ast.KeyValueExpr); ok {
highlightWriteInExpr(expr.Key)
}
}
}
case *ast.RangeStmt:
highlightWriteInExpr(n.Key)
highlightWriteInExpr(n.Value)
case *ast.Field:
for _, name := range n.Names {
highlightIdent(name, protocol.Text)
}
case *ast.Ident:
// This case is reached for all Idents,
// including those also visited by highlightWriteInExpr.
if is[*types.Var](info.ObjectOf(n)) {
highlightIdent(n, protocol.Read)
} else {
// kind of idents in PkgName, etc. is Text
highlightIdent(n, protocol.Text)
}
case *ast.ImportSpec:
pkgname := info.PkgNameOf(n)
if pkgname == obj {
if n.Name != nil {
highlightNode(result, n.Name, protocol.Text)
} else {
highlightNode(result, n, protocol.Text)
}
}
}
return true
})
}