Skip to content

Commit c41b78f

Browse files
adonovanSilverRainZ
authored andcommitted
go/analysis/checker: a go/packages-based driver library
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
1 parent d03c59d commit c41b78f

File tree

3 files changed

+180
-0
lines changed

3 files changed

+180
-0
lines changed

go/analysis/checker/checker.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Package checker provides functions for loading a Go program using
2+
// go/packages and running moduler analyses on it.
3+
package checker
4+
5+
import (
6+
"golang.org/x/tools/go/analysis"
7+
internalchecker "golang.org/x/tools/go/analysis/internal/checker"
8+
"golang.org/x/tools/go/packages"
9+
)
10+
11+
// A Result holds the results of an analysis pass: the application of
12+
// a particular analyzer to a particular package.
13+
type Result = internalchecker.Result
14+
15+
// Analyze runs the specified analyzers on the initial packages.
16+
// It also runs any analyzer that makes use of Facts on all the
17+
// dependencies of the initial packages. (In this case the program
18+
// must have been loaded using the packages.LoadAllSyntax flag.)
19+
//
20+
// The elements of the results slice correspond to those of the the
21+
// initial packages slice. If analyses were applied to dependencies,
22+
// these may be found by traversing the Result.Deps edges.
23+
func Analyze(initial []*packages.Package, analyzers []*analysis.Analyzer) []*Result {
24+
return internalchecker.Analyze(initial, analyzers)
25+
}
26+
27+
// TODO(adonovan): API questions to resolve before we set this in stone:
28+
// - Provide some or all of the functionality of internalchecker.printDiagnostics,
29+
// including JSON printing? Currently this API makes it the client's responsibility.
30+
// - Move the definition of Result into this package, for clarity?
31+
// - Expose a function to apply one or more of Result.Diagnostics.SuggestedFixes?
32+
// - Expose Debug flags?
33+
// - Add an Options struct to give us latitude?

go/analysis/checker/example_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// The example command demonstrates a simple go/packages-based
2+
// analysis driver program.
3+
package checker_test
4+
5+
import (
6+
"fmt"
7+
"log"
8+
"reflect"
9+
10+
"golang.org/x/tools/go/analysis"
11+
"golang.org/x/tools/go/analysis/checker"
12+
"golang.org/x/tools/go/packages"
13+
)
14+
15+
func Example() {
16+
// Gather analyzers.
17+
analyzers := []*analysis.Analyzer{dummy}
18+
if err := analysis.Validate(analyzers); err != nil {
19+
log.Fatal(err)
20+
}
21+
22+
// Load packages.
23+
// We stop if there are any parse/type errors,
24+
// but that isn't strictly necessary.
25+
cfg := &packages.Config{Mode: packages.LoadAllSyntax}
26+
initial, err := packages.Load(cfg, ".") // (this package)
27+
if err != nil {
28+
log.Fatal(err) // parse/type error
29+
}
30+
if len(initial) == 0 {
31+
log.Fatalf("no initial packages")
32+
}
33+
34+
// Run analyzers.
35+
results := checker.Analyze(initial, analyzers)
36+
37+
// Print graph of results.
38+
seen := make(map[*checker.Result]bool)
39+
var printAll func(results []*checker.Result)
40+
printAll = func(results []*checker.Result) {
41+
for _, r := range results {
42+
if !seen[r] {
43+
seen[r] = true
44+
printAll(r.Deps)
45+
46+
// Print the Result and any diagnostics.
47+
fmt.Printf("%v:", r) // name
48+
if r.Result != nil {
49+
fmt.Printf(" (result=%v)", r.Result)
50+
}
51+
if r.Err != nil {
52+
fmt.Printf("%v\n", r.Err)
53+
} else {
54+
fmt.Println()
55+
for _, diag := range r.Diagnostics {
56+
fmt.Printf("- %s: %s\n", r.Package.Fset.Position(diag.Pos), diag.Message)
57+
}
58+
}
59+
}
60+
}
61+
}
62+
printAll(results)
63+
64+
// Output:
65+
//
66+
// TODO: think of a simple analyzer that produces predictable
67+
// results that we could use in an example test. e.g.
68+
// compute the largest package among the transitive deps.
69+
}
70+
71+
var dummy = &analysis.Analyzer{
72+
Name: "dummy",
73+
Doc: "An trivial analyzer that reports the number of files in a package.",
74+
Run: dummyRun,
75+
ResultType: reflect.TypeOf(0), // (number of source lines in package)
76+
FactTypes: []analysis.Fact{(*dummyFact)(nil)},
77+
}
78+
79+
// A dummy fact that causes analysis of all dependencies
80+
type dummyFact struct{}
81+
82+
func (dummyFact) AFact() {}
83+
84+
func dummyRun(pass *analysis.Pass) (interface{}, error) {
85+
nlines := 0
86+
for _, f := range pass.Files {
87+
nlines += pass.Fset.Position(f.End()).Line
88+
}
89+
if nfiles := len(pass.Files); nfiles > 0 {
90+
pass.Reportf(pass.Files[0].Package, "package %s has %d files", pass.Pkg.Name(), nfiles)
91+
}
92+
return nlines, nil
93+
}

go/analysis/internal/checker/checker.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -981,3 +981,57 @@ func (act *action) allPackageFacts() []analysis.PackageFact {
981981
}
982982

983983
func dbg(b byte) bool { return strings.IndexByte(Debug, b) >= 0 }
984+
985+
// -- public API for checker building blocks --
986+
987+
// A Result holds the result of an analysis pass: the application of
988+
// an Analyzer to a Package.
989+
//
990+
// TODO(adonovan): move this out of here, for API clarity. That may
991+
// require declaring it in analysis/checker and moving the conversion
992+
// logic in Analyze into that package, which would in turn require
993+
// that 'action' expose more of itself to analysis/checker.
994+
type Result struct {
995+
Analyzer *analysis.Analyzer
996+
Package *packages.Package
997+
Deps []*Result // analysis results of direct dependencies
998+
Result interface{} // computed result of Analyzer.run, if any
999+
Err error // error result of Analyzer.run
1000+
Diagnostics []analysis.Diagnostic
1001+
}
1002+
1003+
func (r *Result) String() string {
1004+
return fmt.Sprintf("%s@%s", r.Analyzer, r.Package)
1005+
}
1006+
1007+
// Analyze runs the core of the analysis as a pure function (unlike the other
1008+
// entry points which use globals like flags, profiling, and os.Exit).
1009+
func Analyze(initial []*packages.Package, analyzers []*analysis.Analyzer) (results []*Result) {
1010+
// Run the analysis.
1011+
roots := analyze(initial, analyzers)
1012+
1013+
// Convert action graph to public Result graph.
1014+
m := make(map[*action]*Result)
1015+
var convert func(act *action) *Result
1016+
convert = func(act *action) *Result {
1017+
res := m[act]
1018+
if res == nil {
1019+
res = &Result{
1020+
Analyzer: act.a,
1021+
Package: act.pkg,
1022+
Result: act.result,
1023+
Diagnostics: act.diagnostics,
1024+
Err: act.err,
1025+
}
1026+
m[act] = res
1027+
for _, dep := range act.deps {
1028+
res.Deps = append(res.Deps, convert(dep))
1029+
}
1030+
}
1031+
return res
1032+
}
1033+
for _, root := range roots {
1034+
results = append(results, convert(root))
1035+
}
1036+
return results
1037+
}

0 commit comments

Comments
 (0)