Black Lives Matter. Support the Equal Justice Initiative.

Source file src/cmd/go/internal/generate/generate.go

Documentation: cmd/go/internal/generate

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

View as plain text