internal/lsp: add label completion candidates

Now we offer completion candidates for labels when completing "break",
"continue", and "goto" statements. We are reasonably smart about
filtering unusable labels, except we don't filter "goto" candidates
that jump across variable definitions.

Fixes golang/go#33987.

Change-Id: If296a7579845aba5d86c7050ab195c35d4b147ed
Reviewed-on: https://go-review.googlesource.com/c/tools/+/197417
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/internal/lsp/source/completion.go b/internal/lsp/source/completion.go
index 9f45283..4d96d89 100644
--- a/internal/lsp/source/completion.go
+++ b/internal/lsp/source/completion.go
@@ -163,8 +163,9 @@
 	// candidate to be. It will be the zero value if no information is available.
 	expectedType typeInference
 
-	// enclosingFunction is the function declaration enclosing the position.
-	enclosingFunction *types.Signature
+	// enclosingFunc contains information about the function enclosing
+	// the position.
+	enclosingFunc *funcInfo
 
 	// enclosingCompositeLiteral contains information about the composite literal
 	// enclosing the position.
@@ -189,6 +190,15 @@
 	startTime time.Time
 }
 
+// funcInfo holds info about a function object.
+type funcInfo struct {
+	// sig is the function declaration enclosing the position.
+	sig *types.Signature
+
+	// body is the function's body.
+	body *ast.BlockStmt
+}
+
 type compLitInfo struct {
 	// cl is the *ast.CompositeLit enclosing the position.
 	cl *ast.CompositeLit
@@ -434,7 +444,6 @@
 		return nil, nil, nil
 	}
 
-	clInfo := enclosingCompositeLiteral(path, rng.Start, pkg.GetTypesInfo())
 	c := &completer{
 		pkg:                       pkg,
 		snapshot:                  snapshot,
@@ -446,8 +455,8 @@
 		path:                      path,
 		pos:                       rng.Start,
 		seen:                      make(map[types.Object]bool),
-		enclosingFunction:         enclosingFunction(path, rng.Start, pkg.GetTypesInfo()),
-		enclosingCompositeLiteral: clInfo,
+		enclosingFunc:             enclosingFunction(path, rng.Start, pkg.GetTypesInfo()),
+		enclosingCompositeLiteral: enclosingCompositeLiteral(path, rng.Start, pkg.GetTypesInfo()),
 		opts:                      opts,
 		// default to a matcher that always matches
 		matcher:        prefixMatcher(""),
@@ -476,6 +485,11 @@
 		return c.items, c.getSurrounding(), nil
 	}
 
+	if lt := c.wantLabelCompletion(); lt != labelNone {
+		c.labels(lt)
+		return c.items, c.getSurrounding(), nil
+	}
+
 	switch n := path[0].(type) {
 	case *ast.Ident:
 		// Is this the Sel part of a selector?
@@ -847,17 +861,24 @@
 	return nil
 }
 
-// enclosingFunction returns the signature of the function enclosing the given position.
-func enclosingFunction(path []ast.Node, pos token.Pos, info *types.Info) *types.Signature {
+// enclosingFunction returns the signature and body of the function
+// enclosing the given position.
+func enclosingFunction(path []ast.Node, pos token.Pos, info *types.Info) *funcInfo {
 	for _, node := range path {
 		switch t := node.(type) {
 		case *ast.FuncDecl:
 			if obj, ok := info.Defs[t.Name]; ok {
-				return obj.Type().(*types.Signature)
+				return &funcInfo{
+					sig:  obj.Type().(*types.Signature),
+					body: t.Body,
+				}
 			}
 		case *ast.FuncLit:
 			if typ, ok := info.Types[t]; ok {
-				return typ.Type.(*types.Signature)
+				return &funcInfo{
+					sig:  typ.Type.(*types.Signature),
+					body: t.Body,
+				}
 			}
 		}
 	}
@@ -1017,7 +1038,8 @@
 			}
 			return typeInference{}
 		case *ast.ReturnStmt:
-			if sig := c.enclosingFunction; sig != nil {
+			if c.enclosingFunc != nil {
+				sig := c.enclosingFunc.sig
 				// Find signature result that corresponds to our return statement.
 				if resultIdx := indexExprAtPos(c.pos, node.Results); resultIdx < len(node.Results) {
 					if resultIdx < sig.Results().Len() {
diff --git a/internal/lsp/source/completion_format.go b/internal/lsp/source/completion_format.go
index 3e445e2..80b1c4f 100644
--- a/internal/lsp/source/completion_format.go
+++ b/internal/lsp/source/completion_format.go
@@ -83,6 +83,9 @@
 	case *types.PkgName:
 		kind = protocol.ModuleCompletion
 		detail = fmt.Sprintf("%q", obj.Imported().Path())
+	case *types.Label:
+		kind = protocol.ConstantCompletion
+		detail = "label"
 	}
 
 	// If this candidate needs an additional import statement,
diff --git a/internal/lsp/source/completion_labels.go b/internal/lsp/source/completion_labels.go
new file mode 100644
index 0000000..887b333
--- /dev/null
+++ b/internal/lsp/source/completion_labels.go
@@ -0,0 +1,111 @@
+// 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 (
+	"go/ast"
+	"go/token"
+)
+
+type labelType int
+
+const (
+	labelNone labelType = iota
+	labelBreak
+	labelContinue
+	labelGoto
+)
+
+// wantLabelCompletion returns true if we want (only) label
+// completions at the position.
+func (c *completer) wantLabelCompletion() labelType {
+	if _, ok := c.path[0].(*ast.Ident); ok && len(c.path) > 1 {
+		// We want a label if we are an *ast.Ident child of a statement
+		// that accepts a label, e.g. "break Lo<>".
+		return takesLabel(c.path[1])
+	}
+
+	return labelNone
+}
+
+// takesLabel returns the corresponding labelType if n is a statement
+// that accepts a label, otherwise labelNone.
+func takesLabel(n ast.Node) labelType {
+	if bs, ok := n.(*ast.BranchStmt); ok {
+		switch bs.Tok {
+		case token.BREAK:
+			return labelBreak
+		case token.CONTINUE:
+			return labelContinue
+		case token.GOTO:
+			return labelGoto
+		}
+	}
+	return labelNone
+}
+
+// labels adds completion items for labels defined in the enclosing
+// function.
+func (c *completer) labels(lt labelType) {
+	if c.enclosingFunc == nil {
+		return
+	}
+
+	addLabel := func(l *ast.LabeledStmt) {
+		labelObj := c.pkg.GetTypesInfo().ObjectOf(l.Label)
+		if labelObj != nil {
+			c.found(labelObj, highScore, nil)
+		}
+	}
+
+	switch lt {
+	case labelBreak, labelContinue:
+		// "break" and "continue" only accept labels from enclosing statements.
+
+		for _, p := range c.path {
+			switch p := p.(type) {
+			case *ast.FuncLit:
+				// Labels are function scoped, so don't continue out of functions.
+				return
+			case *ast.LabeledStmt:
+				switch p.Stmt.(type) {
+				case *ast.ForStmt:
+					// Loop labels can be used for "break" or "continue".
+					addLabel(p)
+				case *ast.SwitchStmt, *ast.SelectStmt:
+					// Switch and select labels can be used only for "break".
+					if lt == labelBreak {
+						addLabel(p)
+					}
+				}
+			}
+		}
+	case labelGoto:
+		// Goto accepts any label in the same function not in a nested
+		// block. It also doesn't take labels that would jump across
+		// variable definitions, but ignore that case for now.
+		ast.Inspect(c.enclosingFunc.body, func(n ast.Node) bool {
+			if n == nil {
+				return false
+			}
+
+			switch n := n.(type) {
+			// Only search into block-like nodes enclosing our "goto".
+			// This prevents us from finding labels in nested blocks.
+			case *ast.BlockStmt, *ast.CommClause, *ast.CaseClause:
+				for _, p := range c.path {
+					if n == p {
+						return true
+					}
+				}
+				return false
+			case *ast.LabeledStmt:
+				addLabel(n)
+			}
+
+			return true
+		})
+	}
+}
diff --git a/internal/lsp/testdata/labels/labels.go b/internal/lsp/testdata/labels/labels.go
new file mode 100644
index 0000000..2162fec
--- /dev/null
+++ b/internal/lsp/testdata/labels/labels.go
@@ -0,0 +1,39 @@
+package labels
+
+func _() {
+	goto F //@complete(" //", label1, label4)
+
+Foo1: //@item(label1, "Foo1", "label", "const")
+	for {
+	Foo2: //@item(label2, "Foo2", "label", "const")
+		switch {
+		case true:
+			break F //@complete(" //", label2, label1)
+
+			continue F //@complete(" //", label1)
+
+			{
+			FooUnjumpable:
+			}
+
+			goto F //@complete(" //", label1, label2, label4)
+
+			func() {
+				goto F //@complete(" //", label3)
+
+				break F //@complete(" //")
+
+				continue F //@complete(" //")
+
+			Foo3: //@item(label3, "Foo3", "label", "const")
+			}()
+		}
+	}
+
+	break F //@complete(" //")
+
+	continue F //@complete(" //")
+
+Foo4: //@item(label4, "Foo4", "label", "const")
+	return
+}
diff --git a/internal/lsp/testdata/summary.txt.golden b/internal/lsp/testdata/summary.txt.golden
index 5c1e1b2..bccb234 100644
--- a/internal/lsp/testdata/summary.txt.golden
+++ b/internal/lsp/testdata/summary.txt.golden
@@ -1,5 +1,5 @@
 -- summary --
-CompletionsCount = 169
+CompletionsCount = 178
 CompletionSnippetCount = 36
 UnimportedCompletionsCount = 1
 DeepCompletionsCount = 5