github.com/bir3/gocompiler@v0.3.205/src/cmd/gocmd/internal/generate/generate.go (about)

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