Skip to content

Commit

Permalink
go/analysis/checker: a go/packages-based driver library
Browse files Browse the repository at this point in the history
The {single,multi}checker packages provide the main function
for a complete application, as a black box. Many users want
the ability to customize the analyzer behavior with additional
logic, as described in the attached issues.

This change creates a new package, go/analysis/checker, that
exposes an Analyze pure function---one that avoids global flags,
os.Exit, logging, profiling, and other side effects---that
runs a set of analyzers on a set of packages loaded (by the
client) using go/packages, and presents the graph of results
in a form that allows postprocessing.

This is just a sketch. API feedback welcome.

DO NOT SUBMIT

Updates golang/go#30231
Updates golang/go#30219
Updates golang/go#31007
Updates golang/go#31897
Updates golang/go#50265
Updates golang/go#53215
Updates golang/go#53336

Change-Id: I745d319a587dca506564a4624b52a7f1eb5f4751
  • Loading branch information
adonovan authored and SilverRainZ committed Apr 11, 2023
1 parent d03c59d commit c41b78f
Show file tree
Hide file tree
Showing 3 changed files with 180 additions and 0 deletions.
33 changes: 33 additions & 0 deletions go/analysis/checker/checker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Package checker provides functions for loading a Go program using
// go/packages and running moduler analyses on it.
package checker

import (
"golang.org/x/tools/go/analysis"
internalchecker "golang.org/x/tools/go/analysis/internal/checker"
"golang.org/x/tools/go/packages"
)

// A Result holds the results of an analysis pass: the application of
// a particular analyzer to a particular package.
type Result = internalchecker.Result

// Analyze runs the specified analyzers on the initial packages.
// It also runs any analyzer that makes use of Facts on all the
// dependencies of the initial packages. (In this case the program
// must have been loaded using the packages.LoadAllSyntax flag.)
//
// The elements of the results slice correspond to those of the the
// initial packages slice. If analyses were applied to dependencies,
// these may be found by traversing the Result.Deps edges.
func Analyze(initial []*packages.Package, analyzers []*analysis.Analyzer) []*Result {
return internalchecker.Analyze(initial, analyzers)
}

// TODO(adonovan): API questions to resolve before we set this in stone:
// - Provide some or all of the functionality of internalchecker.printDiagnostics,
// including JSON printing? Currently this API makes it the client's responsibility.
// - Move the definition of Result into this package, for clarity?
// - Expose a function to apply one or more of Result.Diagnostics.SuggestedFixes?
// - Expose Debug flags?
// - Add an Options struct to give us latitude?
93 changes: 93 additions & 0 deletions go/analysis/checker/example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// The example command demonstrates a simple go/packages-based
// analysis driver program.
package checker_test

import (
"fmt"
"log"
"reflect"

"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/checker"
"golang.org/x/tools/go/packages"
)

func Example() {
// Gather analyzers.
analyzers := []*analysis.Analyzer{dummy}
if err := analysis.Validate(analyzers); err != nil {
log.Fatal(err)
}

// Load packages.
// We stop if there are any parse/type errors,
// but that isn't strictly necessary.
cfg := &packages.Config{Mode: packages.LoadAllSyntax}
initial, err := packages.Load(cfg, ".") // (this package)
if err != nil {
log.Fatal(err) // parse/type error
}
if len(initial) == 0 {
log.Fatalf("no initial packages")
}

// Run analyzers.
results := checker.Analyze(initial, analyzers)

// Print graph of results.
seen := make(map[*checker.Result]bool)
var printAll func(results []*checker.Result)
printAll = func(results []*checker.Result) {
for _, r := range results {
if !seen[r] {
seen[r] = true
printAll(r.Deps)

// Print the Result and any diagnostics.
fmt.Printf("%v:", r) // name
if r.Result != nil {
fmt.Printf(" (result=%v)", r.Result)
}
if r.Err != nil {
fmt.Printf("%v\n", r.Err)
} else {
fmt.Println()
for _, diag := range r.Diagnostics {
fmt.Printf("- %s: %s\n", r.Package.Fset.Position(diag.Pos), diag.Message)
}
}
}
}
}
printAll(results)

// Output:
//
// TODO: think of a simple analyzer that produces predictable
// results that we could use in an example test. e.g.
// compute the largest package among the transitive deps.
}

var dummy = &analysis.Analyzer{
Name: "dummy",
Doc: "An trivial analyzer that reports the number of files in a package.",
Run: dummyRun,
ResultType: reflect.TypeOf(0), // (number of source lines in package)
FactTypes: []analysis.Fact{(*dummyFact)(nil)},
}

// A dummy fact that causes analysis of all dependencies
type dummyFact struct{}

func (dummyFact) AFact() {}

func dummyRun(pass *analysis.Pass) (interface{}, error) {
nlines := 0
for _, f := range pass.Files {
nlines += pass.Fset.Position(f.End()).Line
}
if nfiles := len(pass.Files); nfiles > 0 {
pass.Reportf(pass.Files[0].Package, "package %s has %d files", pass.Pkg.Name(), nfiles)
}
return nlines, nil
}
54 changes: 54 additions & 0 deletions go/analysis/internal/checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -981,3 +981,57 @@ func (act *action) allPackageFacts() []analysis.PackageFact {
}

func dbg(b byte) bool { return strings.IndexByte(Debug, b) >= 0 }

// -- public API for checker building blocks --

// A Result holds the result of an analysis pass: the application of
// an Analyzer to a Package.
//
// TODO(adonovan): move this out of here, for API clarity. That may
// require declaring it in analysis/checker and moving the conversion
// logic in Analyze into that package, which would in turn require
// that 'action' expose more of itself to analysis/checker.
type Result struct {
Analyzer *analysis.Analyzer
Package *packages.Package
Deps []*Result // analysis results of direct dependencies
Result interface{} // computed result of Analyzer.run, if any
Err error // error result of Analyzer.run
Diagnostics []analysis.Diagnostic
}

func (r *Result) String() string {
return fmt.Sprintf("%s@%s", r.Analyzer, r.Package)
}

// Analyze runs the core of the analysis as a pure function (unlike the other
// entry points which use globals like flags, profiling, and os.Exit).
func Analyze(initial []*packages.Package, analyzers []*analysis.Analyzer) (results []*Result) {
// Run the analysis.
roots := analyze(initial, analyzers)

// Convert action graph to public Result graph.
m := make(map[*action]*Result)
var convert func(act *action) *Result
convert = func(act *action) *Result {
res := m[act]
if res == nil {
res = &Result{
Analyzer: act.a,
Package: act.pkg,
Result: act.result,
Diagnostics: act.diagnostics,
Err: act.err,
}
m[act] = res
for _, dep := range act.deps {
res.Deps = append(res.Deps, convert(dep))
}
}
return res
}
for _, root := range roots {
results = append(results, convert(root))
}
return results
}

0 comments on commit c41b78f

Please sign in to comment.