gotype.go raw

   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