| // Copyright 2023 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package unitchecker_test |
| |
| // This file illustrates separate analysis with an example. |
| |
| import ( |
| "bytes" |
| "encoding/json" |
| "fmt" |
| "go/token" |
| "go/types" |
| "io" |
| "os" |
| "path/filepath" |
| "sort" |
| "strings" |
| "sync/atomic" |
| "testing" |
| |
| "golang.org/x/tools/go/analysis/passes/printf" |
| "golang.org/x/tools/go/analysis/unitchecker" |
| "golang.org/x/tools/go/gcexportdata" |
| "golang.org/x/tools/go/packages" |
| "golang.org/x/tools/internal/testenv" |
| "golang.org/x/tools/internal/testfiles" |
| "golang.org/x/tools/txtar" |
| ) |
| |
| // TestExampleSeparateAnalysis demonstrates the principle of separate |
| // analysis, the distribution of units of type-checking and analysis |
| // work across several processes, using serialized summaries to |
| // communicate between them. |
| // |
| // It uses two different kinds of task, "manager" and "worker": |
| // |
| // - The manager computes the graph of package dependencies, and makes |
| // a request to the worker for each package. It does not parse, |
| // type-check, or analyze Go code. It is analogous "go vet". |
| // |
| // - The worker, which contains the Analyzers, reads each request, |
| // loads, parses, and type-checks the files of one package, |
| // applies all necessary analyzers to the package, then writes |
| // its results to a file. It is a unitchecker-based driver, |
| // analogous to the program specified by go vet -vettool= flag. |
| // |
| // In practice these would be separate executables, but for simplicity |
| // of this example they are provided by one executable in two |
| // different modes: the Example function is the manager, and the same |
| // executable invoked with ENTRYPOINT=worker is the worker. |
| // (See TestIntegration for how this happens.) |
| // |
| // Unfortunately this can't be a true Example because of the skip, |
| // which requires a testing.T. |
| func TestExampleSeparateAnalysis(t *testing.T) { |
| testenv.NeedsGoPackages(t) |
| |
| // src is an archive containing a module with a printf mistake. |
| const src = ` |
| -- go.mod -- |
| module separate |
| go 1.18 |
| |
| -- main/main.go -- |
| package main |
| |
| import "separate/lib" |
| |
| func main() { |
| lib.MyPrintf("%s", 123) |
| } |
| |
| -- lib/lib.go -- |
| package lib |
| |
| import "fmt" |
| |
| func MyPrintf(format string, args ...any) { |
| fmt.Printf(format, args...) |
| } |
| ` |
| |
| // Expand archive into tmp tree. |
| fs, err := txtar.FS(txtar.Parse([]byte(src))) |
| if err != nil { |
| t.Fatal(err) |
| } |
| tmpdir := testfiles.CopyToTmp(t, fs) |
| |
| // Load metadata for the main package and all its dependencies. |
| cfg := &packages.Config{ |
| Mode: packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles | packages.NeedImports | packages.NeedModule, |
| Dir: tmpdir, |
| Env: append(os.Environ(), |
| "GOPROXY=off", // disable network |
| "GOWORK=off", // an ambient GOWORK value would break package loading |
| ), |
| Logf: t.Logf, |
| } |
| pkgs, err := packages.Load(cfg, "separate/main") |
| if err != nil { |
| t.Fatal(err) |
| } |
| // Stop if any package had a metadata error. |
| if packages.PrintErrors(pkgs) > 0 { |
| t.Fatal("there were errors among loaded packages") |
| } |
| |
| // Now we have loaded the import graph, |
| // let's begin the proper work of the manager. |
| |
| // Gather root packages. They will get all analyzers, |
| // whereas dependencies get only the subset that |
| // produce facts or are required by them. |
| roots := make(map[*packages.Package]bool) |
| for _, pkg := range pkgs { |
| roots[pkg] = true |
| } |
| |
| // nextID generates sequence numbers for each unit of work. |
| // We use it to create names of temporary files. |
| var nextID atomic.Int32 |
| |
| var allDiagnostics []string |
| |
| // Visit all packages in postorder: dependencies first. |
| // TODO(adonovan): opt: use parallel postorder. |
| packages.Visit(pkgs, nil, func(pkg *packages.Package) { |
| if pkg.PkgPath == "unsafe" { |
| return |
| } |
| |
| // Choose a unique prefix for temporary files |
| // (.cfg .types .facts) produced by this package. |
| // We stow it in an otherwise unused field of |
| // Package so it can be accessed by our importers. |
| prefix := fmt.Sprintf("%s/%d", tmpdir, nextID.Add(1)) |
| pkg.ExportFile = prefix |
| |
| // Construct the request to the worker. |
| var ( |
| importMap = make(map[string]string) |
| packageFile = make(map[string]string) |
| packageVetx = make(map[string]string) |
| ) |
| for importPath, dep := range pkg.Imports { |
| importMap[importPath] = dep.PkgPath |
| if depPrefix := dep.ExportFile; depPrefix != "" { // skip "unsafe" |
| packageFile[dep.PkgPath] = depPrefix + ".types" |
| packageVetx[dep.PkgPath] = depPrefix + ".facts" |
| } |
| } |
| cfg := unitchecker.Config{ |
| ID: pkg.ID, |
| ImportPath: pkg.PkgPath, |
| GoFiles: pkg.CompiledGoFiles, |
| NonGoFiles: pkg.OtherFiles, |
| IgnoredFiles: pkg.IgnoredFiles, |
| ImportMap: importMap, |
| PackageFile: packageFile, |
| PackageVetx: packageVetx, |
| VetxOnly: !roots[pkg], |
| VetxOutput: prefix + ".facts", |
| } |
| if pkg.Module != nil { |
| if v := pkg.Module.GoVersion; v != "" { |
| cfg.GoVersion = "go" + v |
| } |
| cfg.ModulePath = pkg.Module.Path |
| cfg.ModuleVersion = pkg.Module.Version |
| } |
| |
| // Write the JSON configuration message to a file. |
| cfgData, err := json.Marshal(cfg) |
| if err != nil { |
| t.Fatalf("internal error in json.Marshal: %v", err) |
| } |
| cfgFile := prefix + ".cfg" |
| if err := os.WriteFile(cfgFile, cfgData, 0666); err != nil { |
| t.Fatal(err) |
| } |
| |
| // Send the request to the worker. |
| cmd := testenv.Command(t, os.Args[0], "-json", cfgFile) |
| cmd.Stderr = os.Stderr |
| cmd.Stdout = new(bytes.Buffer) |
| cmd.Env = append(os.Environ(), "ENTRYPOINT=worker") |
| if err := cmd.Run(); err != nil { |
| t.Fatal(err) |
| } |
| |
| // Parse JSON output and gather in allDiagnostics. |
| dec := json.NewDecoder(cmd.Stdout.(io.Reader)) |
| for { |
| type jsonDiagnostic struct { |
| Posn string `json:"posn"` |
| Message string `json:"message"` |
| } |
| // 'results' maps Package.Path -> Analyzer.Name -> diagnostics |
| var results map[string]map[string][]jsonDiagnostic |
| if err := dec.Decode(&results); err != nil { |
| if err == io.EOF { |
| break |
| } |
| t.Fatalf("internal error decoding JSON: %v", err) |
| } |
| for _, result := range results { |
| for analyzer, diags := range result { |
| for _, diag := range diags { |
| rel := strings.ReplaceAll(diag.Posn, tmpdir, "") |
| rel = filepath.ToSlash(rel) |
| msg := fmt.Sprintf("%s: [%s] %s", rel, analyzer, diag.Message) |
| allDiagnostics = append(allDiagnostics, msg) |
| } |
| } |
| } |
| } |
| }) |
| |
| // Observe that the example produces a fact-based diagnostic |
| // from separate analysis of "main", "lib", and "fmt": |
| |
| const want = `/main/main.go:6:2: [printf] separate/lib.MyPrintf format %s has arg 123 of wrong type int` |
| sort.Strings(allDiagnostics) |
| if got := strings.Join(allDiagnostics, "\n"); got != want { |
| t.Errorf("Got: %s\nWant: %s", got, want) |
| } |
| } |
| |
| // -- worker process -- |
| |
| // worker is the main entry point for a unitchecker-based driver |
| // with only a single analyzer, for illustration. |
| func worker() { |
| // Currently the unitchecker API doesn't allow clients to |
| // control exactly how and where fact and type information |
| // is produced and consumed. |
| // |
| // So, for example, it assumes that type information has |
| // already been produced by the compiler, which is true when |
| // running under "go vet", but isn't necessary. It may be more |
| // convenient and efficient for a distributed analysis system |
| // if the worker generates both of them, which is the approach |
| // taken in this example; they could even be saved as two |
| // sections of a single file. |
| // |
| // Consequently, this test currently needs special access to |
| // private hooks in unitchecker to control how and where facts |
| // and types are produced and consumed. In due course this |
| // will become a respectable public API. In the meantime, it |
| // should at least serve as a demonstration of how one could |
| // fork unitchecker to achieve separate analysis without go vet. |
| unitchecker.SetTypeImportExport(makeTypesImporter, exportTypes) |
| |
| unitchecker.Main(printf.Analyzer) |
| } |
| |
| func makeTypesImporter(cfg *unitchecker.Config, fset *token.FileSet) types.Importer { |
| imports := make(map[string]*types.Package) |
| return importerFunc(func(importPath string) (*types.Package, error) { |
| // Resolve import path to package path (vendoring, etc) |
| path, ok := cfg.ImportMap[importPath] |
| if !ok { |
| return nil, fmt.Errorf("can't resolve import %q", path) |
| } |
| if path == "unsafe" { |
| return types.Unsafe, nil |
| } |
| |
| // Find, read, and decode file containing type information. |
| file, ok := cfg.PackageFile[path] |
| if !ok { |
| return nil, fmt.Errorf("no package file for %q", path) |
| } |
| f, err := os.Open(file) |
| if err != nil { |
| return nil, err |
| } |
| defer f.Close() // ignore error |
| return gcexportdata.Read(f, fset, imports, path) |
| }) |
| } |
| |
| func exportTypes(cfg *unitchecker.Config, fset *token.FileSet, pkg *types.Package) error { |
| var out bytes.Buffer |
| if err := gcexportdata.Write(&out, fset, pkg); err != nil { |
| return err |
| } |
| typesFile := strings.TrimSuffix(cfg.VetxOutput, ".facts") + ".types" |
| return os.WriteFile(typesFile, out.Bytes(), 0666) |
| } |
| |
| // -- helpers -- |
| |
| type importerFunc func(path string) (*types.Package, error) |
| |
| func (f importerFunc) Import(path string) (*types.Package, error) { return f(path) } |