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// Package generate implements the “go generate” command.
6package generate
7
8import (
9	"bufio"
10	"bytes"
11	"context"
12	"fmt"
13	"go/parser"
14	"go/token"
15	"io"
16	"log"
17	"os"
18	"os/exec"
19	"path/filepath"
20	"regexp"
21	"slices"
22	"strconv"
23	"strings"
24
25	"cmd/go/internal/base"
26	"cmd/go/internal/cfg"
27	"cmd/go/internal/load"
28	"cmd/go/internal/modload"
29	"cmd/go/internal/str"
30	"cmd/go/internal/work"
31)
32
33var CmdGenerate = &base.Command{
34	Run:       runGenerate,
35	UsageLine: "go generate [-run regexp] [-n] [-v] [-x] [build flags] [file.go... | packages]",
36	Short:     "generate Go files by processing source",
37	Long: `
38Generate runs commands described by directives within existing
39files. Those commands can run any process but the intent is to
40create or update Go source files.
41
42Go generate is never run automatically by go build, go test,
43and so on. It must be run explicitly.
44
45Go generate scans the file for directives, which are lines of
46the form,
47
48	//go:generate command argument...
49
50(note: no leading spaces and no space in "//go") where command
51is the generator to be run, corresponding to an executable file
52that can be run locally. It must either be in the shell path
53(gofmt), a fully qualified path (/usr/you/bin/mytool), or a
54command alias, described below.
55
56Note that go generate does not parse the file, so lines that look
57like directives in comments or multiline strings will be treated
58as directives.
59
60The arguments to the directive are space-separated tokens or
61double-quoted strings passed to the generator as individual
62arguments when it is run.
63
64Quoted strings use Go syntax and are evaluated before execution; a
65quoted string appears as a single argument to the generator.
66
67To convey to humans and machine tools that code is generated,
68generated source should have a line that matches the following
69regular expression (in Go syntax):
70
71	^// Code generated .* DO NOT EDIT\.$
72
73This line must appear before the first non-comment, non-blank
74text in the file.
75
76Go generate sets several variables when it runs the generator:
77
78	$GOARCH
79		The execution architecture (arm, amd64, etc.)
80	$GOOS
81		The execution operating system (linux, windows, etc.)
82	$GOFILE
83		The base name of the file.
84	$GOLINE
85		The line number of the directive in the source file.
86	$GOPACKAGE
87		The name of the package of the file containing the directive.
88	$GOROOT
89		The GOROOT directory for the 'go' command that invoked the
90		generator, containing the Go toolchain and standard library.
91	$DOLLAR
92		A dollar sign.
93	$PATH
94		The $PATH of the parent process, with $GOROOT/bin
95		placed at the beginning. This causes generators
96		that execute 'go' commands to use the same 'go'
97		as the parent 'go generate' command.
98
99Other than variable substitution and quoted-string evaluation, no
100special processing such as "globbing" is performed on the command
101line.
102
103As a last step before running the command, any invocations of any
104environment variables with alphanumeric names, such as $GOFILE or
105$HOME, are expanded throughout the command line. The syntax for
106variable expansion is $NAME on all operating systems. Due to the
107order of evaluation, variables are expanded even inside quoted
108strings. If the variable NAME is not set, $NAME expands to the
109empty string.
110
111A directive of the form,
112
113	//go:generate -command xxx args...
114
115specifies, for the remainder of this source file only, that the
116string xxx represents the command identified by the arguments. This
117can be used to create aliases or to handle multiword generators.
118For example,
119
120	//go:generate -command foo go tool foo
121
122specifies that the command "foo" represents the generator
123"go tool foo".
124
125Generate processes packages in the order given on the command line,
126one at a time. If the command line lists .go files from a single directory,
127they are treated as a single package. Within a package, generate processes the
128source files in a package in file name order, one at a time. Within
129a source file, generate runs generators in the order they appear
130in the file, one at a time. The go generate tool also sets the build
131tag "generate" so that files may be examined by go generate but ignored
132during build.
133
134For packages with invalid code, generate processes only source files with a
135valid package clause.
136
137If any generator returns an error exit status, "go generate" skips
138all further processing for that package.
139
140The generator is run in the package's source directory.
141
142Go generate accepts two specific flags:
143
144	-run=""
145		if non-empty, specifies a regular expression to select
146		directives whose full original source text (excluding
147		any trailing spaces and final newline) matches the
148		expression.
149
150	-skip=""
151		if non-empty, specifies a regular expression to suppress
152		directives whose full original source text (excluding
153		any trailing spaces and final newline) matches the
154		expression. If a directive matches both the -run and
155		the -skip arguments, it is skipped.
156
157It also accepts the standard build flags including -v, -n, and -x.
158The -v flag prints the names of packages and files as they are
159processed.
160The -n flag prints commands that would be executed.
161The -x flag prints commands as they are executed.
162
163For more about build flags, see 'go help build'.
164
165For more about specifying packages, see 'go help packages'.
166	`,
167}
168
169var (
170	generateRunFlag string         // generate -run flag
171	generateRunRE   *regexp.Regexp // compiled expression for -run
172
173	generateSkipFlag string         // generate -skip flag
174	generateSkipRE   *regexp.Regexp // compiled expression for -skip
175)
176
177func init() {
178	work.AddBuildFlags(CmdGenerate, work.DefaultBuildFlags)
179	CmdGenerate.Flag.StringVar(&generateRunFlag, "run", "", "")
180	CmdGenerate.Flag.StringVar(&generateSkipFlag, "skip", "", "")
181}
182
183func runGenerate(ctx context.Context, cmd *base.Command, args []string) {
184	modload.InitWorkfile()
185
186	if generateRunFlag != "" {
187		var err error
188		generateRunRE, err = regexp.Compile(generateRunFlag)
189		if err != nil {
190			log.Fatalf("generate: %s", err)
191		}
192	}
193	if generateSkipFlag != "" {
194		var err error
195		generateSkipRE, err = regexp.Compile(generateSkipFlag)
196		if err != nil {
197			log.Fatalf("generate: %s", err)
198		}
199	}
200
201	cfg.BuildContext.BuildTags = append(cfg.BuildContext.BuildTags, "generate")
202
203	// Even if the arguments are .go files, this loop suffices.
204	printed := false
205	pkgOpts := load.PackageOpts{IgnoreImports: true}
206	for _, pkg := range load.PackagesAndErrors(ctx, pkgOpts, args) {
207		if modload.Enabled() && pkg.Module != nil && !pkg.Module.Main {
208			if !printed {
209				fmt.Fprintf(os.Stderr, "go: not generating in packages in dependency modules\n")
210				printed = true
211			}
212			continue
213		}
214
215		if pkg.Error != nil && len(pkg.InternalAllGoFiles()) == 0 {
216			// A directory only contains a Go package if it has at least
217			// one .go source file, so the fact that there are no files
218			// implies that the package couldn't be found.
219			base.Errorf("%v", pkg.Error)
220		}
221
222		for _, file := range pkg.InternalGoFiles() {
223			if !generate(file) {
224				break
225			}
226		}
227
228		for _, file := range pkg.InternalXGoFiles() {
229			if !generate(file) {
230				break
231			}
232		}
233	}
234	base.ExitIfErrors()
235}
236
237// generate runs the generation directives for a single file.
238func generate(absFile string) bool {
239	src, err := os.ReadFile(absFile)
240	if err != nil {
241		log.Fatalf("generate: %s", err)
242	}
243
244	// Parse package clause
245	filePkg, err := parser.ParseFile(token.NewFileSet(), "", src, parser.PackageClauseOnly)
246	if err != nil {
247		// Invalid package clause - ignore file.
248		return true
249	}
250
251	g := &Generator{
252		r:        bytes.NewReader(src),
253		path:     absFile,
254		pkg:      filePkg.Name.String(),
255		commands: make(map[string][]string),
256	}
257	return g.run()
258}
259
260// A Generator represents the state of a single Go source file
261// being scanned for generator commands.
262type Generator struct {
263	r        io.Reader
264	path     string // full rooted path name.
265	dir      string // full rooted directory of file.
266	file     string // base name of file.
267	pkg      string
268	commands map[string][]string
269	lineNum  int // current line number.
270	env      []string
271}
272
273// run runs the generators in the current file.
274func (g *Generator) run() (ok bool) {
275	// Processing below here calls g.errorf on failure, which does panic(stop).
276	// If we encounter an error, we abort the package.
277	defer func() {
278		e := recover()
279		if e != nil {
280			ok = false
281			if e != stop {
282				panic(e)
283			}
284			base.SetExitStatus(1)
285		}
286	}()
287	g.dir, g.file = filepath.Split(g.path)
288	g.dir = filepath.Clean(g.dir) // No final separator please.
289	if cfg.BuildV {
290		fmt.Fprintf(os.Stderr, "%s\n", base.ShortPath(g.path))
291	}
292
293	// Scan for lines that start "//go:generate".
294	// Can't use bufio.Scanner because it can't handle long lines,
295	// which are likely to appear when using generate.
296	input := bufio.NewReader(g.r)
297	var err error
298	// One line per loop.
299	for {
300		g.lineNum++ // 1-indexed.
301		var buf []byte
302		buf, err = input.ReadSlice('\n')
303		if err == bufio.ErrBufferFull {
304			// Line too long - consume and ignore.
305			if isGoGenerate(buf) {
306				g.errorf("directive too long")
307			}
308			for err == bufio.ErrBufferFull {
309				_, err = input.ReadSlice('\n')
310			}
311			if err != nil {
312				break
313			}
314			continue
315		}
316
317		if err != nil {
318			// Check for marker at EOF without final \n.
319			if err == io.EOF && isGoGenerate(buf) {
320				err = io.ErrUnexpectedEOF
321			}
322			break
323		}
324
325		if !isGoGenerate(buf) {
326			continue
327		}
328		if generateRunFlag != "" && !generateRunRE.Match(bytes.TrimSpace(buf)) {
329			continue
330		}
331		if generateSkipFlag != "" && generateSkipRE.Match(bytes.TrimSpace(buf)) {
332			continue
333		}
334
335		g.setEnv()
336		words := g.split(string(buf))
337		if len(words) == 0 {
338			g.errorf("no arguments to directive")
339		}
340		if words[0] == "-command" {
341			g.setShorthand(words)
342			continue
343		}
344		// Run the command line.
345		if cfg.BuildN || cfg.BuildX {
346			fmt.Fprintf(os.Stderr, "%s\n", strings.Join(words, " "))
347		}
348		if cfg.BuildN {
349			continue
350		}
351		g.exec(words)
352	}
353	if err != nil && err != io.EOF {
354		g.errorf("error reading %s: %s", base.ShortPath(g.path), err)
355	}
356	return true
357}
358
359func isGoGenerate(buf []byte) bool {
360	return bytes.HasPrefix(buf, []byte("//go:generate ")) || bytes.HasPrefix(buf, []byte("//go:generate\t"))
361}
362
363// setEnv sets the extra environment variables used when executing a
364// single go:generate command.
365func (g *Generator) setEnv() {
366	env := []string{
367		"GOROOT=" + cfg.GOROOT,
368		"GOARCH=" + cfg.BuildContext.GOARCH,
369		"GOOS=" + cfg.BuildContext.GOOS,
370		"GOFILE=" + g.file,
371		"GOLINE=" + strconv.Itoa(g.lineNum),
372		"GOPACKAGE=" + g.pkg,
373		"DOLLAR=" + "$",
374	}
375	env = base.AppendPATH(env)
376	env = base.AppendPWD(env, g.dir)
377	g.env = env
378}
379
380// split breaks the line into words, evaluating quoted
381// strings and evaluating environment variables.
382// The initial //go:generate element is present in line.
383func (g *Generator) split(line string) []string {
384	// Parse line, obeying quoted strings.
385	var words []string
386	line = line[len("//go:generate ") : len(line)-1] // Drop preamble and final newline.
387	// There may still be a carriage return.
388	if len(line) > 0 && line[len(line)-1] == '\r' {
389		line = line[:len(line)-1]
390	}
391	// One (possibly quoted) word per iteration.
392Words:
393	for {
394		line = strings.TrimLeft(line, " \t")
395		if len(line) == 0 {
396			break
397		}
398		if line[0] == '"' {
399			for i := 1; i < len(line); i++ {
400				c := line[i] // Only looking for ASCII so this is OK.
401				switch c {
402				case '\\':
403					if i+1 == len(line) {
404						g.errorf("bad backslash")
405					}
406					i++ // Absorb next byte (If it's a multibyte we'll get an error in Unquote).
407				case '"':
408					word, err := strconv.Unquote(line[0 : i+1])
409					if err != nil {
410						g.errorf("bad quoted string")
411					}
412					words = append(words, word)
413					line = line[i+1:]
414					// Check the next character is space or end of line.
415					if len(line) > 0 && line[0] != ' ' && line[0] != '\t' {
416						g.errorf("expect space after quoted argument")
417					}
418					continue Words
419				}
420			}
421			g.errorf("mismatched quoted string")
422		}
423		i := strings.IndexAny(line, " \t")
424		if i < 0 {
425			i = len(line)
426		}
427		words = append(words, line[0:i])
428		line = line[i:]
429	}
430	// Substitute command if required.
431	if len(words) > 0 && g.commands[words[0]] != nil {
432		// Replace 0th word by command substitution.
433		//
434		// Force a copy of the command definition to
435		// ensure words doesn't end up as a reference
436		// to the g.commands content.
437		tmpCmdWords := append([]string(nil), (g.commands[words[0]])...)
438		words = append(tmpCmdWords, words[1:]...)
439	}
440	// Substitute environment variables.
441	for i, word := range words {
442		words[i] = os.Expand(word, g.expandVar)
443	}
444	return words
445}
446
447var stop = fmt.Errorf("error in generation")
448
449// errorf logs an error message prefixed with the file and line number.
450// It then exits the program (with exit status 1) because generation stops
451// at the first error.
452func (g *Generator) errorf(format string, args ...any) {
453	fmt.Fprintf(os.Stderr, "%s:%d: %s\n", base.ShortPath(g.path), g.lineNum,
454		fmt.Sprintf(format, args...))
455	panic(stop)
456}
457
458// expandVar expands the $XXX invocation in word. It is called
459// by os.Expand.
460func (g *Generator) expandVar(word string) string {
461	w := word + "="
462	for _, e := range g.env {
463		if strings.HasPrefix(e, w) {
464			return e[len(w):]
465		}
466	}
467	return os.Getenv(word)
468}
469
470// setShorthand installs a new shorthand as defined by a -command directive.
471func (g *Generator) setShorthand(words []string) {
472	// Create command shorthand.
473	if len(words) == 1 {
474		g.errorf("no command specified for -command")
475	}
476	command := words[1]
477	if g.commands[command] != nil {
478		g.errorf("command %q multiply defined", command)
479	}
480	g.commands[command] = slices.Clip(words[2:])
481}
482
483// exec runs the command specified by the argument. The first word is
484// the command name itself.
485func (g *Generator) exec(words []string) {
486	path := words[0]
487	if path != "" && !strings.Contains(path, string(os.PathSeparator)) {
488		// If a generator says '//go:generate go run <blah>' it almost certainly
489		// intends to use the same 'go' as 'go generate' itself.
490		// Prefer to resolve the binary from GOROOT/bin, and for consistency
491		// prefer to resolve any other commands there too.
492		gorootBinPath, err := cfg.LookPath(filepath.Join(cfg.GOROOTbin, path))
493		if err == nil {
494			path = gorootBinPath
495		}
496	}
497	cmd := exec.Command(path, words[1:]...)
498	cmd.Args[0] = words[0] // Overwrite with the original in case it was rewritten above.
499
500	// Standard in and out of generator should be the usual.
501	cmd.Stdout = os.Stdout
502	cmd.Stderr = os.Stderr
503	// Run the command in the package directory.
504	cmd.Dir = g.dir
505	cmd.Env = str.StringList(cfg.OrigEnv, g.env)
506	err := cmd.Run()
507	if err != nil {
508		g.errorf("running %q: %s", words[0], err)
509	}
510}
511