1
0
mirror of https://github.com/golang/go synced 2024-10-01 04:18:33 -06:00
go/internal/lsp/source/highlight.go
Muir Manders 49b8ac185c internal/lsp/cache: add file contents to ParseGoHandle
Currently there is no need for this because the file contents are part
of the file handle. This change is in preparation for an impending
improvement that tweaks the source code during the parse stage to fix
certain kind of terminal parse errors. Any code that wants to use
an *ast.File or *token.File in conjunction with the file contents
needs access to the doctored source code so things line up.

Change-Id: I59d83d3d6150aa1264761aa2c1f6c1269075a2ce
Reviewed-on: https://go-review.googlesource.com/c/tools/+/218979
Run-TryBot: Muir Manders <muir@mnd.rs>
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
2020-02-13 05:05:14 +00:00

346 lines
10 KiB
Go

// 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/internal/lsp/protocol"
"golang.org/x/tools/internal/telemetry/log"
"golang.org/x/tools/internal/telemetry/trace"
errors "golang.org/x/xerrors"
)
func Highlight(ctx context.Context, snapshot Snapshot, fh FileHandle, pos protocol.Position) ([]protocol.Range, error) {
ctx, done := trace.StartSpan(ctx, "source.Highlight")
defer done()
pkg, pgh, err := getParsedFile(ctx, snapshot, fh, WidestPackageHandle)
if err != nil {
return nil, fmt.Errorf("getting file for Highlight: %v", err)
}
file, _, m, _, err := pgh.Parse(ctx)
if err != nil {
return nil, err
}
spn, err := m.PointSpan(pos)
if err != nil {
return nil, err
}
rng, err := spn.Range(m.Converter)
if err != nil {
return nil, err
}
path, _ := astutil.PathEnclosingInterval(file, rng.Start, rng.Start)
if len(path) == 0 {
return nil, errors.Errorf("no enclosing position found for %v:%v", int(pos.Line), int(pos.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(file, rng.Start-1, rng.Start-1); p != nil {
switch p[0].(type) {
case *ast.Ident, *ast.SelectorExpr:
path = p // use preceding ident/selector
}
}
}
switch path[0].(type) {
case *ast.BasicLit:
if len(path) > 1 {
if _, ok := path[1].(*ast.ImportSpec); ok {
return highlightImportUses(ctx, snapshot.View(), pkg, path)
}
}
return highlightFuncControlFlow(ctx, snapshot.View(), pkg, path)
case *ast.ReturnStmt, *ast.FuncDecl, *ast.FuncType:
return highlightFuncControlFlow(ctx, snapshot.View(), pkg, path)
case *ast.Ident:
return highlightIdentifiers(ctx, snapshot.View(), pkg, path)
case *ast.BranchStmt, *ast.ForStmt, *ast.RangeStmt:
return highlightLoopControlFlow(ctx, snapshot.View(), pkg, path)
}
// If the cursor is in an unidentified area, return empty results.
return nil, nil
}
func highlightFuncControlFlow(ctx context.Context, view View, pkg Package, path []ast.Node) ([]protocol.Range, error) {
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 nil, nil
case *ast.CallExpr:
// If cusor 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 nil, nil
}
}
}
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 nil, nil
}
// 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 nil, nil
}
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())
result := make(map[protocol.Range]bool)
// Highlight the correct argument in the function declaration return types.
if resultsList != nil && -1 < index && index < len(resultsList.List) {
rng, err := nodeToProtocolRange(view, pkg, resultsList.List[index])
if err != nil {
log.Error(ctx, "Error getting range for node", err)
} else {
result[rng] = true
}
}
// Add the "func" part of the func declaration.
if highlightAllReturnsAndFunc {
funcStmt, err := posToMappedRange(view, pkg, enclosingFunc.Pos(), enclosingFunc.Pos()+token.Pos(len("func")))
if err != nil {
return nil, err
}
rng, err := funcStmt.Range()
if err != nil {
return nil, err
}
result[rng] = true
}
// Traverse the AST to highlight the other relevant return statements in the function.
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
}
if n, ok := n.(*ast.ReturnStmt); ok {
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(n.Results) {
toAdd = n.Results[index]
}
if toAdd != nil {
rng, err := nodeToProtocolRange(view, pkg, toAdd)
if err != nil {
log.Error(ctx, "Error getting range for node", err)
} else {
result[rng] = true
}
return false
}
}
return true
})
return rangeMapToSlice(result), nil
}
func highlightLoopControlFlow(ctx context.Context, view View, pkg Package, path []ast.Node) ([]protocol.Range, error) {
var loop ast.Node
Outer:
// Reverse walk the path till we get to the for loop.
for _, n := range path {
switch n.(type) {
case *ast.ForStmt, *ast.RangeStmt:
loop = n
break Outer
}
}
// Cursor is not in a for loop.
if loop == nil {
return nil, nil
}
result := make(map[protocol.Range]bool)
// Add the for statement.
forStmt, err := posToMappedRange(view, pkg, loop.Pos(), loop.Pos()+token.Pos(len("for")))
if err != nil {
return nil, err
}
rng, err := forStmt.Range()
if err != nil {
return nil, err
}
result[rng] = true
ast.Inspect(loop, func(n ast.Node) bool {
// Don't traverse any other for loops.
switch n.(type) {
case *ast.ForStmt, *ast.RangeStmt:
return loop == n
}
// Add all branch statements in same scope as the identified one.
if n, ok := n.(*ast.BranchStmt); ok {
rng, err := nodeToProtocolRange(view, pkg, n)
if err != nil {
log.Error(ctx, "Error getting range for node", err)
return false
}
result[rng] = true
}
return true
})
return rangeMapToSlice(result), nil
}
func highlightImportUses(ctx context.Context, view View, pkg Package, path []ast.Node) ([]protocol.Range, error) {
result := make(map[protocol.Range]bool)
basicLit, ok := path[0].(*ast.BasicLit)
if !ok {
return nil, errors.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 {
if rng, err := nodeToProtocolRange(view, pkg, node); err == nil {
result[rng] = true
return false
}
}
n, ok := node.(*ast.Ident)
if !ok {
return true
}
obj, ok := pkg.GetTypesInfo().ObjectOf(n).(*types.PkgName)
if !ok {
return true
}
if !strings.Contains(basicLit.Value, obj.Name()) {
return true
}
if rng, err := nodeToProtocolRange(view, pkg, n); err == nil {
result[rng] = true
} else {
log.Error(ctx, "Error getting range for node", err)
}
return false
})
return rangeMapToSlice(result), nil
}
func highlightIdentifiers(ctx context.Context, view View, pkg Package, path []ast.Node) ([]protocol.Range, error) {
result := make(map[protocol.Range]bool)
id, ok := path[0].(*ast.Ident)
if !ok {
return nil, errors.Errorf("highlightIdentifiers called with an ast.Node of type %T", id)
}
// Check if ident is inside return or func decl.
if toAdd, err := highlightFuncControlFlow(ctx, view, pkg, path); toAdd != nil && err == nil {
for _, r := range toAdd {
result[r] = true
}
}
// TODO: maybe check if ident is a reserved word, if true then don't continue and return results.
idObj := pkg.GetTypesInfo().ObjectOf(id)
pkgObj, isImported := idObj.(*types.PkgName)
ast.Inspect(path[len(path)-1], func(node ast.Node) bool {
if imp, ok := node.(*ast.ImportSpec); ok && isImported {
if rng, err := highlightImport(view, pkg, pkgObj, imp); rng != nil && err == nil {
result[*rng] = true
}
}
n, ok := node.(*ast.Ident)
if !ok {
return true
}
if n.Name != id.Name {
return false
}
if nObj := pkg.GetTypesInfo().ObjectOf(n); nObj != idObj {
return false
}
if rng, err := nodeToProtocolRange(view, pkg, n); err == nil {
result[rng] = true
} else {
log.Error(ctx, "Error getting range for node", err)
}
return false
})
return rangeMapToSlice(result), nil
}
func highlightImport(view View, pkg Package, obj *types.PkgName, imp *ast.ImportSpec) (*protocol.Range, error) {
if imp.Name != nil || imp.Path == nil {
return nil, nil
}
if !strings.Contains(imp.Path.Value, obj.Name()) {
return nil, nil
}
rng, err := nodeToProtocolRange(view, pkg, imp.Path)
if err != nil {
return nil, err
}
return &rng, nil
}
func rangeMapToSlice(rangeMap map[protocol.Range]bool) []protocol.Range {
var list []protocol.Range
for i := range rangeMap {
list = append(list, i)
}
return list
}