Description
Background
The existing singlechecker.Main and multichecker.Main are simple:
they take package patterns string args (e.g. ./...), loaded them with a hard-coded go/packages.Config
, and applied analyzers on the loaded packages. They also hand flags common across many analyzers, and handle error parsing, etc.
They are convenient, but not flexible enough to support analyzers that need special setup, more involved go/packages.Config (e.g. excluding tests, loading modules, applying overlays). An example analyzer I want to write:
-
It needs to initialize its state based on the module information loadable with go/packages.NeedModules load mode. Current APIs don't use NeedModules nor expose the loaded packages.
-
The initialization needs to access the loaded packages - e.g. this loads vulnerability info from a remote DB, and the loaded go/packages.Package and their modules are needed to retrieve only relevant info.
-
Since
singlechecker.Main
andmultichecker.Main
handle flag parsing, attempting to parse flags or command arguments before them will clobber their flag parsing logic.
Potential workaround is either to load the packages twice or to write the analyzer in a way that the very first call of its Run triggers the remaining initialization work. , which isn't ideal.
I propose we introduce a new API that provides hooks and potential customization options. With the change users can
- Use a different load mode, configuration, or even filter/fake the packages.
- Inject an extra initialization logic that requires loaded packages before running analysis.
Option 1: A new Run
that takes a Config
parameter:
package singlechecker
type Config = checker.Config
func Run(cfg Config, analyzers ...*analysis.Analyzer) { ... }
func Main(analyzers ...*analysis.Analyzer) {
Run(Config{}, analyzers...)
}
package checker // golang.org/x/tools/go/analysis/internal/checker
type Config struct {
// Load is called once at the start of Run to load packages specified by patterns.
Load func(mode packages.LoadMode, patterns []string) ([]*packages.Package, error)
}
Option 2: A package-level global variable for stubbing:
We can consider a package-level global Load
or Config
variable that affects the existing Main
's functionality.
package multichecker
// Load is a load function used by Main when loading packages to analyze.
// This is exported so users can inject their custom package load logic.
var Load = func(mode packages.LoadMode, patterns []string) ([]*packages.Package, error) { /* default load */ }
// Main uses Loader to load packages.
func Main(analyzers... *analysis.Analyzer) { ... }
Since singlechecker
and multichecker
are used as the 'main' of an analysis tool and already made use of many process-wide global variables, I expect the introduction of a global, exported variable is not too bad.
cc @golang/tools-team @adonovan @timothy-king