github.com/gernest/nezuko@v0.1.2/internal/modfile/rule.go (about) 1 // Copyright 2018 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 modfile 6 7 import ( 8 "bytes" 9 "errors" 10 "fmt" 11 "path/filepath" 12 "regexp" 13 "sort" 14 "strconv" 15 "strings" 16 "unicode" 17 18 "github.com/gernest/nezuko/internal/module" 19 "github.com/gernest/nezuko/internal/semver" 20 ) 21 22 // A File is the parsed, interpreted form of a z.mod file. 23 type File struct { 24 Module *Module 25 Exports *Exports 26 Require []*Require 27 Exclude []*Exclude 28 Replace []*Replace 29 30 Syntax *FileSyntax 31 } 32 33 // A Module is the module statement. 34 type Module struct { 35 Mod module.Version 36 Syntax *Line 37 } 38 39 // A Go is the go statement. 40 type Go struct { 41 Version string // "1.23" 42 Syntax *Line 43 } 44 45 // Exports exports is the exports statement 46 type Exports struct { 47 Name string 48 Syntax *Line 49 } 50 51 // A Require is a single require statement. 52 type Require struct { 53 Mod module.Version 54 Indirect bool // has "// indirect" comment 55 Syntax *Line 56 } 57 58 // An Exclude is a single exclude statement. 59 type Exclude struct { 60 Mod module.Version 61 Syntax *Line 62 } 63 64 // A Replace is a single replace statement. 65 type Replace struct { 66 Old module.Version 67 New module.Version 68 Syntax *Line 69 } 70 71 func (f *File) AddModuleStmt(path string) error { 72 if f.Syntax == nil { 73 f.Syntax = new(FileSyntax) 74 } 75 if f.Module == nil { 76 f.Module = &Module{ 77 Mod: module.Version{Path: path}, 78 Syntax: f.Syntax.addLine(nil, "module", AutoQuote(path)), 79 } 80 } else { 81 f.Module.Mod.Path = path 82 f.Syntax.updateLine(f.Module.Syntax, "module", AutoQuote(path)) 83 } 84 return nil 85 } 86 87 func (f *File) AddComment(text string) { 88 if f.Syntax == nil { 89 f.Syntax = new(FileSyntax) 90 } 91 f.Syntax.Stmt = append(f.Syntax.Stmt, &CommentBlock{ 92 Comments: Comments{ 93 Before: []Comment{ 94 { 95 Token: text, 96 }, 97 }, 98 }, 99 }) 100 } 101 102 type VersionFixer func(path, version string) (string, error) 103 104 // Parse parses the data, reported in errors as being from file, 105 // into a File struct. It applies fix, if non-nil, to canonicalize all module versions found. 106 func Parse(file string, data []byte, fix VersionFixer) (*File, error) { 107 return parseToFile(file, data, fix, true) 108 } 109 110 // ParseLax is like Parse but ignores unknown statements. 111 // It is used when parsing z.mod files other than the main module, 112 // under the theory that most statement types we add in the future will 113 // only apply in the main module, like exclude and replace, 114 // and so we get better gradual deployments if old go commands 115 // simply ignore those statements when found in z.mod files 116 // in dependencies. 117 func ParseLax(file string, data []byte, fix VersionFixer) (*File, error) { 118 return parseToFile(file, data, fix, false) 119 } 120 121 func parseToFile(file string, data []byte, fix VersionFixer, strict bool) (*File, error) { 122 fs, err := parse(file, data) 123 if err != nil { 124 return nil, err 125 } 126 f := &File{ 127 Syntax: fs, 128 } 129 130 var errs bytes.Buffer 131 for _, x := range fs.Stmt { 132 switch x := x.(type) { 133 case *Line: 134 f.add(&errs, x, x.Token[0], x.Token[1:], fix, strict) 135 136 case *LineBlock: 137 if len(x.Token) > 1 { 138 if strict { 139 fmt.Fprintf(&errs, "%s:%d: unknown block type: %s\n", file, x.Start.Line, strings.Join(x.Token, " ")) 140 } 141 continue 142 } 143 switch x.Token[0] { 144 default: 145 if strict { 146 fmt.Fprintf(&errs, "%s:%d: unknown block type: %s\n", file, x.Start.Line, strings.Join(x.Token, " ")) 147 } 148 continue 149 case "module", "require", "exclude", "replace": 150 for _, l := range x.Line { 151 f.add(&errs, l, x.Token[0], l.Token, fix, strict) 152 } 153 } 154 } 155 } 156 157 if errs.Len() > 0 { 158 return nil, errors.New(strings.TrimRight(errs.String(), "\n")) 159 } 160 return f, nil 161 } 162 163 var GoVersionRE = regexp.MustCompile(`([1-9][0-9]*)\.(0|[1-9][0-9]*)`) 164 165 func (f *File) add(errs *bytes.Buffer, line *Line, verb string, args []string, fix VersionFixer, strict bool) { 166 // If strict is false, this module is a dependency. 167 // We ignore all unknown directives as well as main-module-only 168 // directives like replace and exclude. It will work better for 169 // forward compatibility if we can depend on modules that have unknown 170 // statements (presumed relevant only when acting as the main module) 171 // and simply ignore those statements. 172 if !strict { 173 switch verb { 174 case "module", "require", "go": 175 // want these even for dependency z.mods 176 default: 177 return 178 } 179 } 180 181 switch verb { 182 default: 183 fmt.Fprintf(errs, "%s:%d: unknown directive: %s\n", f.Syntax.Name, line.Start.Line, verb) 184 185 case "exports": 186 if f.Exports != nil { 187 fmt.Fprintf(errs, "%s:%d: repeated go statement\n", f.Syntax.Name, line.Start.Line) 188 return 189 } 190 if len(args) != 1 || !IsValidExport(args[0]) { 191 fmt.Fprintf(errs, "%s:%d: usage: go 1.23\n", f.Syntax.Name, line.Start.Line) 192 return 193 } 194 f.Exports = &Exports{Syntax: line} 195 f.Exports.Name = args[0] 196 case "module": 197 if f.Module != nil { 198 fmt.Fprintf(errs, "%s:%d: repeated module statement\n", f.Syntax.Name, line.Start.Line) 199 return 200 } 201 f.Module = &Module{Syntax: line} 202 if len(args) != 1 { 203 204 fmt.Fprintf(errs, "%s:%d: usage: module module/path [version]\n", f.Syntax.Name, line.Start.Line) 205 return 206 } 207 s, err := parseString(&args[0]) 208 if err != nil { 209 fmt.Fprintf(errs, "%s:%d: invalid quoted string: %v\n", f.Syntax.Name, line.Start.Line, err) 210 return 211 } 212 f.Module.Mod = module.Version{Path: s} 213 case "require", "exclude": 214 if len(args) != 2 { 215 fmt.Fprintf(errs, "%s:%d: usage: %s module/path v1.2.3\n", f.Syntax.Name, line.Start.Line, verb) 216 return 217 } 218 s, err := parseString(&args[0]) 219 if err != nil { 220 fmt.Fprintf(errs, "%s:%d: invalid quoted string: %v\n", f.Syntax.Name, line.Start.Line, err) 221 return 222 } 223 old := args[1] 224 v, err := parseVersion(s, &args[1], fix) 225 if err != nil { 226 fmt.Fprintf(errs, "%s:%d: invalid module version %q: %v\n", f.Syntax.Name, line.Start.Line, old, err) 227 return 228 } 229 pathMajor, err := modulePathMajor(s) 230 if err != nil { 231 fmt.Fprintf(errs, "%s:%d: %v\n", f.Syntax.Name, line.Start.Line, err) 232 return 233 } 234 if !module.MatchPathMajor(v, pathMajor) { 235 if pathMajor == "" { 236 pathMajor = "v0 or v1" 237 } 238 fmt.Fprintf(errs, "%s:%d: invalid module: %s should be %s, not %s (%s)\n", f.Syntax.Name, line.Start.Line, s, pathMajor, semver.Major(v), v) 239 return 240 } 241 if verb == "require" { 242 f.Require = append(f.Require, &Require{ 243 Mod: module.Version{Path: s, Version: v}, 244 Syntax: line, 245 Indirect: isIndirect(line), 246 }) 247 } else { 248 f.Exclude = append(f.Exclude, &Exclude{ 249 Mod: module.Version{Path: s, Version: v}, 250 Syntax: line, 251 }) 252 } 253 case "replace": 254 arrow := 2 255 if len(args) >= 2 && args[1] == "=>" { 256 arrow = 1 257 } 258 if len(args) < arrow+2 || len(args) > arrow+3 || args[arrow] != "=>" { 259 fmt.Fprintf(errs, "%s:%d: usage: %s module/path [v1.2.3] => other/module v1.4\n\t or %s module/path [v1.2.3] => ../local/directory\n", f.Syntax.Name, line.Start.Line, verb, verb) 260 return 261 } 262 s, err := parseString(&args[0]) 263 if err != nil { 264 fmt.Fprintf(errs, "%s:%d: invalid quoted string: %v\n", f.Syntax.Name, line.Start.Line, err) 265 return 266 } 267 pathMajor, err := modulePathMajor(s) 268 if err != nil { 269 fmt.Fprintf(errs, "%s:%d: %v\n", f.Syntax.Name, line.Start.Line, err) 270 return 271 } 272 var v string 273 if arrow == 2 { 274 old := args[1] 275 v, err = parseVersion(s, &args[1], fix) 276 if err != nil { 277 fmt.Fprintf(errs, "%s:%d: invalid module version %v: %v\n", f.Syntax.Name, line.Start.Line, old, err) 278 return 279 } 280 if !module.MatchPathMajor(v, pathMajor) { 281 if pathMajor == "" { 282 pathMajor = "v0 or v1" 283 } 284 fmt.Fprintf(errs, "%s:%d: invalid module: %s should be %s, not %s (%s)\n", f.Syntax.Name, line.Start.Line, s, pathMajor, semver.Major(v), v) 285 return 286 } 287 } 288 ns, err := parseString(&args[arrow+1]) 289 if err != nil { 290 fmt.Fprintf(errs, "%s:%d: invalid quoted string: %v\n", f.Syntax.Name, line.Start.Line, err) 291 return 292 } 293 nv := "" 294 if len(args) == arrow+2 { 295 if !IsDirectoryPath(ns) { 296 fmt.Fprintf(errs, "%s:%d: replacement module without version must be directory path (rooted or starting with ./ or ../)\n", f.Syntax.Name, line.Start.Line) 297 return 298 } 299 if filepath.Separator == '/' && strings.Contains(ns, `\`) { 300 fmt.Fprintf(errs, "%s:%d: replacement directory appears to be Windows path (on a non-windows system)\n", f.Syntax.Name, line.Start.Line) 301 return 302 } 303 } 304 if len(args) == arrow+3 { 305 old := args[arrow+1] 306 nv, err = parseVersion(ns, &args[arrow+2], fix) 307 if err != nil { 308 fmt.Fprintf(errs, "%s:%d: invalid module version %v: %v\n", f.Syntax.Name, line.Start.Line, old, err) 309 return 310 } 311 if IsDirectoryPath(ns) { 312 fmt.Fprintf(errs, "%s:%d: replacement module directory path %q cannot have version\n", f.Syntax.Name, line.Start.Line, ns) 313 return 314 } 315 } 316 f.Replace = append(f.Replace, &Replace{ 317 Old: module.Version{Path: s, Version: v}, 318 New: module.Version{Path: ns, Version: nv}, 319 Syntax: line, 320 }) 321 } 322 } 323 324 // isIndirect reports whether line has a "// indirect" comment, 325 // meaning it is in z.mod only for its effect on indirect dependencies, 326 // so that it can be dropped entirely once the effective version of the 327 // indirect dependency reaches the given minimum version. 328 func isIndirect(line *Line) bool { 329 if len(line.Suffix) == 0 { 330 return false 331 } 332 f := strings.Fields(line.Suffix[0].Token) 333 return (len(f) == 2 && f[1] == "indirect" || len(f) > 2 && f[1] == "indirect;") && f[0] == "//" 334 } 335 336 // setIndirect sets line to have (or not have) a "// indirect" comment. 337 func setIndirect(line *Line, indirect bool) { 338 if isIndirect(line) == indirect { 339 return 340 } 341 if indirect { 342 // Adding comment. 343 if len(line.Suffix) == 0 { 344 // New comment. 345 line.Suffix = []Comment{{Token: "// indirect", Suffix: true}} 346 return 347 } 348 // Insert at beginning of existing comment. 349 com := &line.Suffix[0] 350 space := " " 351 if len(com.Token) > 2 && com.Token[2] == ' ' || com.Token[2] == '\t' { 352 space = "" 353 } 354 com.Token = "// indirect;" + space + com.Token[2:] 355 return 356 } 357 358 // Removing comment. 359 f := strings.Fields(line.Suffix[0].Token) 360 if len(f) == 2 { 361 // Remove whole comment. 362 line.Suffix = nil 363 return 364 } 365 366 // Remove comment prefix. 367 com := &line.Suffix[0] 368 i := strings.Index(com.Token, "indirect;") 369 com.Token = "//" + com.Token[i+len("indirect;"):] 370 } 371 372 // IsDirectoryPath reports whether the given path should be interpreted 373 // as a directory path. Just like on the go command line, relative paths 374 // and rooted paths are directory paths; the rest are module paths. 375 func IsDirectoryPath(ns string) bool { 376 // Because z.mod files can move from one system to another, 377 // we check all known path syntaxes, both Unix and Windows. 378 return strings.HasPrefix(ns, "./") || strings.HasPrefix(ns, "../") || strings.HasPrefix(ns, "/") || 379 strings.HasPrefix(ns, `.\`) || strings.HasPrefix(ns, `..\`) || strings.HasPrefix(ns, `\`) || 380 len(ns) >= 2 && ('A' <= ns[0] && ns[0] <= 'Z' || 'a' <= ns[0] && ns[0] <= 'z') && ns[1] == ':' 381 } 382 383 // MustQuote reports whether s must be quoted in order to appear as 384 // a single token in a z.mod line. 385 func MustQuote(s string) bool { 386 for _, r := range s { 387 if !unicode.IsPrint(r) || r == ' ' || r == '"' || r == '\'' || r == '`' { 388 return true 389 } 390 } 391 return s == "" || strings.Contains(s, "//") || strings.Contains(s, "/*") 392 } 393 394 // AutoQuote returns s or, if quoting is required for s to appear in a z.mod, 395 // the quotation of s. 396 func AutoQuote(s string) string { 397 if MustQuote(s) { 398 return strconv.Quote(s) 399 } 400 return s 401 } 402 403 func parseString(s *string) (string, error) { 404 t := *s 405 if strings.HasPrefix(t, `"`) { 406 var err error 407 if t, err = strconv.Unquote(t); err != nil { 408 return "", err 409 } 410 } else if strings.ContainsAny(t, "\"'`") { 411 // Other quotes are reserved both for possible future expansion 412 // and to avoid confusion. For example if someone types 'x' 413 // we want that to be a syntax error and not a literal x in literal quotation marks. 414 return "", fmt.Errorf("unquoted string cannot contain quote") 415 } 416 *s = AutoQuote(t) 417 return t, nil 418 } 419 420 func parseVersion(path string, s *string, fix VersionFixer) (string, error) { 421 t, err := parseString(s) 422 if err != nil { 423 return "", err 424 } 425 if fix != nil { 426 var err error 427 t, err = fix(path, t) 428 if err != nil { 429 return "", err 430 } 431 } 432 if v := module.CanonicalVersion(t); v != "" { 433 *s = v 434 return *s, nil 435 } 436 return "", fmt.Errorf("version must be of the form v1.2.3") 437 } 438 439 func modulePathMajor(path string) (string, error) { 440 _, major, ok := module.SplitPathVersion(path) 441 if !ok { 442 return "", fmt.Errorf("invalid module path") 443 } 444 return major, nil 445 } 446 447 func (f *File) Format() ([]byte, error) { 448 return Format(f.Syntax), nil 449 } 450 451 // Cleanup cleans up the file f after any edit operations. 452 // To avoid quadratic behavior, modifications like DropRequire 453 // clear the entry but do not remove it from the slice. 454 // Cleanup cleans out all the cleared entries. 455 func (f *File) Cleanup() { 456 w := 0 457 for _, r := range f.Require { 458 if r.Mod.Path != "" { 459 f.Require[w] = r 460 w++ 461 } 462 } 463 f.Require = f.Require[:w] 464 465 w = 0 466 for _, x := range f.Exclude { 467 if x.Mod.Path != "" { 468 f.Exclude[w] = x 469 w++ 470 } 471 } 472 f.Exclude = f.Exclude[:w] 473 474 w = 0 475 for _, r := range f.Replace { 476 if r.Old.Path != "" { 477 f.Replace[w] = r 478 w++ 479 } 480 } 481 f.Replace = f.Replace[:w] 482 483 f.Syntax.Cleanup() 484 } 485 486 func IsValidExport(name string) bool { 487 return name != "" 488 } 489 490 func (f *File) AddExportsStmt(name string) error { 491 if !IsValidExport(name) { 492 return fmt.Errorf("invalid module exports string %q", name) 493 } 494 if f.Exports == nil { 495 f.Exports = &Exports{ 496 Name: name, 497 Syntax: f.Syntax.addLine(nil, "exports", name), 498 } 499 } else { 500 f.Exports.Name = name 501 f.Syntax.updateLine(f.Exports.Syntax, "exports", name) 502 } 503 return nil 504 } 505 506 func (f *File) AddRequire(path, vers string) error { 507 need := true 508 for _, r := range f.Require { 509 if r.Mod.Path == path { 510 if need { 511 r.Mod.Version = vers 512 f.Syntax.updateLine(r.Syntax, "require", AutoQuote(path), vers) 513 need = false 514 } else { 515 f.Syntax.removeLine(r.Syntax) 516 *r = Require{} 517 } 518 } 519 } 520 521 if need { 522 f.AddNewRequire(path, vers, false) 523 } 524 return nil 525 } 526 527 func (f *File) AddNewRequire(path, vers string, indirect bool) { 528 line := f.Syntax.addLine(nil, "require", AutoQuote(path), vers) 529 setIndirect(line, indirect) 530 f.Require = append(f.Require, &Require{module.Version{Path: path, Version: vers}, indirect, line}) 531 } 532 533 func (f *File) SetRequire(req []*Require) { 534 need := make(map[string]string) 535 indirect := make(map[string]bool) 536 for _, r := range req { 537 need[r.Mod.Path] = r.Mod.Version 538 indirect[r.Mod.Path] = r.Indirect 539 } 540 541 for _, r := range f.Require { 542 if v, ok := need[r.Mod.Path]; ok { 543 r.Mod.Version = v 544 r.Indirect = indirect[r.Mod.Path] 545 } 546 } 547 548 var newStmts []Expr 549 for _, stmt := range f.Syntax.Stmt { 550 switch stmt := stmt.(type) { 551 case *LineBlock: 552 if len(stmt.Token) > 0 && stmt.Token[0] == "require" { 553 var newLines []*Line 554 for _, line := range stmt.Line { 555 if p, err := parseString(&line.Token[0]); err == nil && need[p] != "" { 556 line.Token[1] = need[p] 557 delete(need, p) 558 setIndirect(line, indirect[p]) 559 newLines = append(newLines, line) 560 } 561 } 562 if len(newLines) == 0 { 563 continue // drop stmt 564 } 565 stmt.Line = newLines 566 } 567 568 case *Line: 569 if len(stmt.Token) > 0 && stmt.Token[0] == "require" { 570 if p, err := parseString(&stmt.Token[1]); err == nil && need[p] != "" { 571 stmt.Token[2] = need[p] 572 delete(need, p) 573 setIndirect(stmt, indirect[p]) 574 } else { 575 continue // drop stmt 576 } 577 } 578 } 579 newStmts = append(newStmts, stmt) 580 } 581 f.Syntax.Stmt = newStmts 582 583 for path, vers := range need { 584 f.AddNewRequire(path, vers, indirect[path]) 585 } 586 f.SortBlocks() 587 } 588 589 func (f *File) DropRequire(path string) error { 590 for _, r := range f.Require { 591 if r.Mod.Path == path { 592 f.Syntax.removeLine(r.Syntax) 593 *r = Require{} 594 } 595 } 596 return nil 597 } 598 599 func (f *File) AddExclude(path, vers string) error { 600 var hint *Line 601 for _, x := range f.Exclude { 602 if x.Mod.Path == path && x.Mod.Version == vers { 603 return nil 604 } 605 if x.Mod.Path == path { 606 hint = x.Syntax 607 } 608 } 609 610 f.Exclude = append(f.Exclude, &Exclude{Mod: module.Version{Path: path, Version: vers}, Syntax: f.Syntax.addLine(hint, "exclude", AutoQuote(path), vers)}) 611 return nil 612 } 613 614 func (f *File) DropExclude(path, vers string) error { 615 for _, x := range f.Exclude { 616 if x.Mod.Path == path && x.Mod.Version == vers { 617 f.Syntax.removeLine(x.Syntax) 618 *x = Exclude{} 619 } 620 } 621 return nil 622 } 623 624 func (f *File) AddReplace(oldPath, oldVers, newPath, newVers string) error { 625 need := true 626 old := module.Version{Path: oldPath, Version: oldVers} 627 new := module.Version{Path: newPath, Version: newVers} 628 tokens := []string{"replace", AutoQuote(oldPath)} 629 if oldVers != "" { 630 tokens = append(tokens, oldVers) 631 } 632 tokens = append(tokens, "=>", AutoQuote(newPath)) 633 if newVers != "" { 634 tokens = append(tokens, newVers) 635 } 636 637 var hint *Line 638 for _, r := range f.Replace { 639 if r.Old.Path == oldPath && (oldVers == "" || r.Old.Version == oldVers) { 640 if need { 641 // Found replacement for old; update to use new. 642 r.New = new 643 f.Syntax.updateLine(r.Syntax, tokens...) 644 need = false 645 continue 646 } 647 // Already added; delete other replacements for same. 648 f.Syntax.removeLine(r.Syntax) 649 *r = Replace{} 650 } 651 if r.Old.Path == oldPath { 652 hint = r.Syntax 653 } 654 } 655 if need { 656 f.Replace = append(f.Replace, &Replace{Old: old, New: new, Syntax: f.Syntax.addLine(hint, tokens...)}) 657 } 658 return nil 659 } 660 661 func (f *File) DropReplace(oldPath, oldVers string) error { 662 for _, r := range f.Replace { 663 if r.Old.Path == oldPath && r.Old.Version == oldVers { 664 f.Syntax.removeLine(r.Syntax) 665 *r = Replace{} 666 } 667 } 668 return nil 669 } 670 671 func (f *File) SortBlocks() { 672 f.removeDups() // otherwise sorting is unsafe 673 674 for _, stmt := range f.Syntax.Stmt { 675 block, ok := stmt.(*LineBlock) 676 if !ok { 677 continue 678 } 679 sort.Slice(block.Line, func(i, j int) bool { 680 li := block.Line[i] 681 lj := block.Line[j] 682 for k := 0; k < len(li.Token) && k < len(lj.Token); k++ { 683 if li.Token[k] != lj.Token[k] { 684 return li.Token[k] < lj.Token[k] 685 } 686 } 687 return len(li.Token) < len(lj.Token) 688 }) 689 } 690 } 691 692 func (f *File) removeDups() { 693 have := make(map[module.Version]bool) 694 kill := make(map[*Line]bool) 695 for _, x := range f.Exclude { 696 if have[x.Mod] { 697 kill[x.Syntax] = true 698 continue 699 } 700 have[x.Mod] = true 701 } 702 var excl []*Exclude 703 for _, x := range f.Exclude { 704 if !kill[x.Syntax] { 705 excl = append(excl, x) 706 } 707 } 708 f.Exclude = excl 709 710 have = make(map[module.Version]bool) 711 // Later replacements take priority over earlier ones. 712 for i := len(f.Replace) - 1; i >= 0; i-- { 713 x := f.Replace[i] 714 if have[x.Old] { 715 kill[x.Syntax] = true 716 continue 717 } 718 have[x.Old] = true 719 } 720 var repl []*Replace 721 for _, x := range f.Replace { 722 if !kill[x.Syntax] { 723 repl = append(repl, x) 724 } 725 } 726 f.Replace = repl 727 728 var stmts []Expr 729 for _, stmt := range f.Syntax.Stmt { 730 switch stmt := stmt.(type) { 731 case *Line: 732 if kill[stmt] { 733 continue 734 } 735 case *LineBlock: 736 var lines []*Line 737 for _, line := range stmt.Line { 738 if !kill[line] { 739 lines = append(lines, line) 740 } 741 } 742 stmt.Line = lines 743 if len(lines) == 0 { 744 continue 745 } 746 } 747 stmts = append(stmts, stmt) 748 } 749 f.Syntax.Stmt = stmts 750 }