internal/lsp: add support for analyzers with dependencies on other analyzers

Steal alan's parallel analysis-graph-running code from multichecker.
Facts are still not supported.

Change-Id: I22f83375d7a314b49d4f458d6dd40c33febc795b
Reviewed-on: https://go-review.googlesource.com/c/161659
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go
index 0edfac1..eb8b0f3 100644
--- a/internal/lsp/lsp_test.go
+++ b/internal/lsp/lsp_test.go
@@ -36,7 +36,7 @@
 	// We hardcode the expected number of test cases to ensure that all tests
 	// are being executed. If a test is added, this number must be changed.
 	const expectedCompletionsCount = 63
-	const expectedDiagnosticsCount = 15
+	const expectedDiagnosticsCount = 16
 	const expectedFormatCount = 3
 	const expectedDefinitionsCount = 16
 	const expectedTypeDefinitionsCount = 2
@@ -206,7 +206,7 @@
 			if g.Range.Start != g.Range.End || w.Range.Start != g.Range.End {
 				goto Failed
 			}
-		} else {
+		} else if g.Range.End != g.Range.Start { // Accept any 'want' range if the diagnostic returns a zero-length range.
 			if w.Range.End != g.Range.End {
 				goto Failed
 			}
diff --git a/internal/lsp/source/diagnostics.go b/internal/lsp/source/diagnostics.go
index 53bcb0a..4a7fb78 100644
--- a/internal/lsp/source/diagnostics.go
+++ b/internal/lsp/source/diagnostics.go
@@ -9,8 +9,30 @@
 	"context"
 	"fmt"
 	"go/token"
+	"sort"
 	"strconv"
 	"strings"
+	"sync"
+
+	"golang.org/x/tools/go/analysis/passes/asmdecl"
+	"golang.org/x/tools/go/analysis/passes/assign"
+	"golang.org/x/tools/go/analysis/passes/atomic"
+	"golang.org/x/tools/go/analysis/passes/atomicalign"
+	"golang.org/x/tools/go/analysis/passes/bools"
+	"golang.org/x/tools/go/analysis/passes/buildtag"
+	"golang.org/x/tools/go/analysis/passes/cgocall"
+	"golang.org/x/tools/go/analysis/passes/composite"
+	"golang.org/x/tools/go/analysis/passes/copylock"
+	"golang.org/x/tools/go/analysis/passes/httpresponse"
+	"golang.org/x/tools/go/analysis/passes/loopclosure"
+	"golang.org/x/tools/go/analysis/passes/nilfunc"
+	"golang.org/x/tools/go/analysis/passes/shift"
+	"golang.org/x/tools/go/analysis/passes/stdmethods"
+	"golang.org/x/tools/go/analysis/passes/structtag"
+	"golang.org/x/tools/go/analysis/passes/unmarshal"
+	"golang.org/x/tools/go/analysis/passes/unreachable"
+	"golang.org/x/tools/go/analysis/passes/unsafeptr"
+	"golang.org/x/tools/go/analysis/passes/unusedresult"
 
 	"golang.org/x/tools/go/analysis"
 	"golang.org/x/tools/go/analysis/passes/tests"
@@ -166,34 +188,128 @@
 }
 
 func runAnalyses(pkg *packages.Package, report func(a *analysis.Analyzer, diag analysis.Diagnostic)) error {
+	// These are the analyses in the vetsuite, except for lostcancel and printf.
+	// Those rely on facts from other packages.
+	// TODO(matloob): Add fact support.
 	analyzers := []*analysis.Analyzer{
-		tests.Analyzer, // an analyzer that doesn't have facts or requires
+		asmdecl.Analyzer,
+		assign.Analyzer,
+		atomic.Analyzer,
+		atomicalign.Analyzer,
+		bools.Analyzer,
+		buildtag.Analyzer,
+		cgocall.Analyzer,
+		composite.Analyzer,
+		copylock.Analyzer,
+		httpresponse.Analyzer,
+		loopclosure.Analyzer,
+		nilfunc.Analyzer,
+		shift.Analyzer,
+		stdmethods.Analyzer,
+		structtag.Analyzer,
+		tests.Analyzer,
+		unmarshal.Analyzer,
+		unreachable.Analyzer,
+		unsafeptr.Analyzer,
+		unusedresult.Analyzer,
+	}
+
+	// Execute actions in parallel. Based on unitchecker's analysis execution logic.
+	type action struct {
+		once        sync.Once
+		result      interface{}
+		err         error
+		diagnostics []analysis.Diagnostic
+	}
+	actions := make(map[*analysis.Analyzer]*action)
+
+	var registerActions func(a *analysis.Analyzer)
+	registerActions = func(a *analysis.Analyzer) {
+		act, ok := actions[a]
+		if !ok {
+			act = new(action)
+			for _, req := range a.Requires {
+				registerActions(req)
+			}
+			actions[a] = act
+		}
 	}
 	for _, a := range analyzers {
-		if len(a.FactTypes) > 0 {
-			panic("for analyzer " + a.Name + " modular analyses needing facts are not yet supported")
-		}
-		if len(a.Requires) > 0 {
-			panic("for analyzer " + a.Name + " analyses requiring results are not yet supported")
-		}
-		pass := &analysis.Pass{
-			Analyzer: a,
+		registerActions(a)
+	}
 
-			Fset:       pkg.Fset,
-			Files:      pkg.Syntax,
-			OtherFiles: pkg.OtherFiles,
-			Pkg:        pkg.Types,
-			TypesInfo:  pkg.TypesInfo,
-			TypesSizes: pkg.TypesSizes,
+	// In parallel, execute the DAG of analyzers.
+	var exec func(a *analysis.Analyzer) *action
+	var execAll func(analyzers []*analysis.Analyzer)
+	exec = func(a *analysis.Analyzer) *action {
+		act := actions[a]
+		act.once.Do(func() {
+			if len(a.FactTypes) > 0 {
+				panic("for analyzer " + a.Name + " modular analyses needing facts are not yet supported")
+			}
 
-			Report: func(diagnostic analysis.Diagnostic) { report(a, diagnostic) },
+			execAll(a.Requires) // prefetch dependencies in parallel
 
-			// TODO(matloob): Fill in the fields ResultOf, ImportObjectFact, ImportPackageFact,
-			// ExportObjectFact, ExportPackageFact once modular facts and results are supported.
+			// The inputs to this analysis are the
+			// results of its prerequisites.
+			inputs := make(map[*analysis.Analyzer]interface{})
+			var failed []string
+			for _, req := range a.Requires {
+				reqact := exec(req)
+				if reqact.err != nil {
+					failed = append(failed, req.String())
+					continue
+				}
+				inputs[req] = reqact.result
+			}
+
+			// Report an error if any dependency failed.
+			if failed != nil {
+				sort.Strings(failed)
+				act.err = fmt.Errorf("failed prerequisites: %s", strings.Join(failed, ", "))
+				return
+			}
+
+			pass := &analysis.Pass{
+				Analyzer:   a,
+				Fset:       pkg.Fset,
+				Files:      pkg.Syntax,
+				OtherFiles: pkg.OtherFiles,
+				Pkg:        pkg.Types,
+				TypesInfo:  pkg.TypesInfo,
+				TypesSizes: pkg.TypesSizes,
+				ResultOf:   inputs,
+				Report:     func(d analysis.Diagnostic) { act.diagnostics = append(act.diagnostics, d) },
+			}
+
+			act.result, act.err = a.Run(pass)
+		})
+		return act
+	}
+	execAll = func(analyzers []*analysis.Analyzer) {
+		var wg sync.WaitGroup
+		for _, a := range analyzers {
+			wg.Add(1)
+			go func(a *analysis.Analyzer) {
+				_ = exec(a)
+				wg.Done()
+			}(a)
 		}
-		_, err := a.Run(pass)
-		if err != nil {
-			return err
+		wg.Wait()
+	}
+
+	execAll(analyzers)
+
+	// Report diagnostics and errors from root analyzers.
+	for _, a := range analyzers {
+		act := actions[a]
+		if act.err != nil {
+			// TODO(matloob): This isn't quite right: we might return a failed prerequisites error,
+			// which isn't super useful...
+			return act.err
+		}
+		for _, diag := range act.diagnostics {
+			report(a, diag)
 		}
 	}
 
diff --git a/internal/lsp/testdata/analyzer/bad_test.go b/internal/lsp/testdata/analyzer/bad_test.go
index cd8be65..7e3e864 100644
--- a/internal/lsp/testdata/analyzer/bad_test.go
+++ b/internal/lsp/testdata/analyzer/bad_test.go
@@ -1,7 +1,11 @@
 package analyzer
 
-import "testing"
+import (
+	"sync"
+	"testing"
+)
 
 func Testbad(t *testing.T) { //@diag("", "tests", "Testbad has malformed name: first letter after 'Test' must not be lowercase")
-
+	var x sync.Mutex
+	_ = x //@diag("x", "copylocks", "assignment copies lock value to _: sync.Mutex")
 }