1
0
mirror of https://github.com/golang/go synced 2024-11-18 18:34:40 -07:00
go/internal/lsp/source/highlight.go
Heschi Kreinick a9439ae9c1 internal/lsp: replace ParseGoHandle with concrete data
ParseGoHandles serve two purposes: they pin cache entries so that
redundant calculations are cached, and they allow users to obtain the
actual parsed AST. The former is an implementation detail, and the
latter turns out to just be an annoyance.

Parsed Go files are obtained from two places. By far the most common is
from a type checked package. But a type checked package must by
definition have already parsed all the files it contains, so the PGH
is already computed and cannot have failed. Type checked packages can
simply return the parsed file without requiring a separate Check
operation. We do want to pin the cache entries in this case, which I've
done by holding on to the PGH in cache.pkg.

There are some cases where we directly parse a file, such as for the
FoldingRange LSP call, which doesn't need type information. Those parses
can actually fail, so we do need an error check. But we don't need the
PGH; in all cases we are immediately using and discarding it.

So it turns out we don't actually need the PGH type at all, at least not
in the public API. Instead, we can pass around a concrete struct that
has the various pieces of data directly available.

This uncovered a bug in typeCheck: it should fail if it encounters any
real errors.

Change-Id: I203bf2dd79d5d65c01392d69c2cf4f7744fde7fc
Reviewed-on: https://go-review.googlesource.com/c/tools/+/244021
Run-TryBot: Heschi Kreinick <heschi@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
2020-07-28 17:35:11 +00:00

502 lines
13 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/event"
"golang.org/x/tools/internal/lsp/protocol"
errors "golang.org/x/xerrors"
)
func Highlight(ctx context.Context, snapshot Snapshot, fh FileHandle, pos protocol.Position) ([]protocol.Range, error) {
ctx, done := event.Start(ctx, "source.Highlight")
defer done()
pkg, pgf, err := getParsedFile(ctx, snapshot, fh, WidestPackageHandle)
if err != nil {
return nil, fmt.Errorf("getting file for Highlight: %w", err)
}
spn, err := pgf.Mapper.PointSpan(pos)
if err != nil {
return nil, err
}
rng, err := spn.Range(pgf.Mapper.Converter)
if err != nil {
return nil, err
}
path, _ := astutil.PathEnclosingInterval(pgf.File, rng.Start, rng.Start)
if len(path) == 0 {
return nil, fmt.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(pgf.File, rng.Start-1, rng.Start-1); p != nil {
switch p[0].(type) {
case *ast.Ident, *ast.SelectorExpr:
path = p // use preceding ident/selector
}
}
}
result, err := highlightPath(pkg, path)
if err != nil {
return nil, err
}
var ranges []protocol.Range
for rng := range result {
mRng, err := posToMappedRange(snapshot.View(), pkg, rng.start, rng.end)
if err != nil {
return nil, err
}
pRng, err := mRng.Range()
if err != nil {
return nil, err
}
ranges = append(ranges, pRng)
}
return ranges, nil
}
func highlightPath(pkg Package, path []ast.Node) (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(pkg, path, result)
return result, err
}
}
highlightFuncControlFlow(path, result)
case *ast.ReturnStmt, *ast.FuncDecl, *ast.FuncType:
highlightFuncControlFlow(path, result)
case *ast.Ident:
highlightIdentifiers(pkg, path, result)
case *ast.ForStmt, *ast.RangeStmt:
highlightLoopControlFlow(path, result)
case *ast.SwitchStmt:
highlightSwitchFlow(path, 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.
if node.Label != nil {
highlightLabeledFlow(node, result)
} else {
switch node.Tok {
case token.BREAK:
highlightUnlabeledBreakFlow(path, result)
case token.CONTINUE:
highlightLoopControlFlow(path, 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 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
}
}
}
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
})
}
func highlightUnlabeledBreakFlow(path []ast.Node, 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, result)
return // only highlight the innermost statement
case *ast.SwitchStmt:
highlightSwitchFlow(path, result)
return
case *ast.SelectStmt:
// TODO: add highlight when breaking a select.
return
}
}
}
func highlightLabeledFlow(node *ast.BranchStmt, result map[posRange]struct{}) {
obj := node.Label.Obj
if obj == nil || obj.Decl == nil {
return
}
label, ok := obj.Decl.(*ast.LabeledStmt)
if !ok {
return
}
switch label.Stmt.(type) {
case *ast.ForStmt, *ast.RangeStmt:
highlightLoopControlFlow([]ast.Node{label.Stmt, label}, result)
case *ast.SwitchStmt:
highlightSwitchFlow([]ast.Node{label.Stmt, label}, result)
}
}
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, 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 || labelDecl(b.Label) == 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
}
// Statment with labels that matches the loop.
if b.Label != nil && labelDecl(b.Label) == loopLabel {
result[posRange{start: b.Pos(), end: b.End()}] = struct{}{}
}
return true
})
}
func highlightSwitchFlow(path []ast.Node, 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 || labelDecl(b.Label) == 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 && labelDecl(b.Label) == switchNodeLabel {
result[posRange{start: b.Pos(), end: b.End()}] = struct{}{}
}
return true
})
}
func labelDecl(n *ast.Ident) *ast.Ident {
if n == nil {
return nil
}
if n.Obj == nil {
return nil
}
if n.Obj.Decl == nil {
return nil
}
stmt, ok := n.Obj.Decl.(*ast.LabeledStmt)
if !ok {
return nil
}
return stmt.Label
}
func highlightImportUses(pkg Package, path []ast.Node, result map[posRange]struct{}) error {
basicLit, ok := path[0].(*ast.BasicLit)
if !ok {
return 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 {
result[posRange{start: node.Pos(), end: node.End()}] = struct{}{}
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
}
result[posRange{start: n.Pos(), end: n.End()}] = struct{}{}
return false
})
return nil
}
func highlightIdentifiers(pkg Package, path []ast.Node, result map[posRange]struct{}) error {
id, ok := path[0].(*ast.Ident)
if !ok {
return errors.Errorf("highlightIdentifiers called with an ast.Node of type %T", id)
}
// Check if ident is inside return or func decl.
highlightFuncControlFlow(path, result)
// 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 {
highlightImport(pkgObj, imp, result)
}
n, ok := node.(*ast.Ident)
if !ok {
return true
}
if n.Name != id.Name {
return false
}
if nObj := pkg.GetTypesInfo().ObjectOf(n); nObj == idObj {
result[posRange{start: n.Pos(), end: n.End()}] = struct{}{}
}
return false
})
return nil
}
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{}{}
}