internal/lsp: type-check packages from source

This change moves gopls from type-checking packages using the
go/packages API to type-checking from source. This is the first step in
adding caching to gopls.

Change-Id: I2a7dcfd8c9c0bfc6c35c86eadcdc6f9ce53d9be7
Reviewed-on: https://go-review.googlesource.com/c/161497
Reviewed-by: Ian Cottrell <iancottrell@google.com>
diff --git a/go/packages/packagestest/export.go b/go/packages/packagestest/export.go
index 1442495..ea478a3 100644
--- a/go/packages/packagestest/export.go
+++ b/go/packages/packagestest/export.go
@@ -137,6 +137,7 @@
 			Env:     append(os.Environ(), "GOPACKAGESDRIVER=off"),
 			Overlay: make(map[string][]byte),
 			Tests:   true,
+			Mode:    packages.LoadImports,
 		},
 		Modules: modules,
 		temp:    temp,
diff --git a/internal/lsp/cache/file.go b/internal/lsp/cache/file.go
index 406fe3c..cded872 100644
--- a/internal/lsp/cache/file.go
+++ b/internal/lsp/cache/file.go
@@ -60,6 +60,9 @@
 		if err := f.view.parse(f.URI); err != nil {
 			return nil, err
 		}
+		if f.ast == nil {
+			return nil, fmt.Errorf("failed to find or parse %v", f.URI)
+		}
 	}
 	return f.ast, nil
 }
@@ -71,6 +74,9 @@
 		if err := f.view.parse(f.URI); err != nil {
 			return nil, err
 		}
+		if f.pkg == nil {
+			return nil, fmt.Errorf("failed to find or parse %v", f.URI)
+		}
 	}
 	return f.pkg, nil
 }
diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go
index a9f0ab1..90393c8 100644
--- a/internal/lsp/cache/view.go
+++ b/internal/lsp/cache/view.go
@@ -7,7 +7,11 @@
 import (
 	"context"
 	"fmt"
+	"go/ast"
+	"go/parser"
+	"go/scanner"
 	"go/token"
+	"go/types"
 	"log"
 	"sync"
 
@@ -105,35 +109,155 @@
 		}
 		return err
 	}
-	var foundPkg bool // true if we found a package for uri
 	for _, pkg := range pkgs {
-		if len(pkg.Syntax) == 0 {
-			return fmt.Errorf("no syntax trees for %s", pkg.PkgPath)
+		imp := &importer{
+			entries:         make(map[string]*entry),
+			packages:        make(map[string]*packages.Package),
+			v:               v,
+			topLevelPkgPath: pkg.PkgPath,
 		}
-		// Add every file in this package to our cache.
-		for _, fAST := range pkg.Syntax {
-			// TODO: If a file is in multiple packages, which package do we store?
-			if !fAST.Pos().IsValid() {
-				log.Printf("invalid position for AST %v", fAST.Name)
-				continue
-			}
-			fToken := v.Config.Fset.File(fAST.Pos())
-			if fToken == nil {
-				log.Printf("no token.File for %v", fAST.Name)
-				continue
-			}
-			fURI := source.ToURI(fToken.Name())
-			f := v.getFile(fURI)
-			f.token = fToken
-			f.ast = fAST
-			f.pkg = pkg
-			if fURI == uri {
-				foundPkg = true
-			}
+		if err := imp.addImports(pkg); err != nil {
+			return err
 		}
-	}
-	if !foundPkg {
-		return fmt.Errorf("no package found for %v", uri)
+		imp.importPackage(pkg.PkgPath)
 	}
 	return nil
 }
+
+type importer struct {
+	mu              sync.Mutex
+	entries         map[string]*entry
+	packages        map[string]*packages.Package
+	topLevelPkgPath string
+
+	v *View
+}
+
+type entry struct {
+	pkg   *types.Package
+	err   error
+	ready chan struct{}
+}
+
+func (imp *importer) addImports(pkg *packages.Package) error {
+	imp.packages[pkg.PkgPath] = pkg
+	for _, i := range pkg.Imports {
+		if i.PkgPath == pkg.PkgPath {
+			return fmt.Errorf("import cycle: [%v]", pkg.PkgPath)
+		}
+		if err := imp.addImports(i); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (imp *importer) Import(path string) (*types.Package, error) {
+	if path == imp.topLevelPkgPath {
+		return nil, fmt.Errorf("import cycle: [%v]", path)
+	}
+	imp.mu.Lock()
+	e, ok := imp.entries[path]
+	if ok {
+		// cache hit
+		imp.mu.Unlock()
+		// wait for entry to become ready
+		<-e.ready
+	} else {
+		// cache miss
+		e = &entry{ready: make(chan struct{})}
+		imp.entries[path] = e
+		imp.mu.Unlock()
+
+		// This goroutine becomes responsible for populating
+		// the entry and broadcasting its readiness.
+		e.pkg, e.err = imp.importPackage(path)
+		close(e.ready)
+	}
+	return e.pkg, e.err
+}
+
+func (imp *importer) importPackage(pkgPath string) (*types.Package, error) {
+	imp.mu.Lock()
+	pkg, ok := imp.packages[pkgPath]
+	imp.mu.Unlock()
+	if !ok {
+		return nil, fmt.Errorf("no metadata for %v", pkgPath)
+	}
+	pkg.Fset = imp.v.Config.Fset
+	pkg.Syntax = make([]*ast.File, len(pkg.GoFiles))
+	for i, filename := range pkg.GoFiles {
+		var src interface{}
+		overlay, ok := imp.v.Config.Overlay[filename]
+		if ok {
+			src = overlay
+		}
+		file, err := parser.ParseFile(imp.v.Config.Fset, filename, src, parser.AllErrors|parser.ParseComments)
+		if file == nil {
+			return nil, err
+		}
+		if err != nil {
+			switch err := err.(type) {
+			case *scanner.Error:
+				pkg.Errors = append(pkg.Errors, packages.Error{
+					Pos:  err.Pos.String(),
+					Msg:  err.Msg,
+					Kind: packages.ParseError,
+				})
+			case scanner.ErrorList:
+				// The first parser error is likely the root cause of the problem.
+				if err.Len() > 0 {
+					pkg.Errors = append(pkg.Errors, packages.Error{
+						Pos:  err[0].Pos.String(),
+						Msg:  err[0].Msg,
+						Kind: packages.ParseError,
+					})
+				}
+			}
+		}
+		pkg.Syntax[i] = file
+	}
+	cfg := &types.Config{
+		Error: func(err error) {
+			if err, ok := err.(types.Error); ok {
+				pkg.Errors = append(pkg.Errors, packages.Error{
+					Pos:  imp.v.Config.Fset.Position(err.Pos).String(),
+					Msg:  err.Msg,
+					Kind: packages.TypeError,
+				})
+			}
+		},
+		Importer: imp,
+	}
+	pkg.Types = types.NewPackage(pkg.PkgPath, pkg.Name)
+	pkg.TypesInfo = &types.Info{
+		Types:      make(map[ast.Expr]types.TypeAndValue),
+		Defs:       make(map[*ast.Ident]types.Object),
+		Uses:       make(map[*ast.Ident]types.Object),
+		Implicits:  make(map[ast.Node]types.Object),
+		Selections: make(map[*ast.SelectorExpr]*types.Selection),
+		Scopes:     make(map[ast.Node]*types.Scope),
+	}
+	check := types.NewChecker(cfg, imp.v.Config.Fset, pkg.Types, pkg.TypesInfo)
+	check.Files(pkg.Syntax)
+
+	// Add every file in this package to our cache.
+	for _, file := range pkg.Syntax {
+		// TODO: If a file is in multiple packages, which package do we store?
+		if !file.Pos().IsValid() {
+			log.Printf("invalid position for file %v", file.Name)
+			continue
+		}
+		tok := imp.v.Config.Fset.File(file.Pos())
+		if tok == nil {
+			log.Printf("no token.File for %v", file.Name)
+			continue
+		}
+		fURI := source.ToURI(tok.Name())
+		f := imp.v.getFile(fURI)
+		f.token = tok
+		f.ast = file
+		f.pkg = pkg
+	}
+	return pkg.Types, nil
+}
diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go
index eb8b0f3..3425e38 100644
--- a/internal/lsp/lsp_test.go
+++ b/internal/lsp/lsp_test.go
@@ -14,7 +14,6 @@
 	"strings"
 	"testing"
 
-	"golang.org/x/tools/go/packages"
 	"golang.org/x/tools/go/packages/packagestest"
 	"golang.org/x/tools/internal/lsp/cache"
 	"golang.org/x/tools/internal/lsp/diff"
@@ -60,7 +59,6 @@
 	// Merge the exported.Config with the view.Config.
 	cfg := *exported.Config
 	cfg.Fset = token.NewFileSet()
-	cfg.Mode = packages.LoadSyntax
 
 	s := &server{
 		view: cache.NewView(&cfg),
diff --git a/internal/lsp/server.go b/internal/lsp/server.go
index 2558da2..b70fec6 100644
--- a/internal/lsp/server.go
+++ b/internal/lsp/server.go
@@ -90,7 +90,7 @@
 
 	s.view = cache.NewView(&packages.Config{
 		Dir:     rootPath,
-		Mode:    packages.LoadSyntax,
+		Mode:    packages.LoadImports,
 		Fset:    token.NewFileSet(),
 		Tests:   true,
 		Overlay: make(map[string][]byte),
@@ -170,9 +170,11 @@
 		return jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "no content changes provided")
 	}
 	// We expect the full content of file, i.e. a single change with no range.
-	if change := params.ContentChanges[0]; change.RangeLength == 0 {
-		s.cacheAndDiagnose(ctx, params.TextDocument.URI, change.Text)
+	change := params.ContentChanges[0]
+	if change.RangeLength != 0 {
+		return jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "unexpected change range provided")
 	}
+	s.cacheAndDiagnose(ctx, params.TextDocument.URI, change.Text)
 	return nil
 }
 
diff --git a/internal/lsp/source/definition.go b/internal/lsp/source/definition.go
index 27dc0d1..bac247b 100644
--- a/internal/lsp/source/definition.go
+++ b/internal/lsp/source/definition.go
@@ -5,7 +5,6 @@
 package source
 
 import (
-	"bytes"
 	"context"
 	"fmt"
 	"go/ast"
@@ -142,33 +141,6 @@
 	if !p.IsValid() {
 		return Range{}, fmt.Errorf("invalid position for %v", obj.Name())
 	}
-	tok := v.FileSet().File(p)
-	pos := tok.Position(p)
-	if pos.Column == 1 {
-		// We do not have full position information because exportdata does not
-		// store the column. For now, we attempt to read the original source
-		// and find the identifier within the line. If we find it, we patch the
-		// column to match its offset.
-		//
-		// TODO: If we parse from source, we will never need this hack.
-		f, err := v.GetFile(ctx, ToURI(pos.Filename))
-		if err != nil {
-			goto Return
-		}
-		src, err := f.Read()
-		if err != nil {
-			goto Return
-		}
-		tok, err := f.GetToken()
-		if err != nil {
-			goto Return
-		}
-		start := lineStart(tok, pos.Line)
-		offset := tok.Offset(start)
-		col := bytes.Index(src[offset:], []byte(obj.Name()))
-		p = tok.Pos(offset + col)
-	}
-Return:
 	return Range{
 		Start: p,
 		End:   p + token.Pos(identifierLen(obj.Name())),