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