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/* 10The gotype command, like the front-end of a Go compiler, parses and 11type-checks a single Go package. Errors are reported if the analysis 12fails; otherwise gotype is quiet (unless -v is set). 13 14Without a list of paths, gotype reads from standard input, which 15must provide a single Go source file defining a complete package. 16 17With a single directory argument, gotype checks the Go files in 18that directory, comprising a single package. Use -t to include the 19(in-package) _test.go files. Use -x to type check only external 20test files. 21 22Otherwise, each path must be the filename of a Go file belonging 23to the same package. 24 25Imports are processed by importing directly from the source of 26imported packages (default), or by importing from compiled and 27installed packages (by setting -c to the respective compiler). 28 29The -c flag must be set to a compiler ("gc", "gccgo") when type- 30checking packages containing imports with relative import paths 31(import "./mypkg") because the source importer cannot know which 32files to include for such packages. 33 34Usage: 35 36 gotype [flags] [path...] 37 38The 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 51Flags 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 62Examples: 63 64To check the files a.go, b.go, and c.go: 65 66 gotype a.go b.go c.go 67 68To check an entire package including (in-package) tests in the directory dir and print the processed files: 69 70 gotype -t -v dir 71 72To check the external test package (if any) in the current directory, based on installed packages compiled with 73cmd/compile: 74 75 gotype -c=gc -x . 76 77To verify the output of a pipe: 78 79 echo "package foo" | gotype 80*/ 81package main 82 83import ( 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 100var ( 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 115var ( 116 fset = token.NewFileSet() 117 errorCount = 0 118 sequential = false 119 parserMode parser.Mode 120) 121 122func 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 138const usageString = `usage: gotype [flags] [path ...] 139 140The gotype command, like the front-end of a Go compiler, parses and 141type-checks a single Go package. Errors are reported if the analysis 142fails; otherwise gotype is quiet (unless -v is set). 143 144Without a list of paths, gotype reads from standard input, which 145must provide a single Go source file defining a complete package. 146 147With a single directory argument, gotype checks the Go files in 148that directory, comprising a single package. Use -t to include the 149(in-package) _test.go files. Use -x to type check only external 150test files. 151 152Otherwise, each path must be the filename of a Go file belonging 153to the same package. 154 155Imports are processed by importing directly from the source of 156imported packages (default), or by importing from compiled and 157installed packages (by setting -c to the respective compiler). 158 159The -c flag must be set to a compiler ("gc", "gccgo") when type- 160checking packages containing imports with relative import paths 161(import "./mypkg") because the source importer cannot know which 162files to include for such packages. 163` 164 165func usage() { 166 fmt.Fprintln(os.Stderr, usageString) 167 flag.PrintDefaults() 168 os.Exit(2) 169} 170 171func 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. 184func 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 195func 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 203func 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 246func 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 264func 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 290func 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 320func 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 335func 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