perf: lazily load gazelle manifest files (#2746)

In large repositories where Python may not be the only language, the
gazelle manifest loading is done unnecessarily, and is done during the
configuration walk.

This means that even for non-python gazelle invocations (eg `bazel run
gazelle -- web/`), Python manifest files are being parsed and loaded
into memory.
This issue compounds if the repository uses multiple dependency
closures, ie multiple `gazelle_python.yaml` files.
In our repo, we currently have ~250 Python manifests, so loading them
when Gazelle is only running over other languages is time consuming.

Co-authored-by: Douglas Thor <dougthor42@users.noreply.github.com>
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e7f9fe3..299a43e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -76,6 +76,9 @@
 * (pypi) The PyPI extension will no longer write the lock file entries as the
   extension has been marked reproducible.
   Fixes [#2434](https://github.com/bazel-contrib/rules_python/issues/2434).
+* (gazelle) Lazily load and parse manifest files when running Gazelle. This ensures no
+  manifest files are loaded when Gazelle is run over a set of non-python directories
+  [PR #2746](https://github.com/bazel-contrib/rules_python/pull/2746).
 * (rules) {attr}`py_binary.srcs` and {attr}`py_test.srcs` is no longer mandatory when
   `main_module` is specified (for `--bootstrap_impl=script`)
 
diff --git a/gazelle/python/configure.go b/gazelle/python/configure.go
index 7b1f091..a00b0ba 100644
--- a/gazelle/python/configure.go
+++ b/gazelle/python/configure.go
@@ -18,7 +18,6 @@
 	"flag"
 	"fmt"
 	"log"
-	"os"
 	"path/filepath"
 	"strconv"
 	"strings"
@@ -27,7 +26,6 @@
 	"github.com/bazelbuild/bazel-gazelle/rule"
 	"github.com/bmatcuk/doublestar/v4"
 
-	"github.com/bazel-contrib/rules_python/gazelle/manifest"
 	"github.com/bazel-contrib/rules_python/gazelle/pythonconfig"
 )
 
@@ -228,25 +226,5 @@
 	}
 
 	gazelleManifestPath := filepath.Join(c.RepoRoot, rel, gazelleManifestFilename)
-	gazelleManifest, err := py.loadGazelleManifest(gazelleManifestPath)
-	if err != nil {
-		log.Fatal(err)
-	}
-	if gazelleManifest != nil {
-		config.SetGazelleManifest(gazelleManifest)
-	}
-}
-
-func (py *Configurer) loadGazelleManifest(gazelleManifestPath string) (*manifest.Manifest, error) {
-	if _, err := os.Stat(gazelleManifestPath); err != nil {
-		if os.IsNotExist(err) {
-			return nil, nil
-		}
-		return nil, fmt.Errorf("failed to load Gazelle manifest at %q: %w", gazelleManifestPath, err)
-	}
-	manifestFile := new(manifest.File)
-	if err := manifestFile.Decode(gazelleManifestPath); err != nil {
-		return nil, fmt.Errorf("failed to load Gazelle manifest at %q: %w", gazelleManifestPath, err)
-	}
-	return manifestFile.Manifest, nil
+	config.SetGazelleManifestPath(gazelleManifestPath)
 }
diff --git a/gazelle/pythonconfig/pythonconfig.go b/gazelle/pythonconfig/pythonconfig.go
index 23c0cfd..866339d 100644
--- a/gazelle/pythonconfig/pythonconfig.go
+++ b/gazelle/pythonconfig/pythonconfig.go
@@ -16,6 +16,8 @@
 
 import (
 	"fmt"
+	"log"
+	"os"
 	"path"
 	"regexp"
 	"strings"
@@ -153,10 +155,11 @@
 type Config struct {
 	parent *Config
 
-	extensionEnabled  bool
-	repoRoot          string
-	pythonProjectRoot string
-	gazelleManifest   *manifest.Manifest
+	extensionEnabled    bool
+	repoRoot            string
+	pythonProjectRoot   string
+	gazelleManifestPath string
+	gazelleManifest     *manifest.Manifest
 
 	excludedPatterns                          *singlylinkedlist.List
 	ignoreFiles                               map[string]struct{}
@@ -281,11 +284,26 @@
 	c.gazelleManifest = gazelleManifest
 }
 
+// SetGazelleManifestPath sets the path to the gazelle_python.yaml file
+// for the current configuration.
+func (c *Config) SetGazelleManifestPath(gazelleManifestPath string) {
+	c.gazelleManifestPath = gazelleManifestPath
+}
+
 // FindThirdPartyDependency scans the gazelle manifests for the current config
 // and the parent configs up to the root finding if it can resolve the module
 // name.
 func (c *Config) FindThirdPartyDependency(modName string) (string, string, bool) {
 	for currentCfg := c; currentCfg != nil; currentCfg = currentCfg.parent {
+		// Attempt to load the manifest if needed.
+		if currentCfg.gazelleManifestPath != "" && currentCfg.gazelleManifest == nil {
+			currentCfgManifest, err := loadGazelleManifest(currentCfg.gazelleManifestPath)
+			if err != nil {
+				log.Fatal(err)
+			}
+			currentCfg.SetGazelleManifest(currentCfgManifest)
+		}
+
 		if currentCfg.gazelleManifest != nil {
 			gazelleManifest := currentCfg.gazelleManifest
 			if distributionName, ok := gazelleManifest.ModulesMapping[modName]; ok {
@@ -526,3 +544,17 @@
 
 	return label.New(repositoryName, normConventionalDistributionName, normConventionalDistributionName)
 }
+
+func loadGazelleManifest(gazelleManifestPath string) (*manifest.Manifest, error) {
+	if _, err := os.Stat(gazelleManifestPath); err != nil {
+		if os.IsNotExist(err) {
+			return nil, nil
+		}
+		return nil, fmt.Errorf("failed to load Gazelle manifest at %q: %w", gazelleManifestPath, err)
+	}
+	manifestFile := new(manifest.File)
+	if err := manifestFile.Decode(gazelleManifestPath); err != nil {
+		return nil, fmt.Errorf("failed to load Gazelle manifest at %q: %w", gazelleManifestPath, err)
+	}
+	return manifestFile.Manifest, nil
+}