github.com/jd-ly/tools@v0.5.7/internal/lsp/source/completion/statements.go (about)

     1  // Copyright 2020 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package completion
     6  
     7  import (
     8  	"fmt"
     9  	"go/ast"
    10  	"go/token"
    11  	"go/types"
    12  
    13  	"github.com/jd-ly/tools/internal/lsp/protocol"
    14  	"github.com/jd-ly/tools/internal/lsp/snippet"
    15  	"github.com/jd-ly/tools/internal/lsp/source"
    16  )
    17  
    18  // addStatementCandidates adds full statement completion candidates
    19  // appropriate for the current context.
    20  func (c *completer) addStatementCandidates() {
    21  	c.addErrCheckAndReturn()
    22  	c.addAssignAppend()
    23  }
    24  
    25  // addAssignAppend offers a completion candidate of the form:
    26  //
    27  //     someSlice = append(someSlice, )
    28  //
    29  // It will offer the "append" completion in two situations:
    30  //
    31  // 1. Position is in RHS of assign, prefix matches "append", and
    32  //    corresponding LHS object is a slice. For example,
    33  //    "foo = ap<>" completes to "foo = append(foo, )".
    34  //
    35  // Or
    36  //
    37  // 2. Prefix is an ident or selector in an *ast.ExprStmt (i.e.
    38  //    beginning of statement), and our best matching candidate is a
    39  //    slice. For example: "foo.ba" completes to "foo.bar = append(foo.bar, )".
    40  func (c *completer) addAssignAppend() {
    41  	if len(c.path) < 3 {
    42  		return
    43  	}
    44  
    45  	ident, _ := c.path[0].(*ast.Ident)
    46  	if ident == nil {
    47  		return
    48  	}
    49  
    50  	var (
    51  		// sliceText is the full name of our slice object, e.g. "s.abc" in
    52  		// "s.abc = app<>".
    53  		sliceText string
    54  		// needsLHS is true if we need to prepend the LHS slice name and
    55  		// "=" to our candidate.
    56  		needsLHS = false
    57  		fset     = c.snapshot.FileSet()
    58  	)
    59  
    60  	switch n := c.path[1].(type) {
    61  	case *ast.AssignStmt:
    62  		// We are already in an assignment. Make sure our prefix matches "append".
    63  		if c.matcher.Score("append") <= 0 {
    64  			return
    65  		}
    66  
    67  		exprIdx := exprAtPos(c.pos, n.Rhs)
    68  		if exprIdx == len(n.Rhs) || exprIdx > len(n.Lhs)-1 {
    69  			return
    70  		}
    71  
    72  		lhsType := c.pkg.GetTypesInfo().TypeOf(n.Lhs[exprIdx])
    73  		if lhsType == nil {
    74  			return
    75  		}
    76  
    77  		// Make sure our corresponding LHS object is a slice.
    78  		if _, isSlice := lhsType.Underlying().(*types.Slice); !isSlice {
    79  			return
    80  		}
    81  
    82  		// The name or our slice is whatever's in the LHS expression.
    83  		sliceText = source.FormatNode(fset, n.Lhs[exprIdx])
    84  	case *ast.SelectorExpr:
    85  		// Make sure we are a selector at the beginning of a statement.
    86  		if _, parentIsExprtStmt := c.path[2].(*ast.ExprStmt); !parentIsExprtStmt {
    87  			return
    88  		}
    89  
    90  		// So far we only know the first part of our slice name. For
    91  		// example in "s.a<>" we only know our slice begins with "s."
    92  		// since the user could still be typing.
    93  		sliceText = source.FormatNode(fset, n.X) + "."
    94  		needsLHS = true
    95  	case *ast.ExprStmt:
    96  		needsLHS = true
    97  	default:
    98  		return
    99  	}
   100  
   101  	var (
   102  		label string
   103  		snip  snippet.Builder
   104  		score = highScore
   105  	)
   106  
   107  	if needsLHS {
   108  		// Offer the long form assign + append candidate if our best
   109  		// candidate is a slice.
   110  		bestItem := c.topCandidate()
   111  		if bestItem == nil || bestItem.obj == nil || bestItem.obj.Type() == nil {
   112  			return
   113  		}
   114  
   115  		if _, isSlice := bestItem.obj.Type().Underlying().(*types.Slice); !isSlice {
   116  			return
   117  		}
   118  
   119  		// Don't rank the full form assign + append candidate above the
   120  		// slice itself.
   121  		score = bestItem.Score - 0.01
   122  
   123  		// Fill in rest of sliceText now that we have the object name.
   124  		sliceText += bestItem.Label
   125  
   126  		// Fill in the candidate's LHS bits.
   127  		label = fmt.Sprintf("%s = ", bestItem.Label)
   128  		snip.WriteText(label)
   129  	}
   130  
   131  	snip.WriteText(fmt.Sprintf("append(%s, ", sliceText))
   132  	snip.WritePlaceholder(nil)
   133  	snip.WriteText(")")
   134  
   135  	c.items = append(c.items, CompletionItem{
   136  		Label:   label + fmt.Sprintf("append(%s, )", sliceText),
   137  		Kind:    protocol.FunctionCompletion,
   138  		Score:   score,
   139  		snippet: &snip,
   140  	})
   141  }
   142  
   143  // topCandidate returns the strictly highest scoring candidate
   144  // collected so far. If the top two candidates have the same score,
   145  // nil is returned.
   146  func (c *completer) topCandidate() *CompletionItem {
   147  	var bestItem, secondBestItem *CompletionItem
   148  	for i := range c.items {
   149  		if bestItem == nil || c.items[i].Score > bestItem.Score {
   150  			bestItem = &c.items[i]
   151  		} else if secondBestItem == nil || c.items[i].Score > secondBestItem.Score {
   152  			secondBestItem = &c.items[i]
   153  		}
   154  	}
   155  
   156  	// If secondBestItem has the same score, bestItem isn't
   157  	// the strict best.
   158  	if secondBestItem != nil && secondBestItem.Score == bestItem.Score {
   159  		return nil
   160  	}
   161  
   162  	return bestItem
   163  }
   164  
   165  // addErrCheckAndReturn offers a completion candidate of the form:
   166  //
   167  //     if err != nil {
   168  //       return nil, err
   169  //     }
   170  //
   171  // The position must be in a function that returns an error, and the
   172  // statement preceding the position must be an assignment where the
   173  // final LHS object is an error. addErrCheckAndReturn will synthesize
   174  // zero values as necessary to make the return statement valid.
   175  func (c *completer) addErrCheckAndReturn() {
   176  	if len(c.path) < 2 || c.enclosingFunc == nil || !c.opts.placeholders {
   177  		return
   178  	}
   179  
   180  	var (
   181  		errorType = types.Universe.Lookup("error").Type()
   182  		result    = c.enclosingFunc.sig.Results()
   183  	)
   184  	// Make sure our enclosing function returns an error.
   185  	if result.Len() == 0 || !types.Identical(result.At(result.Len()-1).Type(), errorType) {
   186  		return
   187  	}
   188  
   189  	prevLine := prevStmt(c.pos, c.path)
   190  	if prevLine == nil {
   191  		return
   192  	}
   193  
   194  	// Make sure our preceding statement was as assignment.
   195  	assign, _ := prevLine.(*ast.AssignStmt)
   196  	if assign == nil || len(assign.Lhs) == 0 {
   197  		return
   198  	}
   199  
   200  	lastAssignee := assign.Lhs[len(assign.Lhs)-1]
   201  
   202  	// Make sure the final assignee is an error.
   203  	if !types.Identical(c.pkg.GetTypesInfo().TypeOf(lastAssignee), errorType) {
   204  		return
   205  	}
   206  
   207  	var (
   208  		// errText is e.g. "err" in "foo, err := bar()".
   209  		errText = source.FormatNode(c.snapshot.FileSet(), lastAssignee)
   210  
   211  		// Whether we need to include the "if" keyword in our candidate.
   212  		needsIf = true
   213  	)
   214  
   215  	// "_" isn't a real object.
   216  	if errText == "_" {
   217  		return
   218  	}
   219  
   220  	// Below we try to detect if the user has already started typing "if
   221  	// err" so we can replace what they've typed with our complete
   222  	// statement.
   223  	switch n := c.path[0].(type) {
   224  	case *ast.Ident:
   225  		switch c.path[1].(type) {
   226  		case *ast.ExprStmt:
   227  			// This handles:
   228  			//
   229  			//     f, err := os.Open("foo")
   230  			//     i<>
   231  
   232  			// Make sure they are typing "if".
   233  			if c.matcher.Score("if") <= 0 {
   234  				return
   235  			}
   236  		case *ast.IfStmt:
   237  			// This handles:
   238  			//
   239  			//     f, err := os.Open("foo")
   240  			//     if er<>
   241  
   242  			// Make sure they are typing the error's name.
   243  			if c.matcher.Score(errText) <= 0 {
   244  				return
   245  			}
   246  
   247  			needsIf = false
   248  		default:
   249  			return
   250  		}
   251  	case *ast.IfStmt:
   252  		// This handles:
   253  		//
   254  		//     f, err := os.Open("foo")
   255  		//     if <>
   256  
   257  		// Avoid false positives by ensuring the if's cond is a bad
   258  		// expression. For example, don't offer the completion in cases
   259  		// like "if <> somethingElse".
   260  		if _, bad := n.Cond.(*ast.BadExpr); !bad {
   261  			return
   262  		}
   263  
   264  		// If "if" is our direct prefix, we need to include it in our
   265  		// candidate since the existing "if" will be overwritten.
   266  		needsIf = c.pos == n.Pos()+token.Pos(len("if"))
   267  	}
   268  
   269  	// Build up a snippet that looks like:
   270  	//
   271  	//     if err != nil {
   272  	//       return <zero value>, ..., ${1:err}
   273  	//     }
   274  	//
   275  	// We make the error a placeholder so it is easy to alter the error.
   276  	var snip snippet.Builder
   277  	if needsIf {
   278  		snip.WriteText("if ")
   279  	}
   280  	snip.WriteText(fmt.Sprintf("%s != nil {\n\treturn ", errText))
   281  
   282  	for i := 0; i < result.Len()-1; i++ {
   283  		snip.WriteText(formatZeroValue(result.At(i).Type(), c.qf))
   284  		snip.WriteText(", ")
   285  	}
   286  
   287  	snip.WritePlaceholder(func(b *snippet.Builder) {
   288  		b.WriteText(errText)
   289  	})
   290  
   291  	snip.WriteText("\n}")
   292  
   293  	label := fmt.Sprintf("%[1]s != nil { return %[1]s }", errText)
   294  	if needsIf {
   295  		label = "if " + label
   296  	}
   297  
   298  	c.items = append(c.items, CompletionItem{
   299  		Label: label,
   300  		// There doesn't seem to be a more appropriate kind.
   301  		Kind:    protocol.KeywordCompletion,
   302  		Score:   highScore,
   303  		snippet: &snip,
   304  	})
   305  }