internal/lsp: improve completion after accidental keywords

Sometimes the prefix of the thing you want to complete is a keyword.
For example:

variance := 123
fmt.Println(var<>)

In this case the parser produces an *ast.BadExpr which breaks
completion. We now repair this BadExpr by replacing it with
an *ast.Ident named "var".

We also repair empty decls using a similar approach. This fixes cases
like:

var typeName string
type<> // want to complete to "typeName"

We also fix accidental keywords in selectors, such as:

foo.var<>

The parser produces a phantom "_" in place of the keyword, so we swap
it back for an *ast.Ident named "var".

In general, though, accidental keywords wreak havoc on the AST so we
can only do so much. There are still many cases where a keyword prefix
breaks completion. Perhaps in the future the parser can be
cursor/in-progress-edit aware and turn accidental keywords into
identifiers.

Fixes golang/go#34332.

PS I tweaked nodeContains() to include n.End() to fix a test failure
against tip related to a change to go/parser. When a syntax error is
present, an *ast.BlockStmt's End() is now set to the block's final
statement's End() (earlier than what it used to be). In order for the
cursor pos to test "inside" the block in this case I had to relax the
End() comparison.

Change-Id: Ib45952cf086cc974f1578298df3dd12829344faa
Reviewed-on: https://go-review.googlesource.com/c/tools/+/209438
Run-TryBot: Muir Manders <muir@mnd.rs>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/internal/lsp/cache/parse.go b/internal/lsp/cache/parse.go
index 6dff423..3ea1d97 100644
--- a/internal/lsp/cache/parse.go
+++ b/internal/lsp/cache/parse.go
@@ -231,21 +231,161 @@
 			// Don't propagate this error since *ast.BadExpr is very common
 			// and it is only sometimes due to array types. Errors from here
 			// are expected and not actionable in general.
-			fixArrayErr := fixArrayType(n, parent, tok, src)
-			if fixArrayErr == nil {
+			if fixArrayType(n, parent, tok, src) == nil {
 				// Recursively fix in our fixed node.
 				err = fix(ctx, parent, tok, src)
+				return false
 			}
+
+			// Fix cases where the parser expects an expression but finds a keyword, e.g.:
+			//
+			//   someFunc(var<>) // want to complete to "variance"
+			//
+			fixAccidentalKeyword(n, parent, tok, src)
+
 			return false
+		case *ast.DeclStmt:
+			// Fix cases where the completion prefix looks like a decl, e.g.:
+			//
+			//   func typeName(obj interface{}) string {}
+			//   type<> // want to call "typeName()" but looks like a "type" decl
+			//
+			fixAccidentalDecl(n, parent, tok, src)
+			return false
+		case *ast.SelectorExpr:
+			// Fix cases where a keyword prefix results in a phantom "_" selector, e.g.:
+			//
+			//   foo.var<> // want to complete to "foo.variance"
+			//
+			fixPhantomSelector(n, tok, src)
+			return true
 		default:
 			ancestors = append(ancestors, n)
 			parent = n
 			return true
 		}
 	})
+
 	return err
 }
 
+// fixAccidentalDecl tries to fix "accidental" declarations. For example:
+//
+// func typeOf() {}
+// type<> // want to call typeOf(), not declare a type
+//
+// If we find an *ast.DeclStmt with only a single phantom "_" spec, we
+// replace the decl statement with an expression statement containing
+// only the keyword. This allows completion to work to some degree.
+func fixAccidentalDecl(decl *ast.DeclStmt, parent ast.Node, tok *token.File, src []byte) {
+	genDecl, _ := decl.Decl.(*ast.GenDecl)
+	if genDecl == nil || len(genDecl.Specs) != 1 {
+		return
+	}
+
+	switch spec := genDecl.Specs[0].(type) {
+	case *ast.TypeSpec:
+		// If the name isn't a phantom "_" identifier inserted by the
+		// parser then the decl is likely legitimate and we shouldn't mess
+		// with it.
+		if !isPhantomUnderscore(spec.Name, tok, src) {
+			return
+		}
+	case *ast.ValueSpec:
+		if len(spec.Names) != 1 || !isPhantomUnderscore(spec.Names[0], tok, src) {
+			return
+		}
+	}
+
+	replaceNode(parent, decl, &ast.ExprStmt{
+		X: &ast.Ident{
+			Name:    genDecl.Tok.String(),
+			NamePos: decl.Pos(),
+		},
+	})
+}
+
+// fixPhantomSelector tries to fix selector expressions with phantom
+// "_" selectors. In particular, we check if the selector is a
+// keyword, and if so we swap in an *ast.Ident with the keyword text. For example:
+//
+// foo.var
+//
+// yields a "_" selector instead of "var" since "var" is a keyword.
+func fixPhantomSelector(sel *ast.SelectorExpr, tok *token.File, src []byte) {
+	if !isPhantomUnderscore(sel.Sel, tok, src) {
+		return
+	}
+
+	maybeKeyword := readKeyword(sel.Sel.Pos(), tok, src)
+	if maybeKeyword == "" {
+		return
+	}
+
+	replaceNode(sel, sel.Sel, &ast.Ident{
+		Name:    maybeKeyword,
+		NamePos: sel.Sel.Pos(),
+	})
+}
+
+// isPhantomUnderscore reports whether the given ident is a phantom
+// underscore. The parser sometimes inserts phantom underscores when
+// it encounters otherwise unparseable situations.
+func isPhantomUnderscore(id *ast.Ident, tok *token.File, src []byte) bool {
+	if id == nil || id.Name != "_" {
+		return false
+	}
+
+	// Phantom underscore means the underscore is not actually in the
+	// program text.
+	offset := tok.Offset(id.Pos())
+	return len(src) <= offset || src[offset] != '_'
+}
+
+// fixAccidentalKeyword tries to fix "accidental" keyword expressions. For example:
+//
+// variance := 123
+// doMath(var<>)
+//
+// If we find an *ast.BadExpr that begins with a keyword, we replace
+// the BadExpr with an *ast.Ident containing the text of the keyword.
+// This allows completion to work to some degree.
+func fixAccidentalKeyword(bad *ast.BadExpr, parent ast.Node, tok *token.File, src []byte) {
+	if !bad.Pos().IsValid() {
+		return
+	}
+
+	maybeKeyword := readKeyword(bad.Pos(), tok, src)
+	if maybeKeyword == "" {
+		return
+	}
+
+	replaceNode(parent, bad, &ast.Ident{Name: maybeKeyword, NamePos: bad.Pos()})
+}
+
+// readKeyword reads the keyword starting at pos, if any.
+func readKeyword(pos token.Pos, tok *token.File, src []byte) string {
+	var kwBytes []byte
+	for i := tok.Offset(pos); i < len(src); i++ {
+		// Use a simplified identifier check since keywords are always lowercase ASCII.
+		if src[i] < 'a' || src[i] > 'z' {
+			break
+		}
+		kwBytes = append(kwBytes, src[i])
+
+		// Stop search at arbitrarily chosen too-long-for-a-keyword length.
+		if len(kwBytes) > 15 {
+			return ""
+		}
+	}
+
+	if kw := string(kwBytes); token.Lookup(kw).IsKeyword() {
+		return kw
+	}
+
+	return ""
+}
+
 // fixArrayType tries to parse an *ast.BadExpr into an *ast.ArrayType.
 // go/parser often turns lone array types like "[]int" into BadExprs
 // if it isn't expecting a type.
diff --git a/internal/lsp/source/completion.go b/internal/lsp/source/completion.go
index d5bcf0f..b06c634 100644
--- a/internal/lsp/source/completion.go
+++ b/internal/lsp/source/completion.go
@@ -786,7 +786,7 @@
 }
 
 func nodeContains(n ast.Node, pos token.Pos) bool {
-	return n != nil && n.Pos() <= pos && pos < n.End()
+	return n != nil && n.Pos() <= pos && pos <= n.End()
 }
 
 func (c *completer) inConstDecl() bool {
diff --git a/internal/lsp/testdata/keywords/accidental_keywords.go.in b/internal/lsp/testdata/keywords/accidental_keywords.go.in
new file mode 100644
index 0000000..1ae8ff3
--- /dev/null
+++ b/internal/lsp/testdata/keywords/accidental_keywords.go.in
@@ -0,0 +1,19 @@
+package keywords
+
+// non-matching candidate - shouldn't show up as completion
+var apple = "apple"
+
+func _() {
+	variance := 123 //@item(kwVariance, "variance", "int", "var")
+	println(var) //@complete(")", kwVariance)
+}
+
+func _() {
+	var s struct { variance int } //@item(kwVarianceField, "variance", "int", "field")
+	s.var //@complete(" //", kwVarianceField)
+}
+
+func _() {
+	var typeName string //@item(kwTypeName, "typeName", "string", "var")
+	type //@complete(" //", kwTypeName)
+}
diff --git a/internal/lsp/testdata/summary.txt.golden b/internal/lsp/testdata/summary.txt.golden
index fdfde62..60d49aa 100644
--- a/internal/lsp/testdata/summary.txt.golden
+++ b/internal/lsp/testdata/summary.txt.golden
@@ -1,5 +1,5 @@
 -- summary --
-CompletionsCount = 216
+CompletionsCount = 219
 CompletionSnippetCount = 51
 UnimportedCompletionsCount = 3
 DeepCompletionsCount = 5