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

View as plain text