internal/checker: run despite all errors

Previously, we would run an analysis when RunDespiteErrors=true only
if all package errors were syntax or type errors. We should run the
analysis on all errors.

The previous design choice was motivated by a concern that list and
unknown errors might indicate something unexpectedly wrong about the
program. If RunDespiteErrors=true, then this would go under the users'
radar.

This design choice seems somewhat arbitrary and creates problems when a
list error is a duplicate of a type error; the analysis is not
executed despite a conceptually single type error. Further, package
errors are always printed to stderr, so they are visible to users.

Lastly, we return exit code 3 if there are any diagnostics. Otherwise,
we return 1 exit code, if there are any packages errors.

Fixes golang/go#67790

Change-Id: Ie3186a08ed3c8292033344aec35fc755a6d256f2
Reviewed-on: https://go-review.googlesource.com/c/tools/+/591117
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
diff --git a/go/analysis/internal/checker/checker.go b/go/analysis/internal/checker/checker.go
index 52f0e55..5a71465 100644
--- a/go/analysis/internal/checker/checker.go
+++ b/go/analysis/internal/checker/checker.go
@@ -11,7 +11,6 @@
 import (
 	"bytes"
 	"encoding/gob"
-	"errors"
 	"flag"
 	"fmt"
 	"go/format"
@@ -134,16 +133,21 @@
 	allSyntax := needFacts(analyzers)
 	initial, err := load(args, allSyntax)
 	if err != nil {
-		if _, ok := err.(typeParseError); !ok {
-			// Fail when some of the errors are not
-			// related to parsing nor typing.
-			log.Print(err)
-			return 1
-		}
-		// TODO: filter analyzers based on RunDespiteError?
+		log.Print(err)
+		return 1
 	}
 
-	// Run the analysis.
+	pkgsExitCode := 0
+	// Print package errors regardless of RunDespiteErrors.
+	// Do not exit if there are errors, yet.
+	if n := packages.PrintErrors(initial); n > 0 {
+		pkgsExitCode = 1
+	}
+
+	// Run the analyzers. On each package with (transitive)
+	// errors, we run only the subset of analyzers that are
+	// marked (and whose transitive requirements are also
+	// marked) with RunDespiteErrors.
 	roots := analyze(initial, analyzers)
 
 	// Apply fixes.
@@ -155,18 +159,18 @@
 		}
 	}
 
-	// Print the results.
-	return printDiagnostics(roots)
+	// Print the results. If !RunDespiteErrors and there
+	// are errors in the packages, this will have 0 exit
+	// code. Otherwise, we prefer to return exit code
+	// indicating diagnostics.
+	if diagExitCode := printDiagnostics(roots); diagExitCode != 0 {
+		return diagExitCode // there were diagnostics
+	}
+	return pkgsExitCode // package errors but no diagnostics
 }
 
-// typeParseError represents a package load error
-// that is related to typing and parsing.
-type typeParseError struct {
-	error
-}
-
-// load loads the initial packages. If all loading issues are related to
-// typing and parsing, the returned error is of type typeParseError.
+// load loads the initial packages. Returns only top-level loading
+// errors. Does not consider errors in packages.
 func load(patterns []string, allSyntax bool) ([]*packages.Package, error) {
 	mode := packages.LoadSyntax
 	if allSyntax {
@@ -178,44 +182,12 @@
 		Tests: IncludeTests,
 	}
 	initial, err := packages.Load(&conf, patterns...)
-	if err == nil {
-		if len(initial) == 0 {
-			err = fmt.Errorf("%s matched no packages", strings.Join(patterns, " "))
-		} else {
-			err = loadingError(initial)
-		}
+	if err == nil && len(initial) == 0 {
+		err = fmt.Errorf("%s matched no packages", strings.Join(patterns, " "))
 	}
 	return initial, err
 }
 
-// loadingError checks for issues during the loading of initial
-// packages. Returns nil if there are no issues. Returns error
-// of type typeParseError if all errors, including those in
-// dependencies, are related to typing or parsing. Otherwise,
-// a plain error is returned with an appropriate message.
-func loadingError(initial []*packages.Package) error {
-	var err error
-	if n := packages.PrintErrors(initial); n > 1 {
-		err = fmt.Errorf("%d errors during loading", n)
-	} else if n == 1 {
-		err = errors.New("error during loading")
-	} else {
-		// no errors
-		return nil
-	}
-	all := true
-	packages.Visit(initial, nil, func(pkg *packages.Package) {
-		for _, err := range pkg.Errors {
-			typeOrParse := err.Kind == packages.TypeError || err.Kind == packages.ParseError
-			all = all && typeOrParse
-		}
-	})
-	if all {
-		return typeParseError{err}
-	}
-	return err
-}
-
 // TestAnalyzer applies an analyzer to a set of packages (and their
 // dependencies if necessary) and returns the results.
 // The analyzer must be valid according to [analysis.Validate].
diff --git a/go/analysis/internal/checker/checker_test.go b/go/analysis/internal/checker/checker_test.go
index 69ea944..46c74d7 100644
--- a/go/analysis/internal/checker/checker_test.go
+++ b/go/analysis/internal/checker/checker_test.go
@@ -68,10 +68,11 @@
 }
 
 var renameAnalyzer = &analysis.Analyzer{
-	Name:     "rename",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
-	Run:      run,
-	Doc:      "renames symbols named bar to baz",
+	Name:             "rename",
+	Requires:         []*analysis.Analyzer{inspect.Analyzer},
+	Run:              run,
+	Doc:              "renames symbols named bar to baz",
+	RunDespiteErrors: true,
 }
 
 var otherAnalyzer = &analysis.Analyzer{ // like analyzer but with a different Name.
@@ -140,13 +141,28 @@
 func Foo(s string) int {
 	return s + 1
 }
-`}
+`,
+		"cperr/test.go": `package copyerr
+
+import "sync"
+
+func bar() { } // for renameAnalyzer
+
+type T struct{ mu sync.Mutex }
+type T1 struct{ t *T }
+
+func NewT1() *T1 { return &T1{T} }
+`,
+	}
 
 	testdata, cleanup, err := analysistest.WriteFiles(files)
 	if err != nil {
 		t.Fatal(err)
 	}
-	path := filepath.Join(testdata, "src/rderr/test.go")
+	defer cleanup()
+
+	rderrFile := "file=" + filepath.Join(testdata, "src/rderr/test.go")
+	cperrFile := "file=" + filepath.Join(testdata, "src/cperr/test.go")
 
 	// A no-op analyzer that should finish regardless of
 	// parse or type errors in the code.
@@ -159,8 +175,8 @@
 		RunDespiteErrors: true,
 	}
 
-	// A no-op analyzer that should finish regardless of
-	// parse or type errors in the code.
+	// A no-op analyzer, with facts, that should finish
+	// regardless of parse or type errors in the code.
 	noopWithFact := &analysis.Analyzer{
 		Name:     "noopfact",
 		Requires: []*analysis.Analyzer{inspect.Analyzer},
@@ -178,30 +194,34 @@
 		code      int
 	}{
 		// parse/type errors
-		{name: "skip-error", pattern: []string{"file=" + path}, analyzers: []*analysis.Analyzer{renameAnalyzer}, code: 1},
+		{name: "skip-error", pattern: []string{rderrFile}, analyzers: []*analysis.Analyzer{renameAnalyzer}, code: 1},
 		// RunDespiteErrors allows a driver to run an Analyzer even after parse/type errors.
 		//
 		// The noop analyzer doesn't use facts, so the driver loads only the root
 		// package from source. For the rest, it asks 'go list' for export data,
 		// which fails because the compiler encounters the type error.  Since the
 		// errors come from 'go list', the driver doesn't run the analyzer.
-		{name: "despite-error", pattern: []string{"file=" + path}, analyzers: []*analysis.Analyzer{noop}, code: 1},
+		{name: "despite-error", pattern: []string{rderrFile}, analyzers: []*analysis.Analyzer{noop}, code: 1},
 		// The noopfact analyzer does use facts, so the driver loads source for
 		// all dependencies, does type checking itself, recognizes the error as a
 		// type error, and runs the analyzer.
-		{name: "despite-error-fact", pattern: []string{"file=" + path}, analyzers: []*analysis.Analyzer{noopWithFact}, code: 0},
+		{name: "despite-error-fact", pattern: []string{rderrFile}, analyzers: []*analysis.Analyzer{noopWithFact}, code: 1},
 		// combination of parse/type errors and no errors
-		{name: "despite-error-and-no-error", pattern: []string{"file=" + path, "sort"}, analyzers: []*analysis.Analyzer{renameAnalyzer, noop}, code: 1},
+		{name: "despite-error-and-no-error", pattern: []string{rderrFile, "sort"}, analyzers: []*analysis.Analyzer{renameAnalyzer, noop}, code: 1},
 		// non-existing package error
 		{name: "no-package", pattern: []string{"xyz"}, analyzers: []*analysis.Analyzer{renameAnalyzer}, code: 1},
 		{name: "no-package-despite-error", pattern: []string{"abc"}, analyzers: []*analysis.Analyzer{noop}, code: 1},
 		{name: "no-multi-package-despite-error", pattern: []string{"xyz", "abc"}, analyzers: []*analysis.Analyzer{noop}, code: 1},
 		// combination of type/parsing and different errors
-		{name: "different-errors", pattern: []string{"file=" + path, "xyz"}, analyzers: []*analysis.Analyzer{renameAnalyzer, noop}, code: 1},
+		{name: "different-errors", pattern: []string{rderrFile, "xyz"}, analyzers: []*analysis.Analyzer{renameAnalyzer, noop}, code: 1},
 		// non existing dir error
 		{name: "no-match-dir", pattern: []string{"file=non/existing/dir"}, analyzers: []*analysis.Analyzer{renameAnalyzer, noop}, code: 1},
 		// no errors
 		{name: "no-errors", pattern: []string{"sort"}, analyzers: []*analysis.Analyzer{renameAnalyzer, noop}, code: 0},
+		// duplicate list error with no findings
+		{name: "list-error", pattern: []string{cperrFile}, analyzers: []*analysis.Analyzer{noop}, code: 1},
+		// duplicate list errors with findings (issue #67790)
+		{name: "list-error-findings", pattern: []string{cperrFile}, analyzers: []*analysis.Analyzer{renameAnalyzer}, code: 3},
 	} {
 		if test.name == "despite-error" && testenv.Go1Point() < 20 {
 			// The behavior in the comment on the despite-error test only occurs for Go 1.20+.
@@ -211,8 +231,6 @@
 			t.Errorf("got incorrect exit code %d for test %s; want %d", got, test.name, test.code)
 		}
 	}
-
-	defer cleanup()
 }
 
 type EmptyFact struct{}