Source file src/go/types/gotype.go

     1  // Copyright 2011 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  //go:build ignore
     6  
     7  // Build this command explicitly: go build gotype.go
     8  
     9  /*
    10  The gotype command, like the front-end of a Go compiler, parses and
    11  type-checks a single Go package. Errors are reported if the analysis
    12  fails; otherwise gotype is quiet (unless -v is set).
    13  
    14  Without a list of paths, gotype reads from standard input, which
    15  must provide a single Go source file defining a complete package.
    16  
    17  With a single directory argument, gotype checks the Go files in
    18  that directory, comprising a single package. Use -t to include the
    19  (in-package) _test.go files. Use -x to type check only external
    20  test files.
    21  
    22  Otherwise, each path must be the filename of a Go file belonging
    23  to the same package.
    24  
    25  Imports are processed by importing directly from the source of
    26  imported packages (default), or by importing from compiled and
    27  installed packages (by setting -c to the respective compiler).
    28  
    29  The -c flag must be set to a compiler ("gc", "gccgo") when type-
    30  checking packages containing imports with relative import paths
    31  (import "./mypkg") because the source importer cannot know which
    32  files to include for such packages.
    33  
    34  Usage:
    35  
    36  	gotype [flags] [path...]
    37  
    38  The flags are:
    39  
    40  	-t
    41  		include local test files in a directory (ignored if -x is provided)
    42  	-x
    43  		consider only external test files in a directory
    44  	-e
    45  		report all errors (not just the first 10)
    46  	-v
    47  		verbose mode
    48  	-c
    49  		compiler used for installed packages (gc, gccgo, or source); default: source
    50  
    51  Flags controlling additional output:
    52  
    53  	-ast
    54  		print AST
    55  	-trace
    56  		print parse trace
    57  	-comments
    58  		parse comments (ignored unless -ast or -trace is provided)
    59  	-panic
    60  		panic on first error
    61  
    62  Examples:
    63  
    64  To check the files a.go, b.go, and c.go:
    65  
    66  	gotype a.go b.go c.go
    67  
    68  To check an entire package including (in-package) tests in the directory dir and print the processed files:
    69  
    70  	gotype -t -v dir
    71  
    72  To check the external test package (if any) in the current directory, based on installed packages compiled with
    73  cmd/compile:
    74  
    75  	gotype -c=gc -x .
    76  
    77  To verify the output of a pipe:
    78  
    79  	echo "package foo" | gotype
    80  */
    81  package main
    82  
    83  import (
    84  	"flag"
    85  	"fmt"
    86  	"go/ast"
    87  	"go/build"
    88  	"go/importer"
    89  	"go/parser"
    90  	"go/scanner"
    91  	"go/token"
    92  	"go/types"
    93  	"io"
    94  	"os"
    95  	"path/filepath"
    96  	"sync"
    97  	"time"
    98  )
    99  
   100  var (
   101  	// main operation modes
   102  	testFiles  = flag.Bool("t", false, "include in-package test files in a directory")
   103  	xtestFiles = flag.Bool("x", false, "consider only external test files in a directory")
   104  	allErrors  = flag.Bool("e", false, "report all errors, not just the first 10")
   105  	verbose    = flag.Bool("v", false, "verbose mode")
   106  	compiler   = flag.String("c", "source", "compiler used for installed packages (gc, gccgo, or source)")
   107  
   108  	// additional output control
   109  	printAST      = flag.Bool("ast", false, "print AST")
   110  	printTrace    = flag.Bool("trace", false, "print parse trace")
   111  	parseComments = flag.Bool("comments", false, "parse comments (ignored unless -ast or -trace is provided)")
   112  	panicOnError  = flag.Bool("panic", false, "panic on first error")
   113  )
   114  
   115  var (
   116  	fset       = token.NewFileSet()
   117  	errorCount = 0
   118  	sequential = false
   119  	parserMode parser.Mode
   120  )
   121  
   122  func initParserMode() {
   123  	parserMode = parser.SkipObjectResolution
   124  	if *allErrors {
   125  		parserMode |= parser.AllErrors
   126  	}
   127  	if *printAST {
   128  		sequential = true
   129  	}
   130  	if *printTrace {
   131  		parserMode |= parser.Trace
   132  		sequential = true
   133  	}
   134  	if *parseComments && (*printAST || *printTrace) {
   135  		parserMode |= parser.ParseComments
   136  	}
   137  }
   138  
   139  const usageString = `usage: gotype [flags] [path ...]
   140  
   141  The gotype command, like the front-end of a Go compiler, parses and
   142  type-checks a single Go package. Errors are reported if the analysis
   143  fails; otherwise gotype is quiet (unless -v is set).
   144  
   145  Without a list of paths, gotype reads from standard input, which
   146  must provide a single Go source file defining a complete package.
   147  
   148  With a single directory argument, gotype checks the Go files in
   149  that directory, comprising a single package. Use -t to include the
   150  (in-package) _test.go files. Use -x to type check only external
   151  test files.
   152  
   153  Otherwise, each path must be the filename of a Go file belonging
   154  to the same package.
   155  
   156  Imports are processed by importing directly from the source of
   157  imported packages (default), or by importing from compiled and
   158  installed packages (by setting -c to the respective compiler).
   159  
   160  The -c flag must be set to a compiler ("gc", "gccgo") when type-
   161  checking packages containing imports with relative import paths
   162  (import "./mypkg") because the source importer cannot know which
   163  files to include for such packages.
   164  `
   165  
   166  func usage() {
   167  	fmt.Fprintln(os.Stderr, usageString)
   168  	flag.PrintDefaults()
   169  	os.Exit(2)
   170  }
   171  
   172  func report(err error) {
   173  	if *panicOnError {
   174  		panic(err)
   175  	}
   176  	scanner.PrintError(os.Stderr, err)
   177  	if list, ok := err.(scanner.ErrorList); ok {
   178  		errorCount += len(list)
   179  		return
   180  	}
   181  	errorCount++
   182  }
   183  
   184  // parse may be called concurrently.
   185  func parse(filename string, src any) (*ast.File, error) {
   186  	if *verbose {
   187  		fmt.Println(filename)
   188  	}
   189  	file, err := parser.ParseFile(fset, filename, src, parserMode) // ok to access fset concurrently
   190  	if *printAST {
   191  		ast.Print(fset, file)
   192  	}
   193  	return file, err
   194  }
   195  
   196  func parseStdin() (*ast.File, error) {
   197  	src, err := io.ReadAll(os.Stdin)
   198  	if err != nil {
   199  		return nil, err
   200  	}
   201  	return parse("<standard input>", src)
   202  }
   203  
   204  func parseFiles(dir string, filenames []string) ([]*ast.File, error) {
   205  	files := make([]*ast.File, len(filenames))
   206  	errors := make([]error, len(filenames))
   207  
   208  	var wg sync.WaitGroup
   209  	for i, filename := range filenames {
   210  		wg.Add(1)
   211  		go func(i int, filepath string) {
   212  			defer wg.Done()
   213  			files[i], errors[i] = parse(filepath, nil)
   214  		}(i, filepath.Join(dir, filename))
   215  		if sequential {
   216  			wg.Wait()
   217  		}
   218  	}
   219  	wg.Wait()
   220  
   221  	// If there are errors, return the first one for deterministic results.
   222  	var first error
   223  	for _, err := range errors {
   224  		if err != nil {
   225  			first = err
   226  			// If we have an error, some files may be nil.
   227  			// Remove them. (The go/parser always returns
   228  			// a possibly partial AST even in the presence
   229  			// of errors, except if the file doesn't exist
   230  			// in the first place, in which case it cannot
   231  			// matter.)
   232  			i := 0
   233  			for _, f := range files {
   234  				if f != nil {
   235  					files[i] = f
   236  					i++
   237  				}
   238  			}
   239  			files = files[:i]
   240  			break
   241  		}
   242  	}
   243  
   244  	return files, first
   245  }
   246  
   247  func parseDir(dir string) ([]*ast.File, error) {
   248  	ctxt := build.Default
   249  	pkginfo, err := ctxt.ImportDir(dir, 0)
   250  	if _, nogo := err.(*build.NoGoError); err != nil && !nogo {
   251  		return nil, err
   252  	}
   253  
   254  	if *xtestFiles {
   255  		return parseFiles(dir, pkginfo.XTestGoFiles)
   256  	}
   257  
   258  	filenames := append(pkginfo.GoFiles, pkginfo.CgoFiles...)
   259  	if *testFiles {
   260  		filenames = append(filenames, pkginfo.TestGoFiles...)
   261  	}
   262  	return parseFiles(dir, filenames)
   263  }
   264  
   265  func getPkgFiles(args []string) ([]*ast.File, error) {
   266  	if len(args) == 0 {
   267  		// stdin
   268  		file, err := parseStdin()
   269  		if err != nil {
   270  			return nil, err
   271  		}
   272  		return []*ast.File{file}, nil
   273  	}
   274  
   275  	if len(args) == 1 {
   276  		// possibly a directory
   277  		path := args[0]
   278  		info, err := os.Stat(path)
   279  		if err != nil {
   280  			return nil, err
   281  		}
   282  		if info.IsDir() {
   283  			return parseDir(path)
   284  		}
   285  	}
   286  
   287  	// list of files
   288  	return parseFiles("", args)
   289  }
   290  
   291  func checkPkgFiles(files []*ast.File) {
   292  	type bailout struct{}
   293  
   294  	// if checkPkgFiles is called multiple times, set up conf only once
   295  	conf := types.Config{
   296  		FakeImportC: true,
   297  		Error: func(err error) {
   298  			if !*allErrors && errorCount >= 10 {
   299  				panic(bailout{})
   300  			}
   301  			report(err)
   302  		},
   303  		Importer: importer.ForCompiler(fset, *compiler, nil),
   304  		Sizes:    types.SizesFor(build.Default.Compiler, build.Default.GOARCH),
   305  	}
   306  
   307  	defer func() {
   308  		switch p := recover().(type) {
   309  		case nil, bailout:
   310  			// normal return or early exit
   311  		default:
   312  			// re-panic
   313  			panic(p)
   314  		}
   315  	}()
   316  
   317  	const path = "pkg" // any non-empty string will do for now
   318  	conf.Check(path, fset, files, nil)
   319  }
   320  
   321  func printStats(d time.Duration) {
   322  	fileCount := 0
   323  	lineCount := 0
   324  	fset.Iterate(func(f *token.File) bool {
   325  		fileCount++
   326  		lineCount += f.LineCount()
   327  		return true
   328  	})
   329  
   330  	fmt.Printf(
   331  		"%s (%d files, %d lines, %d lines/s)\n",
   332  		d, fileCount, lineCount, int64(float64(lineCount)/d.Seconds()),
   333  	)
   334  }
   335  
   336  func main() {
   337  	flag.Usage = usage
   338  	flag.Parse()
   339  	initParserMode()
   340  
   341  	start := time.Now()
   342  
   343  	files, err := getPkgFiles(flag.Args())
   344  	if err != nil {
   345  		report(err)
   346  		// ok to continue (files may be empty, but not nil)
   347  	}
   348  
   349  	checkPkgFiles(files)
   350  	if errorCount > 0 {
   351  		os.Exit(2)
   352  	}
   353  
   354  	if *verbose {
   355  		printStats(time.Since(start))
   356  	}
   357  }
   358  

View as plain text