...

Source file src/cmd/vendor/golang.org/x/tools/go/analysis/unitchecker/unitchecker.go

Documentation: cmd/vendor/golang.org/x/tools/go/analysis/unitchecker

     1  // Copyright 2018 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // The unitchecker package defines the main function for an analysis
     6  // driver that analyzes a single compilation unit during a build.
     7  // It is invoked by a build system such as "go vet":
     8  //
     9  //	$ go vet -vettool=$(which vet)
    10  //
    11  // It supports the following command-line protocol:
    12  //
    13  //	-V=full         describe executable               (to the build tool)
    14  //	-flags          describe flags                    (to the build tool)
    15  //	foo.cfg         description of compilation unit (from the build tool)
    16  //
    17  // This package does not depend on go/packages.
    18  // If you need a standalone tool, use multichecker,
    19  // which supports this mode but can also load packages
    20  // from source using go/packages.
    21  package unitchecker
    22  
    23  // TODO(adonovan):
    24  // - with gccgo, go build does not build standard library,
    25  //   so we will not get to analyze it. Yet we must in order
    26  //   to create base facts for, say, the fmt package for the
    27  //   printf checker.
    28  
    29  import (
    30  	"archive/zip"
    31  	"encoding/gob"
    32  	"encoding/json"
    33  	"flag"
    34  	"fmt"
    35  	"go/ast"
    36  	"go/build"
    37  	"go/importer"
    38  	"go/parser"
    39  	"go/token"
    40  	"go/types"
    41  	"io"
    42  	"log"
    43  	"os"
    44  	"path/filepath"
    45  	"reflect"
    46  	"sort"
    47  	"strings"
    48  	"sync"
    49  	"time"
    50  
    51  	"golang.org/x/tools/go/analysis"
    52  	"golang.org/x/tools/go/analysis/internal/analysisflags"
    53  	"golang.org/x/tools/internal/analysis/driverutil"
    54  	"golang.org/x/tools/internal/facts"
    55  )
    56  
    57  // A Config describes a compilation unit to be analyzed.
    58  // It is provided to the tool in a JSON-encoded file
    59  // whose name ends with ".cfg".
    60  type Config struct {
    61  	ID                        string // e.g. "fmt [fmt.test]"
    62  	Compiler                  string // gc or gccgo, provided to MakeImporter
    63  	Dir                       string // (unused)
    64  	ImportPath                string // package path
    65  	GoVersion                 string // minimum required Go version, such as "go1.21.0"
    66  	GoFiles                   []string
    67  	NonGoFiles                []string
    68  	IgnoredFiles              []string
    69  	ModulePath                string            // Deprecated: redundant w.r.t. Module.Path in go1.27; remove after go1.28.
    70  	ModuleVersion             string            // Deprecated: redundant w.r.t. Module.Version in go1.27; remove after go1.28.
    71  	Module                    *analysis.Module  // module information, if any
    72  	ImportMap                 map[string]string // maps import path to package path
    73  	PackageFile               map[string]string // maps package path to file of type information
    74  	Standard                  map[string]bool   // package belongs to standard library
    75  	PackageVetx               map[string]string // maps package path to file of fact information
    76  	VetxOnly                  bool              // run analysis only for facts, not diagnostics
    77  	VetxOutput                string            // where to write file of fact information
    78  	Stdout                    string            // write stdout (e.g. JSON, unified diff) to this file
    79  	FixArchive                string            // write fixed files to this zip archive, if non-empty
    80  	SucceedOnTypecheckFailure bool              // obsolete awful hack; see #18395 and below
    81  }
    82  
    83  // Main is the main function of a vet-like analysis tool that must be
    84  // invoked by a build system to analyze a single package.
    85  //
    86  // The protocol required by 'go vet -vettool=...' is that the tool must support:
    87  //
    88  //	-flags          describe flags in JSON
    89  //	-V=full         describe executable for build caching
    90  //	foo.cfg         perform separate modular analyze on the single
    91  //	                unit described by a JSON config file foo.cfg.
    92  //	-fix		don't print each diagnostic, apply its first fix
    93  //	-diff		don't apply a fix, print the diff (requires -fix)
    94  //	-json		print diagnostics and fixes in JSON form
    95  func Main(analyzers ...*analysis.Analyzer) {
    96  	progname := filepath.Base(os.Args[0])
    97  	log.SetFlags(0)
    98  	log.SetPrefix(progname + ": ")
    99  
   100  	if err := analysis.Validate(analyzers); err != nil {
   101  		log.Fatal(err)
   102  	}
   103  
   104  	flag.Usage = func() {
   105  		fmt.Fprintf(os.Stderr, `%[1]s is a tool for static analysis of Go programs.
   106  
   107  Usage of %[1]s:
   108  	%.16[1]s unit.cfg	# execute analysis specified by config file
   109  	%.16[1]s help    	# general help, including listing analyzers and flags
   110  	%.16[1]s help name	# help on specific analyzer and its flags
   111  `, progname)
   112  		os.Exit(1)
   113  	}
   114  
   115  	analyzers = analysisflags.Parse(analyzers, true)
   116  
   117  	args := flag.Args()
   118  	if len(args) == 0 {
   119  		flag.Usage()
   120  	}
   121  	if args[0] == "help" {
   122  		analysisflags.Help(progname, analyzers, args[1:])
   123  		os.Exit(0)
   124  	}
   125  	if len(args) != 1 || !strings.HasSuffix(args[0], ".cfg") {
   126  		log.Fatalf(`invoking "go tool %[1]s" directly is unsupported; use "go %[1]s"`, progname)
   127  	}
   128  	Run(args[0], analyzers)
   129  }
   130  
   131  // Run reads the *.cfg file, runs the analysis,
   132  // and calls os.Exit with an appropriate error code.
   133  // It assumes flags have already been set.
   134  func Run(configFile string, analyzers []*analysis.Analyzer) {
   135  	cfg, err := readConfig(configFile)
   136  	if err != nil {
   137  		log.Fatal(err)
   138  	}
   139  
   140  	// Redirect stdout to a file as requested.
   141  	if cfg.Stdout != "" {
   142  		f, err := os.Create(cfg.Stdout)
   143  		if err != nil {
   144  			log.Fatal(err)
   145  		}
   146  		os.Stdout = f
   147  	}
   148  
   149  	fset := token.NewFileSet()
   150  	results, err := run(fset, cfg, analyzers)
   151  	if err != nil {
   152  		log.Fatal(err)
   153  	}
   154  
   155  	code := 0
   156  
   157  	// In VetxOnly mode, the analysis is run only for facts.
   158  	if !cfg.VetxOnly {
   159  		code = processResults(fset, cfg.ID, cfg.FixArchive, results)
   160  	}
   161  
   162  	os.Exit(code)
   163  }
   164  
   165  func readConfig(filename string) (*Config, error) {
   166  	data, err := os.ReadFile(filename)
   167  	if err != nil {
   168  		return nil, err
   169  	}
   170  	cfg := new(Config)
   171  	if err := json.Unmarshal(data, cfg); err != nil {
   172  		return nil, fmt.Errorf("cannot decode JSON config file %s: %v", filename, err)
   173  	}
   174  	if len(cfg.GoFiles) == 0 {
   175  		// The go command disallows packages with no files.
   176  		// The only exception is unsafe, but the go command
   177  		// doesn't call vet on it.
   178  		return nil, fmt.Errorf("package has no files: %s", cfg.ImportPath)
   179  	}
   180  	return cfg, nil
   181  }
   182  
   183  func processResults(fset *token.FileSet, id, fixArchive string, results []result) (exit int) {
   184  	if analysisflags.Fix {
   185  		// Don't print the diagnostics,
   186  		// but apply all fixes from the root actions.
   187  
   188  		// Convert results to form needed by ApplyFixes.
   189  		fixActions := make([]driverutil.FixAction, len(results))
   190  		for i, res := range results {
   191  			fixActions[i] = driverutil.FixAction{
   192  				Name:         res.a.Name,
   193  				Pkg:          res.pkg,
   194  				Files:        res.files,
   195  				FileSet:      fset,
   196  				ReadFileFunc: os.ReadFile, // TODO(adonovan): respect overlays
   197  				Diagnostics:  res.diagnostics,
   198  			}
   199  		}
   200  
   201  		// By default, fixes overwrite the original file.
   202  		// With the -diff flag, print the diffs to stdout.
   203  		// If "go fix" provides a fix archive, we write files
   204  		// into it so that mutations happen after the build.
   205  		write := func(filename string, content []byte) error {
   206  			return os.WriteFile(filename, content, 0644)
   207  		}
   208  		if fixArchive != "" {
   209  			f, err := os.Create(fixArchive)
   210  			if err != nil {
   211  				log.Fatalf("can't create -fix archive: %v", err)
   212  			}
   213  			zw := zip.NewWriter(f)
   214  			zw.SetComment(id) // ignore error
   215  			defer func() {
   216  				if err := zw.Close(); err != nil {
   217  					log.Fatalf("closing -fix archive zip writer: %v", err)
   218  				}
   219  				if err := f.Close(); err != nil {
   220  					log.Fatalf("closing -fix archive file: %v", err)
   221  				}
   222  			}()
   223  			write = func(filename string, content []byte) error {
   224  				f, err := zw.Create(filename)
   225  				if err != nil {
   226  					return err
   227  				}
   228  				_, err = f.Write(content)
   229  				return err
   230  			}
   231  		}
   232  
   233  		if err := driverutil.ApplyFixes(fixActions, write, analysisflags.Diff, false); err != nil {
   234  			// Fail when applying fixes failed.
   235  			log.Print(err)
   236  			exit = 1
   237  		}
   238  
   239  		// Don't proceed to print text/JSON,
   240  		// and don't report an error
   241  		// just because there were diagnostics.
   242  		return
   243  	}
   244  
   245  	// Keep consistent with analogous logic in
   246  	// printDiagnostics in ../internal/checker/checker.go.
   247  
   248  	if analysisflags.JSON {
   249  		// JSON output
   250  		tree := make(driverutil.JSONTree)
   251  		for _, res := range results {
   252  			tree.Add(fset, id, res.a.Name, res.diagnostics, res.err)
   253  		}
   254  		tree.Print(os.Stdout) // ignore error
   255  
   256  	} else {
   257  		// plain text
   258  		for _, res := range results {
   259  			if res.err != nil {
   260  				log.Println(res.err)
   261  				exit = 1
   262  			}
   263  		}
   264  		for _, res := range results {
   265  			for _, diag := range res.diagnostics {
   266  				driverutil.PrintPlain(os.Stderr, fset, analysisflags.Context, diag)
   267  				exit = 1
   268  			}
   269  		}
   270  	}
   271  
   272  	return
   273  }
   274  
   275  type factImporter = func(pkgPath string) ([]byte, error)
   276  
   277  // These four hook variables are a proof of concept of a future
   278  // parameterization of a unitchecker API that allows the client to
   279  // determine how and where facts and types are produced and consumed.
   280  // (Note that the eventual API will likely be quite different.)
   281  //
   282  // The defaults honor a Config in a manner compatible with 'go vet'.
   283  var (
   284  	makeTypesImporter = func(cfg *Config, fset *token.FileSet) types.Importer {
   285  		compilerImporter := importer.ForCompiler(fset, cfg.Compiler, func(path string) (io.ReadCloser, error) {
   286  			// path is a resolved package path, not an import path.
   287  			file, ok := cfg.PackageFile[path]
   288  			if !ok {
   289  				if cfg.Compiler == "gccgo" && cfg.Standard[path] {
   290  					return nil, nil // fall back to default gccgo lookup
   291  				}
   292  				return nil, fmt.Errorf("no package file for %q", path)
   293  			}
   294  			return os.Open(file)
   295  		})
   296  		return importerFunc(func(importPath string) (*types.Package, error) {
   297  			path, ok := cfg.ImportMap[importPath] // resolve vendoring, etc
   298  			if !ok {
   299  				return nil, fmt.Errorf("can't resolve import %q", path)
   300  			}
   301  			return compilerImporter.Import(path)
   302  		})
   303  	}
   304  
   305  	exportTypes = func(*Config, *token.FileSet, *types.Package) error {
   306  		// By default this is a no-op, because "go vet"
   307  		// makes the compiler produce type information.
   308  		return nil
   309  	}
   310  
   311  	makeFactImporter = func(cfg *Config) factImporter {
   312  		return func(pkgPath string) ([]byte, error) {
   313  			if vetx, ok := cfg.PackageVetx[pkgPath]; ok {
   314  				return os.ReadFile(vetx)
   315  			}
   316  			return nil, nil // no .vetx file, no facts
   317  		}
   318  	}
   319  
   320  	exportFacts = func(cfg *Config, data []byte) error {
   321  		return os.WriteFile(cfg.VetxOutput, data, 0666)
   322  	}
   323  )
   324  
   325  func run(fset *token.FileSet, cfg *Config, analyzers []*analysis.Analyzer) ([]result, error) {
   326  	// Load, parse, typecheck.
   327  	var files []*ast.File
   328  	for _, name := range cfg.GoFiles {
   329  		f, err := parser.ParseFile(fset, name, nil, parser.ParseComments)
   330  		if err != nil {
   331  			if cfg.SucceedOnTypecheckFailure {
   332  				// Silently succeed; let the compiler
   333  				// report parse errors.
   334  				err = nil
   335  			}
   336  			return nil, err
   337  		}
   338  		files = append(files, f)
   339  	}
   340  	tc := &types.Config{
   341  		Importer:  makeTypesImporter(cfg, fset),
   342  		Sizes:     types.SizesFor("gc", build.Default.GOARCH), // TODO(adonovan): use cfg.Compiler
   343  		GoVersion: cfg.GoVersion,
   344  	}
   345  	info := &types.Info{
   346  		Types:        make(map[ast.Expr]types.TypeAndValue),
   347  		Defs:         make(map[*ast.Ident]types.Object),
   348  		Uses:         make(map[*ast.Ident]types.Object),
   349  		Implicits:    make(map[ast.Node]types.Object),
   350  		Instances:    make(map[*ast.Ident]types.Instance),
   351  		Scopes:       make(map[ast.Node]*types.Scope),
   352  		Selections:   make(map[*ast.SelectorExpr]*types.Selection),
   353  		FileVersions: make(map[*ast.File]string),
   354  	}
   355  
   356  	pkg, err := tc.Check(cfg.ImportPath, fset, files, info)
   357  	if err != nil {
   358  		if cfg.SucceedOnTypecheckFailure {
   359  			// Silently succeed; let the compiler
   360  			// report type errors.
   361  			err = nil
   362  		}
   363  		return nil, err
   364  	}
   365  
   366  	// Register fact types with gob.
   367  	// In VetxOnly mode, analyzers are only for their facts,
   368  	// so we can skip any analysis that neither produces facts
   369  	// nor depends on any analysis that produces facts.
   370  	//
   371  	// TODO(adonovan): fix: the command (and logic!) here are backwards.
   372  	// It should say "...nor is required by any...". (Issue 443099)
   373  	//
   374  	// Also build a map to hold working state and result.
   375  	type action struct {
   376  		once        sync.Once
   377  		result      any
   378  		err         error
   379  		usesFacts   bool // (transitively uses)
   380  		diagnostics []analysis.Diagnostic
   381  	}
   382  	actions := make(map[*analysis.Analyzer]*action)
   383  	var registerFacts func(a *analysis.Analyzer) bool
   384  	registerFacts = func(a *analysis.Analyzer) bool {
   385  		act, ok := actions[a]
   386  		if !ok {
   387  			act = new(action)
   388  			var usesFacts bool
   389  			for _, f := range a.FactTypes {
   390  				usesFacts = true
   391  				gob.Register(f)
   392  			}
   393  			for _, req := range a.Requires {
   394  				if registerFacts(req) {
   395  					usesFacts = true
   396  				}
   397  			}
   398  			act.usesFacts = usesFacts
   399  			actions[a] = act
   400  		}
   401  		return act.usesFacts
   402  	}
   403  	var filtered []*analysis.Analyzer
   404  	for _, a := range analyzers {
   405  		if registerFacts(a) || !cfg.VetxOnly {
   406  			filtered = append(filtered, a)
   407  		}
   408  	}
   409  	analyzers = filtered
   410  
   411  	// Read facts from imported packages.
   412  	facts, err := facts.NewDecoder(pkg).Decode(makeFactImporter(cfg))
   413  	if err != nil {
   414  		return nil, err
   415  	}
   416  
   417  	// In parallel, execute the DAG of analyzers.
   418  	var exec func(a *analysis.Analyzer) *action
   419  	var execAll func(analyzers []*analysis.Analyzer)
   420  	exec = func(a *analysis.Analyzer) *action {
   421  		act := actions[a]
   422  		act.once.Do(func() {
   423  			execAll(a.Requires) // prefetch dependencies in parallel
   424  
   425  			// The inputs to this analysis are the
   426  			// results of its prerequisites.
   427  			inputs := make(map[*analysis.Analyzer]any)
   428  			var failed []string
   429  			for _, req := range a.Requires {
   430  				reqact := exec(req)
   431  				if reqact.err != nil {
   432  					failed = append(failed, req.String())
   433  					continue
   434  				}
   435  				inputs[req] = reqact.result
   436  			}
   437  
   438  			// Report an error if any dependency failed.
   439  			if failed != nil {
   440  				sort.Strings(failed)
   441  				act.err = fmt.Errorf("failed prerequisites: %s", strings.Join(failed, ", "))
   442  				return
   443  			}
   444  
   445  			factFilter := make(map[reflect.Type]bool)
   446  			for _, f := range a.FactTypes {
   447  				factFilter[reflect.TypeOf(f)] = true
   448  			}
   449  
   450  			module := cfg.Module
   451  			// cmd/go vet prior to go1.27 did not populate cfg.Module. Do our best.
   452  			if module == nil && cfg.ModulePath != "" {
   453  				module = &analysis.Module{
   454  					Path:      cfg.ModulePath,
   455  					Version:   cfg.ModuleVersion,
   456  					GoVersion: cfg.GoVersion,
   457  				}
   458  			}
   459  
   460  			pass := &analysis.Pass{
   461  				Analyzer:     a,
   462  				Fset:         fset,
   463  				Files:        files,
   464  				OtherFiles:   cfg.NonGoFiles,
   465  				IgnoredFiles: cfg.IgnoredFiles,
   466  				Pkg:          pkg,
   467  				TypesInfo:    info,
   468  				TypesSizes:   tc.Sizes,
   469  				TypeErrors:   nil, // unitchecker doesn't RunDespiteErrors
   470  				ResultOf:     inputs,
   471  				Report: func(d analysis.Diagnostic) {
   472  					// Unitchecker doesn't apply fixes, but it does report them in the JSON output.
   473  					if err := driverutil.ValidateFixes(fset, a, d.SuggestedFixes); err != nil {
   474  						// Since we have diagnostics, the exit code will be nonzero,
   475  						// so logging these errors is sufficient.
   476  						log.Println(err)
   477  						d.SuggestedFixes = nil
   478  					}
   479  					act.diagnostics = append(act.diagnostics, d)
   480  				},
   481  				ImportObjectFact:  facts.ImportObjectFact,
   482  				ExportObjectFact:  facts.ExportObjectFact,
   483  				AllObjectFacts:    func() []analysis.ObjectFact { return facts.AllObjectFacts(factFilter) },
   484  				ImportPackageFact: facts.ImportPackageFact,
   485  				ExportPackageFact: facts.ExportPackageFact,
   486  				AllPackageFacts:   func() []analysis.PackageFact { return facts.AllPackageFacts(factFilter) },
   487  				Module:            module,
   488  			}
   489  			pass.ReadFile = driverutil.CheckedReadFile(pass, os.ReadFile)
   490  
   491  			t0 := time.Now()
   492  			act.result, act.err = a.Run(pass)
   493  
   494  			if act.err == nil { // resolve URLs on diagnostics.
   495  				for i := range act.diagnostics {
   496  					if url, uerr := driverutil.ResolveURL(a, act.diagnostics[i]); uerr == nil {
   497  						act.diagnostics[i].URL = url
   498  					} else {
   499  						act.err = uerr // keep the last error
   500  					}
   501  				}
   502  			}
   503  			if false {
   504  				log.Printf("analysis %s = %s", pass, time.Since(t0))
   505  			}
   506  		})
   507  		return act
   508  	}
   509  	execAll = func(analyzers []*analysis.Analyzer) {
   510  		var wg sync.WaitGroup
   511  		for _, a := range analyzers {
   512  			wg.Add(1)
   513  			go func(a *analysis.Analyzer) {
   514  				_ = exec(a)
   515  				wg.Done()
   516  			}(a)
   517  		}
   518  		wg.Wait()
   519  	}
   520  
   521  	execAll(analyzers)
   522  
   523  	// Return diagnostics and errors from root analyzers.
   524  	results := make([]result, len(analyzers))
   525  	for i, a := range analyzers {
   526  		act := actions[a]
   527  		results[i] = result{pkg, files, a, act.diagnostics, act.err}
   528  	}
   529  
   530  	data := facts.Encode()
   531  	if err := exportFacts(cfg, data); err != nil {
   532  		return nil, fmt.Errorf("failed to export analysis facts: %v", err)
   533  	}
   534  	if err := exportTypes(cfg, fset, pkg); err != nil {
   535  		return nil, fmt.Errorf("failed to export type information: %v", err)
   536  	}
   537  
   538  	return results, nil
   539  }
   540  
   541  type result struct {
   542  	pkg         *types.Package
   543  	files       []*ast.File
   544  	a           *analysis.Analyzer
   545  	diagnostics []analysis.Diagnostic
   546  	err         error
   547  }
   548  
   549  type importerFunc func(path string) (*types.Package, error)
   550  
   551  func (f importerFunc) Import(path string) (*types.Package, error) { return f(path) }
   552  

View as plain text