1
0
mirror of https://github.com/golang/go synced 2024-11-18 22:24:50 -07:00
go/internal/lsp/source/highlight.go

502 lines
13 KiB
Go
Raw Normal View History

// 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, WidestPackage)
if err != nil {
return nil, fmt.Errorf("getting file for Highlight: %w", err)
}
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-21 13:15:06 -06:00
spn, err := pgf.Mapper.PointSpan(pos)
if err != nil {
return nil, err
}
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-21 13:15:06 -06:00
rng, err := spn.Range(pgf.Mapper.Converter)
if err != nil {
return nil, err
}
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-21 13:15:06 -06:00
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 {
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-21 13:15:06 -06:00
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, 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{}{}
}