// 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" "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. tmpdir := t.TempDir() if err := testfiles.ExtractTxtar(tmpdir, txtar.Parse([]byte(src))); err != nil { t.Fatal(err) } // 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 } } // 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` 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) }