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