diff --git a/go/analysis/checker/checker.go b/go/analysis/checker/checker.go new file mode 100644 index 00000000000..e03dc0be746 --- /dev/null +++ b/go/analysis/checker/checker.go @@ -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? diff --git a/go/analysis/checker/example_test.go b/go/analysis/checker/example_test.go new file mode 100644 index 00000000000..0d361e825b2 --- /dev/null +++ b/go/analysis/checker/example_test.go @@ -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 +} diff --git a/go/analysis/internal/checker/checker.go b/go/analysis/internal/checker/checker.go index b791c717e70..187824d33d5 100644 --- a/go/analysis/internal/checker/checker.go +++ b/go/analysis/internal/checker/checker.go @@ -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 +}