cmd/vulncheck_sandbox: categorize package loading errors

This is achieved indirectly by forcing the load.Packages function to
classify loading errors. This in turn implies some naming changes to the
error constants in derrors package.

The rationale for this approach is that package loading is irrespective
of the overall analysis, so these analyses should classify the errors
the same way.

Change-Id: I00c0dfeea816ccc7e5d8942f14a9b7be75251524
Reviewed-on: https://go-review.googlesource.com/c/pkgsite-metrics/+/473396
Reviewed-by: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Zvonimir Pavlinovic <zpavlinovic@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/internal/derrors/derrors.go b/internal/derrors/derrors.go
index 0a89df7..bbefa4e 100644
--- a/internal/derrors/derrors.go
+++ b/internal/derrors/derrors.go
@@ -51,38 +51,38 @@
 	// to disk during the scan setup process. This is not an error with vulncheck.
 	ScanModuleOSError = errors.New("scan module OS error")
 
-	// ScanModuleLoadPackagesError is used to capture general unclassified issues
-	// with load packages during the scan setup process. This is not an error with
+	// LoadPackagesError is used to capture general unclassified issues with
+	// load packages during the scan setup process. This is not an error with
 	// vulncheck. There are specific load packages errors that are categorized
-	// separately, e.g., ScanModuleLoadPackagesNoGoModError.
-	ScanModuleLoadPackagesError = errors.New("scan module load packages error")
+	// separately, e.g., LoadPackagesNoGoModError.
+	LoadPackagesError = errors.New("scan module load packages error")
 
-	// ScanModuleLoadPackagesGoVersionError is used to capture issues with loading
-	// packages where the module is not supported by the current Go version. This
-	// is not an error with any specific scan technique.
-	ScanModuleLoadPackagesGoVersionError = errors.New("scan module load packages error: Go version mismatch")
+	// LoadPackagesGoVersionError is used to capture issues with loading
+	// packages where the module is not supported by the current Go version.
+	// This is not an error with any specific scan technique.
+	LoadPackagesGoVersionError = errors.New("scan module load packages error: Go version mismatch")
 
-	// ScanModuleLoadPackagesNoGoModError is used to capture a specific issue
-	// with loading packages during the scan setup process where a go.mod file
+	// LoadPackagesNoGoModError is used to capture a specific issue with
+	// loading packages during the scan setup process where a go.mod file
 	// is missing. This is not an error with vulncheck.
-	ScanModuleLoadPackagesNoGoModError = errors.New("scan module load packages error: does not have go.mod")
+	LoadPackagesNoGoModError = errors.New("scan module load packages error: does not have go.mod")
 
-	// ScanModuleLoadPackagesNoGoSumError is used to capture a specific issue
-	// with loading packages during the scan setup process where a go.sum file
+	// LoadPackagesNoGoSumError is used to capture a specific issue with
+	// loading packages during the scan setup process where a go.sum file
 	// is missing. This is not an error with vulncheck.
-	ScanModuleLoadPackagesNoGoSumError = errors.New("scan module load packages error: does not have go.sum")
+	LoadPackagesNoGoSumError = errors.New("scan module load packages error: does not have go.sum")
 
-	// ScanModuleLoadPackagesNoRequiredModuleError is used to capture a specific
+	// LoadPackagesNoRequiredModuleError is used to capture a specific
 	// issue with loading packages during the scan setup process where a package
 	// is imported but no required module is provided. This is not an error with
 	// vulncheck and is likely happening due to outdated go.sum file.
-	ScanModuleLoadPackagesNoRequiredModuleError = errors.New("scan module load packages error: no required module provided")
+	LoadPackagesNoRequiredModuleError = errors.New("scan module load packages error: no required module provided")
 
-	// ScanModuleLoadPackagesMissingGoSumEntryError is used to capture a specific
+	// LoadPackagesMissingGoSumEntryError is used to capture a specific
 	// issue with loading packages during the scan setup process where a package
 	// is imported but some of its go.sum entries are missing. This is not an error
 	// with vulncheck and is likely happening due to outdated go.sum file.
-	ScanModuleLoadPackagesMissingGoSumEntryError = errors.New("scan module load packages error: missing go.sum entry")
+	LoadPackagesMissingGoSumEntryError = errors.New("scan module load packages error: missing go.sum entry")
 
 	// ScanModuleVulncheckDBConnectionError is used to capture a specific
 	// vulncheck scan error where a connection to vuln db failed.
@@ -177,17 +177,17 @@
 		return "VULNCHECK - MISC"
 	case errors.Is(err, ScanModuleVulncheckDBConnectionError):
 		return "VULNCHECK - DB CONNECTION"
-	case errors.Is(err, ScanModuleLoadPackagesError):
+	case errors.Is(err, LoadPackagesError):
 		return "LOAD"
-	case errors.Is(err, ScanModuleLoadPackagesGoVersionError):
+	case errors.Is(err, LoadPackagesGoVersionError):
 		return "LOAD - WRONG GO VERSION"
-	case errors.Is(err, ScanModuleLoadPackagesNoGoModError):
+	case errors.Is(err, LoadPackagesNoGoModError):
 		return "LOAD - NO GO.MOD"
-	case errors.Is(err, ScanModuleLoadPackagesNoGoSumError):
+	case errors.Is(err, LoadPackagesNoGoSumError):
 		return "LOAD - NO GO.SUM"
-	case errors.Is(err, ScanModuleLoadPackagesNoRequiredModuleError):
+	case errors.Is(err, LoadPackagesNoRequiredModuleError):
 		return "LOAD - NO REQUIRED MODULE"
-	case errors.Is(err, ScanModuleLoadPackagesMissingGoSumEntryError):
+	case errors.Is(err, LoadPackagesMissingGoSumEntryError):
 		return "LOAD - NO GO.SUM ENTRY"
 	case errors.Is(err, ScanModuleOSError):
 		return "OS"
diff --git a/internal/load/load.go b/internal/load/load.go
index ce252fe..56f6f13 100644
--- a/internal/load/load.go
+++ b/internal/load/load.go
@@ -6,10 +6,14 @@
 package load
 
 import (
+	"errors"
 	"fmt"
 	"go/build"
+	"os"
+	"path/filepath"
 	"strings"
 
+	"golang.org/x/pkgsite-metrics/internal/derrors"
 	"golang.org/x/tools/go/packages"
 )
 
@@ -26,11 +30,25 @@
 
 // Packages loads Go packages from source.
 // In addition to the packages, it returns all errors from loading the packages.
-// If the third return value is non-nil, that indicates a problem performing the load itself,
-// not a problem with the code being loaded.
+//
+// If the third return value is non-nil, that indicates a problem performing the
+// load itself, not a problem with the code being loaded. In that case, Packages
+// tries to classify the error using derrors package.
 func Packages(cfg *packages.Config, patterns ...string) ([]*packages.Package, []error, error) {
 	pkgs, err := packages.Load(cfg, patterns...)
 	if err != nil {
+		switch {
+		case !fileExists(filepath.Join(cfg.Dir, "go.mod")):
+			err = fmt.Errorf("%v: %w", err, derrors.LoadPackagesNoGoModError)
+		case !fileExists(filepath.Join(cfg.Dir, "go.sum")):
+			err = fmt.Errorf("%v: %w", err, derrors.LoadPackagesNoGoSumError)
+		case isNoRequiredModule(err):
+			err = fmt.Errorf("%v: %w", err, derrors.LoadPackagesNoRequiredModuleError)
+		case isMissingGoSumEntry(err.Error()):
+			err = fmt.Errorf("%v: %w", err, derrors.LoadPackagesMissingGoSumEntryError)
+		default:
+			err = fmt.Errorf("%v: %w", err, derrors.LoadPackagesError)
+		}
 		return nil, nil, err
 	}
 	var errors []error
@@ -48,3 +66,25 @@
 	}
 	return pkgs, errors, nil
 }
+
+func isNoRequiredModule(err error) bool {
+	return strings.Contains(err.Error(), "no required module")
+}
+
+func isMissingGoSumEntry(errMsg string) bool {
+	return strings.Contains(errMsg, "missing go.sum entry")
+}
+
+// fileExists checks if file path exists. Returns true
+// if the file exists or it cannot prove that it does
+// not exist. Otherwise, returns false.
+func fileExists(file string) bool {
+	if _, err := os.Stat(file); err == nil {
+		return true
+	} else if errors.Is(err, os.ErrNotExist) {
+		return false
+	}
+	// Conservatively return true if os.Stat fails
+	// for some other reason.
+	return true
+}
diff --git a/internal/worker/vulncheck_scan.go b/internal/worker/vulncheck_scan.go
index 107b7da..8873f9f 100644
--- a/internal/worker/vulncheck_scan.go
+++ b/internal/worker/vulncheck_scan.go
@@ -571,20 +571,6 @@
 	if err == nil && len(pkgErrors) > 0 {
 		err = fmt.Errorf("%v", pkgErrors)
 	}
-	if err != nil {
-		if !fileExists(filepath.Join(tempDir, "go.mod")) {
-			err = fmt.Errorf("%v: %w", err, derrors.ScanModuleLoadPackagesNoGoModError)
-		} else if !fileExists(filepath.Join(tempDir, "go.sum")) {
-			err = fmt.Errorf("%v: %w", err, derrors.ScanModuleLoadPackagesNoGoSumError)
-		} else if isNoRequiredModule(err) {
-			err = fmt.Errorf("%v: %w", err, derrors.ScanModuleLoadPackagesNoRequiredModuleError)
-		} else if isMissingGoSumEntry(err.Error()) {
-			err = fmt.Errorf("%v: %w", err, derrors.ScanModuleLoadPackagesMissingGoSumEntryError)
-		} else {
-			err = fmt.Errorf("%v: %w", err, derrors.ScanModuleLoadPackagesError)
-		}
-		return nil, err
-	}
 
 	stats.pkgsMemory = memSubtract(currHeapUsage(), preScanMemory)
 
@@ -710,34 +696,12 @@
 	return err
 }
 
-// fileExists checks if file path exists. Returns true
-// if the file exists or it cannot prove that it does
-// not exist. Otherwise, returns false.
-func fileExists(file string) bool {
-	if _, err := os.Stat(file); err == nil {
-		return true
-	} else if errors.Is(err, os.ErrNotExist) {
-		return false
-	}
-	// Conservatively return true if os.Stat fails
-	// for some other reason.
-	return true
-}
-
 func isVulnDBConnection(err error) bool {
 	s := err.Error()
 	return strings.Contains(s, "https://vuln.go.dev") &&
 		strings.Contains(s, "connection")
 }
 
-func isNoRequiredModule(err error) bool {
-	return strings.Contains(err.Error(), "no required module")
-}
-
-func isMissingGoSumEntry(errMsg string) bool {
-	return strings.Contains(errMsg, "missing go.sum entry")
-}
-
 func vulncheckConfig(dbClient vulnc.Client, mode string) *vulncheck.Config {
 	cfg := &vulncheck.Config{Client: dbClient}
 	switch mode {