blob: adfc659e20c4cc0c6d08e2e7a060c51d286dc177 [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 source
import (
"context"
"fmt"
"go/ast"
"go/token"
"go/types"
"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 := 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.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:
// Import path string literal?
if len(path) > 1 {
if imp, ok := path[1].(*ast.ImportSpec); ok {
highlight := func(n ast.Node) {
result[posRange{start: n.Pos(), end: n.End()}] = struct{}{}
}
// Highlight the import itself...
highlight(imp)
// ...and all references to it in the file.
if pkgname, ok := ImportedPkgName(info, imp); ok {
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:
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 highlightIdentifier(id *ast.Ident, file *ast.File, info *types.Info, result map[posRange]struct{}) {
highlight := func(n ast.Node) {
result[posRange{start: n.Pos(), end: n.End()}] = struct{}{}
}
// 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)
ast.Inspect(file, func(n ast.Node) bool {
switch n := n.(type) {
case *ast.Ident:
if n.Name == id.Name && info.ObjectOf(n) == obj {
highlight(n)
}
case *ast.ImportSpec:
pkgname, ok := ImportedPkgName(info, n)
if ok && pkgname == obj {
if n.Name != nil {
highlight(n.Name)
} else {
highlight(n)
}
}
}
return true
})
}
// ImportedPkgName returns the PkgName object declared by an ImportSpec.
// TODO(adonovan): make this a method of types.Info.
func ImportedPkgName(info *types.Info, imp *ast.ImportSpec) (*types.PkgName, bool) {
var obj types.Object
if imp.Name != nil {
obj = info.Defs[imp.Name]
} else {
obj = info.Implicits[imp]
}
pkgname, ok := obj.(*types.PkgName)
return pkgname, ok
}