github.com/bir3/gocompiler@v0.9.2202/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 "github.com/bir3/gocompiler/exec" 19 "path/filepath" 20 "regexp" 21 "slices" 22 "strconv" 23 "strings" 24 25 "github.com/bir3/gocompiler/src/cmd/gocmd/internal/base" 26 "github.com/bir3/gocompiler/src/cmd/gocmd/internal/cfg" 27 "github.com/bir3/gocompiler/src/cmd/gocmd/internal/load" 28 "github.com/bir3/gocompiler/src/cmd/gocmd/internal/modload" 29 "github.com/bir3/gocompiler/src/cmd/gocmd/internal/str" 30 "github.com/bir3/gocompiler/src/cmd/gocmd/internal/work" 31 ) 32 33 var 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: ` 38 Generate runs commands described by directives within existing 39 files. Those commands can run any process but the intent is to 40 create or update Go source files. 41 42 Go generate is never run automatically by go build, go test, 43 and so on. It must be run explicitly. 44 45 Go generate scans the file for directives, which are lines of 46 the form, 47 48 //go:generate command argument... 49 50 (note: no leading spaces and no space in "//go") where command 51 is the generator to be run, corresponding to an executable file 52 that can be run locally. It must either be in the shell path 53 (gofmt), a fully qualified path (/usr/you/bin/mytool), or a 54 command alias, described below. 55 56 Note that go generate does not parse the file, so lines that look 57 like directives in comments or multiline strings will be treated 58 as directives. 59 60 The arguments to the directive are space-separated tokens or 61 double-quoted strings passed to the generator as individual 62 arguments when it is run. 63 64 Quoted strings use Go syntax and are evaluated before execution; a 65 quoted string appears as a single argument to the generator. 66 67 To convey to humans and machine tools that code is generated, 68 generated source should have a line that matches the following 69 regular expression (in Go syntax): 70 71 ^// Code generated .* DO NOT EDIT\.$ 72 73 This line must appear before the first non-comment, non-blank 74 text in the file. 75 76 Go 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 99 Other than variable substitution and quoted-string evaluation, no 100 special processing such as "globbing" is performed on the command 101 line. 102 103 As a last step before running the command, any invocations of any 104 environment variables with alphanumeric names, such as $GOFILE or 105 $HOME, are expanded throughout the command line. The syntax for 106 variable expansion is $NAME on all operating systems. Due to the 107 order of evaluation, variables are expanded even inside quoted 108 strings. If the variable NAME is not set, $NAME expands to the 109 empty string. 110 111 A directive of the form, 112 113 //go:generate -command xxx args... 114 115 specifies, for the remainder of this source file only, that the 116 string xxx represents the command identified by the arguments. This 117 can be used to create aliases or to handle multiword generators. 118 For example, 119 120 //go:generate -command foo go tool foo 121 122 specifies that the command "foo" represents the generator 123 "go tool foo". 124 125 Generate processes packages in the order given on the command line, 126 one at a time. If the command line lists .go files from a single directory, 127 they are treated as a single package. Within a package, generate processes the 128 source files in a package in file name order, one at a time. Within 129 a source file, generate runs generators in the order they appear 130 in the file, one at a time. The go generate tool also sets the build 131 tag "generate" so that files may be examined by go generate but ignored 132 during build. 133 134 For packages with invalid code, generate processes only source files with a 135 valid package clause. 136 137 If any generator returns an error exit status, "go generate" skips 138 all further processing for that package. 139 140 The generator is run in the package's source directory. 141 142 Go 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 157 It also accepts the standard build flags including -v, -n, and -x. 158 The -v flag prints the names of packages and files as they are 159 processed. 160 The -n flag prints commands that would be executed. 161 The -x flag prints commands as they are executed. 162 163 For more about build flags, see 'go help build'. 164 165 For more about specifying packages, see 'go help packages'. 166 `, 167 } 168 169 var ( 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 177 func init() { 178 work.AddBuildFlags(CmdGenerate, work.DefaultBuildFlags) 179 CmdGenerate.Flag.StringVar(&generateRunFlag, "run", "", "") 180 CmdGenerate.Flag.StringVar(&generateSkipFlag, "skip", "", "") 181 } 182 183 func 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. 238 func 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. 262 type 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. 274 func (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 359 func 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. 365 func (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. 383 func (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. 392 Words: 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 447 var 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. 452 func (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. 460 func (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. 471 func (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. 485 func (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 }