internal/lsp/cache: trim ellipsis array literals

While looking at Kubernetes I noticed that golang.org/x/text packages
were some of the largest. The problem is the large code-generated
tables, which use ellipsis array literals. Teach gopls to trim the cases
that matter there.

While silly, this trims ~60MB off the live heap, so I think it might be
worth it.

Change-Id: I0cfd80bd5fbc8703ac628312982af9c6ed871758
Reviewed-on: https://go-review.googlesource.com/c/tools/+/248180
Run-TryBot: Heschi Kreinick <heschi@google.com>
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 1c4c64a..95e9de5 100644
--- a/internal/lsp/cache/parse.go
+++ b/internal/lsp/cache/parse.go
@@ -13,6 +13,7 @@
 	"go/scanner"
 	"go/token"
 	"reflect"
+	"strconv"
 
 	"golang.org/x/tools/internal/event"
 	"golang.org/x/tools/internal/lsp/debug/tag"
@@ -343,24 +344,78 @@
 		case *ast.CommClause:
 			n.Body = nil
 		case *ast.CompositeLit:
-			// Leave elts in place for [...]T
-			// array literals, because they can
-			// affect the expression's type.
-			if !isEllipsisArray(n.Type) {
-				n.Elts = nil
+			// types.Info.Types for long slice/array literals are particularly
+			// expensive. Try to clear them out.
+			at, ok := n.Type.(*ast.ArrayType)
+			if !ok {
+				break
 			}
+			// Removing the elements from an ellipsis array changes its type.
+			// Try to set the length explicitly so we can continue.
+			if _, ok := at.Len.(*ast.Ellipsis); ok {
+				length, ok := arrayLength(n)
+				if !ok {
+					break
+				}
+				at.Len = &ast.BasicLit{
+					Kind:     token.INT,
+					Value:    fmt.Sprint(length),
+					ValuePos: at.Len.Pos(),
+				}
+			}
+			n.Elts = nil
 		}
 		return true
 	})
 }
 
-func isEllipsisArray(n ast.Expr) bool {
-	at, ok := n.(*ast.ArrayType)
-	if !ok {
-		return false
+// arrayLength returns the length of some simple forms of ellipsis array literal.
+// Notably, it handles the tables in golang.org/x/text.
+func arrayLength(array *ast.CompositeLit) (int, bool) {
+	litVal := func(expr ast.Expr) (int, bool) {
+		lit, ok := expr.(*ast.BasicLit)
+		if !ok {
+			return 0, false
+		}
+		val, err := strconv.ParseInt(lit.Value, 10, 64)
+		if err != nil {
+			return 0, false
+		}
+		return int(val), true
 	}
-	_, ok = at.Len.(*ast.Ellipsis)
-	return ok
+	largestKey := -1
+	for _, elt := range array.Elts {
+		kve, ok := elt.(*ast.KeyValueExpr)
+		if !ok {
+			continue
+		}
+		switch key := kve.Key.(type) {
+		case *ast.BasicLit:
+			if val, ok := litVal(key); ok && largestKey < val {
+				largestKey = val
+			}
+		case *ast.BinaryExpr:
+			// golang.org/x/text uses subtraction (and only subtraction) in its indices.
+			if key.Op != token.SUB {
+				break
+			}
+			x, ok := litVal(key.X)
+			if !ok {
+				break
+			}
+			y, ok := litVal(key.Y)
+			if !ok {
+				break
+			}
+			if val := x - y; largestKey < val {
+				largestKey = val
+			}
+		}
+	}
+	if largestKey != -1 {
+		return largestKey + 1, true
+	}
+	return len(array.Elts), true
 }
 
 // fixAST inspects the AST and potentially modifies any *ast.BadStmts so that it can be
diff --git a/internal/lsp/cache/parse_test.go b/internal/lsp/cache/parse_test.go
new file mode 100644
index 0000000..ff1b83f
--- /dev/null
+++ b/internal/lsp/cache/parse_test.go
@@ -0,0 +1,37 @@
+// 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 cache
+
+import (
+	"go/ast"
+	"go/parser"
+	"testing"
+)
+
+func TestArrayLength(t *testing.T) {
+	tests := []struct {
+		expr   string
+		length int
+	}{
+		{`[...]int{0,1,2,3,4,5,6,7,8,9}`, 10},
+		{`[...]int{9:0}`, 10},
+		{`[...]int{19-10:0}`, 10},
+		{`[...]int{19-10:0, 17-10:0, 18-10:0}`, 10},
+	}
+
+	for _, tt := range tests {
+		expr, err := parser.ParseExpr(tt.expr)
+		if err != nil {
+			t.Fatal(err)
+		}
+		l, ok := arrayLength(expr.(*ast.CompositeLit))
+		if !ok {
+			t.Errorf("arrayLength did not recognize expression %#v", expr)
+		}
+		if l != tt.length {
+			t.Errorf("arrayLength(%#v) = %v, want %v", expr, l, tt.length)
+		}
+	}
+}