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  	if *allErrors {
   124  		parserMode |= parser.AllErrors
   125  	}
   126  	if *printAST {
   127  		sequential = true
   128  	}
   129  	if *printTrace {
   130  		parserMode |= parser.Trace
   131  		sequential = true
   132  	}
   133  	if *parseComments && (*printAST || *printTrace) {
   134  		parserMode |= parser.ParseComments
   135  	}
   136  }
   137  
   138  const usageString = `usage: gotype [flags] [path ...]
   139  
   140  The gotype command, like the front-end of a Go compiler, parses and
   141  type-checks a single Go package. Errors are reported if the analysis
   142  fails; otherwise gotype is quiet (unless -v is set).
   143  
   144  Without a list of paths, gotype reads from standard input, which
   145  must provide a single Go source file defining a complete package.
   146  
   147  With a single directory argument, gotype checks the Go files in
   148  that directory, comprising a single package. Use -t to include the
   149  (in-package) _test.go files. Use -x to type check only external
   150  test files.
   151  
   152  Otherwise, each path must be the filename of a Go file belonging
   153  to the same package.
   154  
   155  Imports are processed by importing directly from the source of
   156  imported packages (default), or by importing from compiled and
   157  installed packages (by setting -c to the respective compiler).
   158  
   159  The -c flag must be set to a compiler ("gc", "gccgo") when type-
   160  checking packages containing imports with relative import paths
   161  (import "./mypkg") because the source importer cannot know which
   162  files to include for such packages.
   163  `
   164  
   165  func usage() {
   166  	fmt.Fprintln(os.Stderr, usageString)
   167  	flag.PrintDefaults()
   168  	os.Exit(2)
   169  }
   170  
   171  func report(err error) {
   172  	if *panicOnError {
   173  		panic(err)
   174  	}
   175  	scanner.PrintError(os.Stderr, err)
   176  	if list, ok := err.(scanner.ErrorList); ok {
   177  		errorCount += len(list)
   178  		return
   179  	}
   180  	errorCount++
   181  }
   182  
   183  // parse may be called concurrently.
   184  func parse(filename string, src any) (*ast.File, error) {
   185  	if *verbose {
   186  		fmt.Println(filename)
   187  	}
   188  	file, err := parser.ParseFile(fset, filename, src, parserMode) // ok to access fset concurrently
   189  	if *printAST {
   190  		ast.Print(fset, file)
   191  	}
   192  	return file, err
   193  }
   194  
   195  func parseStdin() (*ast.File, error) {
   196  	src, err := io.ReadAll(os.Stdin)
   197  	if err != nil {
   198  		return nil, err
   199  	}
   200  	return parse("<standard input>", src)
   201  }
   202  
   203  func parseFiles(dir string, filenames []string) ([]*ast.File, error) {
   204  	files := make([]*ast.File, len(filenames))
   205  	errors := make([]error, len(filenames))
   206  
   207  	var wg sync.WaitGroup
   208  	for i, filename := range filenames {
   209  		wg.Add(1)
   210  		go func(i int, filepath string) {
   211  			defer wg.Done()
   212  			files[i], errors[i] = parse(filepath, nil)
   213  		}(i, filepath.Join(dir, filename))
   214  		if sequential {
   215  			wg.Wait()
   216  		}
   217  	}
   218  	wg.Wait()
   219  
   220  	// If there are errors, return the first one for deterministic results.
   221  	var first error
   222  	for _, err := range errors {
   223  		if err != nil {
   224  			first = err
   225  			// If we have an error, some files may be nil.
   226  			// Remove them. (The go/parser always returns
   227  			// a possibly partial AST even in the presence
   228  			// of errors, except if the file doesn't exist
   229  			// in the first place, in which case it cannot
   230  			// matter.)
   231  			i := 0
   232  			for _, f := range files {
   233  				if f != nil {
   234  					files[i] = f
   235  					i++
   236  				}
   237  			}
   238  			files = files[:i]
   239  			break
   240  		}
   241  	}
   242  
   243  	return files, first
   244  }
   245  
   246  func parseDir(dir string) ([]*ast.File, error) {
   247  	ctxt := build.Default
   248  	pkginfo, err := ctxt.ImportDir(dir, 0)
   249  	if _, nogo := err.(*build.NoGoError); err != nil && !nogo {
   250  		return nil, err
   251  	}
   252  
   253  	if *xtestFiles {
   254  		return parseFiles(dir, pkginfo.XTestGoFiles)
   255  	}
   256  
   257  	filenames := append(pkginfo.GoFiles, pkginfo.CgoFiles...)
   258  	if *testFiles {
   259  		filenames = append(filenames, pkginfo.TestGoFiles...)
   260  	}
   261  	return parseFiles(dir, filenames)
   262  }
   263  
   264  func getPkgFiles(args []string) ([]*ast.File, error) {
   265  	if len(args) == 0 {
   266  		// stdin
   267  		file, err := parseStdin()
   268  		if err != nil {
   269  			return nil, err
   270  		}
   271  		return []*ast.File{file}, nil
   272  	}
   273  
   274  	if len(args) == 1 {
   275  		// possibly a directory
   276  		path := args[0]
   277  		info, err := os.Stat(path)
   278  		if err != nil {
   279  			return nil, err
   280  		}
   281  		if info.IsDir() {
   282  			return parseDir(path)
   283  		}
   284  	}
   285  
   286  	// list of files
   287  	return parseFiles("", args)
   288  }
   289  
   290  func checkPkgFiles(files []*ast.File) {
   291  	type bailout struct{}
   292  
   293  	// if checkPkgFiles is called multiple times, set up conf only once
   294  	conf := types.Config{
   295  		FakeImportC: true,
   296  		Error: func(err error) {
   297  			if !*allErrors && errorCount >= 10 {
   298  				panic(bailout{})
   299  			}
   300  			report(err)
   301  		},
   302  		Importer: importer.ForCompiler(fset, *compiler, nil),
   303  		Sizes:    types.SizesFor(build.Default.Compiler, build.Default.GOARCH),
   304  	}
   305  
   306  	defer func() {
   307  		switch p := recover().(type) {
   308  		case nil, bailout:
   309  			// normal return or early exit
   310  		default:
   311  			// re-panic
   312  			panic(p)
   313  		}
   314  	}()
   315  
   316  	const path = "pkg" // any non-empty string will do for now
   317  	conf.Check(path, fset, files, nil)
   318  }
   319  
   320  func printStats(d time.Duration) {
   321  	fileCount := 0
   322  	lineCount := 0
   323  	fset.Iterate(func(f *token.File) bool {
   324  		fileCount++
   325  		lineCount += f.LineCount()
   326  		return true
   327  	})
   328  
   329  	fmt.Printf(
   330  		"%s (%d files, %d lines, %d lines/s)\n",
   331  		d, fileCount, lineCount, int64(float64(lineCount)/d.Seconds()),
   332  	)
   333  }
   334  
   335  func main() {
   336  	flag.Usage = usage
   337  	flag.Parse()
   338  	initParserMode()
   339  
   340  	start := time.Now()
   341  
   342  	files, err := getPkgFiles(flag.Args())
   343  	if err != nil {
   344  		report(err)
   345  		// ok to continue (files may be empty, but not nil)
   346  	}
   347  
   348  	checkPkgFiles(files)
   349  	if errorCount > 0 {
   350  		os.Exit(2)
   351  	}
   352  
   353  	if *verbose {
   354  		printStats(time.Since(start))
   355  	}
   356  }
   357  

View as plain text