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 implements a parser and formatter for go.mod files. 6// 7// The go.mod syntax is described in 8// https://pkg.go.dev/cmd/go/#hdr-The_go_mod_file. 9// 10// The [Parse] and [ParseLax] functions both parse a go.mod file and return an 11// abstract syntax tree. ParseLax ignores unknown statements and may be used to 12// parse go.mod files that may have been developed with newer versions of Go. 13// 14// The [File] struct returned by Parse and ParseLax represent an abstract 15// go.mod file. File has several methods like [File.AddNewRequire] and 16// [File.DropReplace] that can be used to programmatically edit a file. 17// 18// The [Format] function formats a File back to a byte slice which can be 19// written to a file. 20package modfile 21 22import ( 23 "errors" 24 "fmt" 25 "path/filepath" 26 "sort" 27 "strconv" 28 "strings" 29 "unicode" 30 31 "golang.org/x/mod/internal/lazyregexp" 32 "golang.org/x/mod/module" 33 "golang.org/x/mod/semver" 34) 35 36// A File is the parsed, interpreted form of a go.mod file. 37type File struct { 38 Module *Module 39 Go *Go 40 Toolchain *Toolchain 41 Godebug []*Godebug 42 Require []*Require 43 Exclude []*Exclude 44 Replace []*Replace 45 Retract []*Retract 46 47 Syntax *FileSyntax 48} 49 50// A Module is the module statement. 51type Module struct { 52 Mod module.Version 53 Deprecated string 54 Syntax *Line 55} 56 57// A Go is the go statement. 58type Go struct { 59 Version string // "1.23" 60 Syntax *Line 61} 62 63// A Toolchain is the toolchain statement. 64type Toolchain struct { 65 Name string // "go1.21rc1" 66 Syntax *Line 67} 68 69// A Godebug is a single godebug key=value statement. 70type Godebug struct { 71 Key string 72 Value string 73 Syntax *Line 74} 75 76// An Exclude is a single exclude statement. 77type Exclude struct { 78 Mod module.Version 79 Syntax *Line 80} 81 82// A Replace is a single replace statement. 83type Replace struct { 84 Old module.Version 85 New module.Version 86 Syntax *Line 87} 88 89// A Retract is a single retract statement. 90type Retract struct { 91 VersionInterval 92 Rationale string 93 Syntax *Line 94} 95 96// A VersionInterval represents a range of versions with upper and lower bounds. 97// Intervals are closed: both bounds are included. When Low is equal to High, 98// the interval may refer to a single version ('v1.2.3') or an interval 99// ('[v1.2.3, v1.2.3]'); both have the same representation. 100type VersionInterval struct { 101 Low, High string 102} 103 104// A Require is a single require statement. 105type Require struct { 106 Mod module.Version 107 Indirect bool // has "// indirect" comment 108 Syntax *Line 109} 110 111func (r *Require) markRemoved() { 112 r.Syntax.markRemoved() 113 *r = Require{} 114} 115 116func (r *Require) setVersion(v string) { 117 r.Mod.Version = v 118 119 if line := r.Syntax; len(line.Token) > 0 { 120 if line.InBlock { 121 // If the line is preceded by an empty line, remove it; see 122 // https://golang.org/issue/33779. 123 if len(line.Comments.Before) == 1 && len(line.Comments.Before[0].Token) == 0 { 124 line.Comments.Before = line.Comments.Before[:0] 125 } 126 if len(line.Token) >= 2 { // example.com v1.2.3 127 line.Token[1] = v 128 } 129 } else { 130 if len(line.Token) >= 3 { // require example.com v1.2.3 131 line.Token[2] = v 132 } 133 } 134 } 135} 136 137// setIndirect sets line to have (or not have) a "// indirect" comment. 138func (r *Require) setIndirect(indirect bool) { 139 r.Indirect = indirect 140 line := r.Syntax 141 if isIndirect(line) == indirect { 142 return 143 } 144 if indirect { 145 // Adding comment. 146 if len(line.Suffix) == 0 { 147 // New comment. 148 line.Suffix = []Comment{{Token: "// indirect", Suffix: true}} 149 return 150 } 151 152 com := &line.Suffix[0] 153 text := strings.TrimSpace(strings.TrimPrefix(com.Token, string(slashSlash))) 154 if text == "" { 155 // Empty comment. 156 com.Token = "// indirect" 157 return 158 } 159 160 // Insert at beginning of existing comment. 161 com.Token = "// indirect; " + text 162 return 163 } 164 165 // Removing comment. 166 f := strings.TrimSpace(strings.TrimPrefix(line.Suffix[0].Token, string(slashSlash))) 167 if f == "indirect" { 168 // Remove whole comment. 169 line.Suffix = nil 170 return 171 } 172 173 // Remove comment prefix. 174 com := &line.Suffix[0] 175 i := strings.Index(com.Token, "indirect;") 176 com.Token = "//" + com.Token[i+len("indirect;"):] 177} 178 179// isIndirect reports whether line has a "// indirect" comment, 180// meaning it is in go.mod only for its effect on indirect dependencies, 181// so that it can be dropped entirely once the effective version of the 182// indirect dependency reaches the given minimum version. 183func isIndirect(line *Line) bool { 184 if len(line.Suffix) == 0 { 185 return false 186 } 187 f := strings.Fields(strings.TrimPrefix(line.Suffix[0].Token, string(slashSlash))) 188 return (len(f) == 1 && f[0] == "indirect" || len(f) > 1 && f[0] == "indirect;") 189} 190 191func (f *File) AddModuleStmt(path string) error { 192 if f.Syntax == nil { 193 f.Syntax = new(FileSyntax) 194 } 195 if f.Module == nil { 196 f.Module = &Module{ 197 Mod: module.Version{Path: path}, 198 Syntax: f.Syntax.addLine(nil, "module", AutoQuote(path)), 199 } 200 } else { 201 f.Module.Mod.Path = path 202 f.Syntax.updateLine(f.Module.Syntax, "module", AutoQuote(path)) 203 } 204 return nil 205} 206 207func (f *File) AddComment(text string) { 208 if f.Syntax == nil { 209 f.Syntax = new(FileSyntax) 210 } 211 f.Syntax.Stmt = append(f.Syntax.Stmt, &CommentBlock{ 212 Comments: Comments{ 213 Before: []Comment{ 214 { 215 Token: text, 216 }, 217 }, 218 }, 219 }) 220} 221 222type VersionFixer func(path, version string) (string, error) 223 224// errDontFix is returned by a VersionFixer to indicate the version should be 225// left alone, even if it's not canonical. 226var dontFixRetract VersionFixer = func(_, vers string) (string, error) { 227 return vers, nil 228} 229 230// Parse parses and returns a go.mod file. 231// 232// file is the name of the file, used in positions and errors. 233// 234// data is the content of the file. 235// 236// fix is an optional function that canonicalizes module versions. 237// If fix is nil, all module versions must be canonical ([module.CanonicalVersion] 238// must return the same string). 239func Parse(file string, data []byte, fix VersionFixer) (*File, error) { 240 return parseToFile(file, data, fix, true) 241} 242 243// ParseLax is like Parse but ignores unknown statements. 244// It is used when parsing go.mod files other than the main module, 245// under the theory that most statement types we add in the future will 246// only apply in the main module, like exclude and replace, 247// and so we get better gradual deployments if old go commands 248// simply ignore those statements when found in go.mod files 249// in dependencies. 250func ParseLax(file string, data []byte, fix VersionFixer) (*File, error) { 251 return parseToFile(file, data, fix, false) 252} 253 254func parseToFile(file string, data []byte, fix VersionFixer, strict bool) (parsed *File, err error) { 255 fs, err := parse(file, data) 256 if err != nil { 257 return nil, err 258 } 259 f := &File{ 260 Syntax: fs, 261 } 262 var errs ErrorList 263 264 // fix versions in retract directives after the file is parsed. 265 // We need the module path to fix versions, and it might be at the end. 266 defer func() { 267 oldLen := len(errs) 268 f.fixRetract(fix, &errs) 269 if len(errs) > oldLen { 270 parsed, err = nil, errs 271 } 272 }() 273 274 for _, x := range fs.Stmt { 275 switch x := x.(type) { 276 case *Line: 277 f.add(&errs, nil, x, x.Token[0], x.Token[1:], fix, strict) 278 279 case *LineBlock: 280 if len(x.Token) > 1 { 281 if strict { 282 errs = append(errs, Error{ 283 Filename: file, 284 Pos: x.Start, 285 Err: fmt.Errorf("unknown block type: %s", strings.Join(x.Token, " ")), 286 }) 287 } 288 continue 289 } 290 switch x.Token[0] { 291 default: 292 if strict { 293 errs = append(errs, Error{ 294 Filename: file, 295 Pos: x.Start, 296 Err: fmt.Errorf("unknown block type: %s", strings.Join(x.Token, " ")), 297 }) 298 } 299 continue 300 case "module", "godebug", "require", "exclude", "replace", "retract": 301 for _, l := range x.Line { 302 f.add(&errs, x, l, x.Token[0], l.Token, fix, strict) 303 } 304 } 305 } 306 } 307 308 if len(errs) > 0 { 309 return nil, errs 310 } 311 return f, nil 312} 313 314var GoVersionRE = lazyregexp.New(`^([1-9][0-9]*)\.(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))?([a-z]+[0-9]+)?$`) 315var laxGoVersionRE = lazyregexp.New(`^v?(([1-9][0-9]*)\.(0|[1-9][0-9]*))([^0-9].*)$`) 316 317// Toolchains must be named beginning with `go1`, 318// like "go1.20.3" or "go1.20.3-gccgo". As a special case, "default" is also permitted. 319// Note that this regexp is a much looser condition than go/version.IsValid, 320// for forward compatibility. 321// (This code has to be work to identify new toolchains even if we tweak the syntax in the future.) 322var ToolchainRE = lazyregexp.New(`^default$|^go1($|\.)`) 323 324func (f *File) add(errs *ErrorList, block *LineBlock, line *Line, verb string, args []string, fix VersionFixer, strict bool) { 325 // If strict is false, this module is a dependency. 326 // We ignore all unknown directives as well as main-module-only 327 // directives like replace and exclude. It will work better for 328 // forward compatibility if we can depend on modules that have unknown 329 // statements (presumed relevant only when acting as the main module) 330 // and simply ignore those statements. 331 if !strict { 332 switch verb { 333 case "go", "module", "retract", "require": 334 // want these even for dependency go.mods 335 default: 336 return 337 } 338 } 339 340 wrapModPathError := func(modPath string, err error) { 341 *errs = append(*errs, Error{ 342 Filename: f.Syntax.Name, 343 Pos: line.Start, 344 ModPath: modPath, 345 Verb: verb, 346 Err: err, 347 }) 348 } 349 wrapError := func(err error) { 350 *errs = append(*errs, Error{ 351 Filename: f.Syntax.Name, 352 Pos: line.Start, 353 Err: err, 354 }) 355 } 356 errorf := func(format string, args ...interface{}) { 357 wrapError(fmt.Errorf(format, args...)) 358 } 359 360 switch verb { 361 default: 362 errorf("unknown directive: %s", verb) 363 364 case "go": 365 if f.Go != nil { 366 errorf("repeated go statement") 367 return 368 } 369 if len(args) != 1 { 370 errorf("go directive expects exactly one argument") 371 return 372 } else if !GoVersionRE.MatchString(args[0]) { 373 fixed := false 374 if !strict { 375 if m := laxGoVersionRE.FindStringSubmatch(args[0]); m != nil { 376 args[0] = m[1] 377 fixed = true 378 } 379 } 380 if !fixed { 381 errorf("invalid go version '%s': must match format 1.23.0", args[0]) 382 return 383 } 384 } 385 386 f.Go = &Go{Syntax: line} 387 f.Go.Version = args[0] 388 389 case "toolchain": 390 if f.Toolchain != nil { 391 errorf("repeated toolchain statement") 392 return 393 } 394 if len(args) != 1 { 395 errorf("toolchain directive expects exactly one argument") 396 return 397 } else if !ToolchainRE.MatchString(args[0]) { 398 errorf("invalid toolchain version '%s': must match format go1.23.0 or default", args[0]) 399 return 400 } 401 f.Toolchain = &Toolchain{Syntax: line} 402 f.Toolchain.Name = args[0] 403 404 case "module": 405 if f.Module != nil { 406 errorf("repeated module statement") 407 return 408 } 409 deprecated := parseDeprecation(block, line) 410 f.Module = &Module{ 411 Syntax: line, 412 Deprecated: deprecated, 413 } 414 if len(args) != 1 { 415 errorf("usage: module module/path") 416 return 417 } 418 s, err := parseString(&args[0]) 419 if err != nil { 420 errorf("invalid quoted string: %v", err) 421 return 422 } 423 f.Module.Mod = module.Version{Path: s} 424 425 case "godebug": 426 if len(args) != 1 || strings.ContainsAny(args[0], "\"`',") { 427 errorf("usage: godebug key=value") 428 return 429 } 430 key, value, ok := strings.Cut(args[0], "=") 431 if !ok { 432 errorf("usage: godebug key=value") 433 return 434 } 435 f.Godebug = append(f.Godebug, &Godebug{ 436 Key: key, 437 Value: value, 438 Syntax: line, 439 }) 440 441 case "require", "exclude": 442 if len(args) != 2 { 443 errorf("usage: %s module/path v1.2.3", verb) 444 return 445 } 446 s, err := parseString(&args[0]) 447 if err != nil { 448 errorf("invalid quoted string: %v", err) 449 return 450 } 451 v, err := parseVersion(verb, s, &args[1], fix) 452 if err != nil { 453 wrapError(err) 454 return 455 } 456 pathMajor, err := modulePathMajor(s) 457 if err != nil { 458 wrapError(err) 459 return 460 } 461 if err := module.CheckPathMajor(v, pathMajor); err != nil { 462 wrapModPathError(s, err) 463 return 464 } 465 if verb == "require" { 466 f.Require = append(f.Require, &Require{ 467 Mod: module.Version{Path: s, Version: v}, 468 Syntax: line, 469 Indirect: isIndirect(line), 470 }) 471 } else { 472 f.Exclude = append(f.Exclude, &Exclude{ 473 Mod: module.Version{Path: s, Version: v}, 474 Syntax: line, 475 }) 476 } 477 478 case "replace": 479 replace, wrappederr := parseReplace(f.Syntax.Name, line, verb, args, fix) 480 if wrappederr != nil { 481 *errs = append(*errs, *wrappederr) 482 return 483 } 484 f.Replace = append(f.Replace, replace) 485 486 case "retract": 487 rationale := parseDirectiveComment(block, line) 488 vi, err := parseVersionInterval(verb, "", &args, dontFixRetract) 489 if err != nil { 490 if strict { 491 wrapError(err) 492 return 493 } else { 494 // Only report errors parsing intervals in the main module. We may 495 // support additional syntax in the future, such as open and half-open 496 // intervals. Those can't be supported now, because they break the 497 // go.mod parser, even in lax mode. 498 return 499 } 500 } 501 if len(args) > 0 && strict { 502 // In the future, there may be additional information after the version. 503 errorf("unexpected token after version: %q", args[0]) 504 return 505 } 506 retract := &Retract{ 507 VersionInterval: vi, 508 Rationale: rationale, 509 Syntax: line, 510 } 511 f.Retract = append(f.Retract, retract) 512 } 513} 514 515func parseReplace(filename string, line *Line, verb string, args []string, fix VersionFixer) (*Replace, *Error) { 516 wrapModPathError := func(modPath string, err error) *Error { 517 return &Error{ 518 Filename: filename, 519 Pos: line.Start, 520 ModPath: modPath, 521 Verb: verb, 522 Err: err, 523 } 524 } 525 wrapError := func(err error) *Error { 526 return &Error{ 527 Filename: filename, 528 Pos: line.Start, 529 Err: err, 530 } 531 } 532 errorf := func(format string, args ...interface{}) *Error { 533 return wrapError(fmt.Errorf(format, args...)) 534 } 535 536 arrow := 2 537 if len(args) >= 2 && args[1] == "=>" { 538 arrow = 1 539 } 540 if len(args) < arrow+2 || len(args) > arrow+3 || args[arrow] != "=>" { 541 return nil, errorf("usage: %s module/path [v1.2.3] => other/module v1.4\n\t or %s module/path [v1.2.3] => ../local/directory", verb, verb) 542 } 543 s, err := parseString(&args[0]) 544 if err != nil { 545 return nil, errorf("invalid quoted string: %v", err) 546 } 547 pathMajor, err := modulePathMajor(s) 548 if err != nil { 549 return nil, wrapModPathError(s, err) 550 551 } 552 var v string 553 if arrow == 2 { 554 v, err = parseVersion(verb, s, &args[1], fix) 555 if err != nil { 556 return nil, wrapError(err) 557 } 558 if err := module.CheckPathMajor(v, pathMajor); err != nil { 559 return nil, wrapModPathError(s, err) 560 } 561 } 562 ns, err := parseString(&args[arrow+1]) 563 if err != nil { 564 return nil, errorf("invalid quoted string: %v", err) 565 } 566 nv := "" 567 if len(args) == arrow+2 { 568 if !IsDirectoryPath(ns) { 569 if strings.Contains(ns, "@") { 570 return nil, errorf("replacement module must match format 'path version', not 'path@version'") 571 } 572 return nil, errorf("replacement module without version must be directory path (rooted or starting with . or ..)") 573 } 574 if filepath.Separator == '/' && strings.Contains(ns, `\`) { 575 return nil, errorf("replacement directory appears to be Windows path (on a non-windows system)") 576 } 577 } 578 if len(args) == arrow+3 { 579 nv, err = parseVersion(verb, ns, &args[arrow+2], fix) 580 if err != nil { 581 return nil, wrapError(err) 582 } 583 if IsDirectoryPath(ns) { 584 return nil, errorf("replacement module directory path %q cannot have version", ns) 585 } 586 } 587 return &Replace{ 588 Old: module.Version{Path: s, Version: v}, 589 New: module.Version{Path: ns, Version: nv}, 590 Syntax: line, 591 }, nil 592} 593 594// fixRetract applies fix to each retract directive in f, appending any errors 595// to errs. 596// 597// Most versions are fixed as we parse the file, but for retract directives, 598// the relevant module path is the one specified with the module directive, 599// and that might appear at the end of the file (or not at all). 600func (f *File) fixRetract(fix VersionFixer, errs *ErrorList) { 601 if fix == nil { 602 return 603 } 604 path := "" 605 if f.Module != nil { 606 path = f.Module.Mod.Path 607 } 608 var r *Retract 609 wrapError := func(err error) { 610 *errs = append(*errs, Error{ 611 Filename: f.Syntax.Name, 612 Pos: r.Syntax.Start, 613 Err: err, 614 }) 615 } 616 617 for _, r = range f.Retract { 618 if path == "" { 619 wrapError(errors.New("no module directive found, so retract cannot be used")) 620 return // only print the first one of these 621 } 622 623 args := r.Syntax.Token 624 if args[0] == "retract" { 625 args = args[1:] 626 } 627 vi, err := parseVersionInterval("retract", path, &args, fix) 628 if err != nil { 629 wrapError(err) 630 } 631 r.VersionInterval = vi 632 } 633} 634 635func (f *WorkFile) add(errs *ErrorList, line *Line, verb string, args []string, fix VersionFixer) { 636 wrapError := func(err error) { 637 *errs = append(*errs, Error{ 638 Filename: f.Syntax.Name, 639 Pos: line.Start, 640 Err: err, 641 }) 642 } 643 errorf := func(format string, args ...interface{}) { 644 wrapError(fmt.Errorf(format, args...)) 645 } 646 647 switch verb { 648 default: 649 errorf("unknown directive: %s", verb) 650 651 case "go": 652 if f.Go != nil { 653 errorf("repeated go statement") 654 return 655 } 656 if len(args) != 1 { 657 errorf("go directive expects exactly one argument") 658 return 659 } else if !GoVersionRE.MatchString(args[0]) { 660 errorf("invalid go version '%s': must match format 1.23.0", args[0]) 661 return 662 } 663 664 f.Go = &Go{Syntax: line} 665 f.Go.Version = args[0] 666 667 case "toolchain": 668 if f.Toolchain != nil { 669 errorf("repeated toolchain statement") 670 return 671 } 672 if len(args) != 1 { 673 errorf("toolchain directive expects exactly one argument") 674 return 675 } else if !ToolchainRE.MatchString(args[0]) { 676 errorf("invalid toolchain version '%s': must match format go1.23.0 or default", args[0]) 677 return 678 } 679 680 f.Toolchain = &Toolchain{Syntax: line} 681 f.Toolchain.Name = args[0] 682 683 case "godebug": 684 if len(args) != 1 || strings.ContainsAny(args[0], "\"`',") { 685 errorf("usage: godebug key=value") 686 return 687 } 688 key, value, ok := strings.Cut(args[0], "=") 689 if !ok { 690 errorf("usage: godebug key=value") 691 return 692 } 693 f.Godebug = append(f.Godebug, &Godebug{ 694 Key: key, 695 Value: value, 696 Syntax: line, 697 }) 698 699 case "use": 700 if len(args) != 1 { 701 errorf("usage: %s local/dir", verb) 702 return 703 } 704 s, err := parseString(&args[0]) 705 if err != nil { 706 errorf("invalid quoted string: %v", err) 707 return 708 } 709 f.Use = append(f.Use, &Use{ 710 Path: s, 711 Syntax: line, 712 }) 713 714 case "replace": 715 replace, wrappederr := parseReplace(f.Syntax.Name, line, verb, args, fix) 716 if wrappederr != nil { 717 *errs = append(*errs, *wrappederr) 718 return 719 } 720 f.Replace = append(f.Replace, replace) 721 } 722} 723 724// IsDirectoryPath reports whether the given path should be interpreted as a directory path. 725// Just like on the go command line, relative paths starting with a '.' or '..' path component 726// and rooted paths are directory paths; the rest are module paths. 727func IsDirectoryPath(ns string) bool { 728 // Because go.mod files can move from one system to another, 729 // we check all known path syntaxes, both Unix and Windows. 730 return ns == "." || strings.HasPrefix(ns, "./") || strings.HasPrefix(ns, `.\`) || 731 ns == ".." || strings.HasPrefix(ns, "../") || strings.HasPrefix(ns, `..\`) || 732 strings.HasPrefix(ns, "/") || strings.HasPrefix(ns, `\`) || 733 len(ns) >= 2 && ('A' <= ns[0] && ns[0] <= 'Z' || 'a' <= ns[0] && ns[0] <= 'z') && ns[1] == ':' 734} 735 736// MustQuote reports whether s must be quoted in order to appear as 737// a single token in a go.mod line. 738func MustQuote(s string) bool { 739 for _, r := range s { 740 switch r { 741 case ' ', '"', '\'', '`': 742 return true 743 744 case '(', ')', '[', ']', '{', '}', ',': 745 if len(s) > 1 { 746 return true 747 } 748 749 default: 750 if !unicode.IsPrint(r) { 751 return true 752 } 753 } 754 } 755 return s == "" || strings.Contains(s, "//") || strings.Contains(s, "/*") 756} 757 758// AutoQuote returns s or, if quoting is required for s to appear in a go.mod, 759// the quotation of s. 760func AutoQuote(s string) string { 761 if MustQuote(s) { 762 return strconv.Quote(s) 763 } 764 return s 765} 766 767func parseVersionInterval(verb string, path string, args *[]string, fix VersionFixer) (VersionInterval, error) { 768 toks := *args 769 if len(toks) == 0 || toks[0] == "(" { 770 return VersionInterval{}, fmt.Errorf("expected '[' or version") 771 } 772 if toks[0] != "[" { 773 v, err := parseVersion(verb, path, &toks[0], fix) 774 if err != nil { 775 return VersionInterval{}, err 776 } 777 *args = toks[1:] 778 return VersionInterval{Low: v, High: v}, nil 779 } 780 toks = toks[1:] 781 782 if len(toks) == 0 { 783 return VersionInterval{}, fmt.Errorf("expected version after '['") 784 } 785 low, err := parseVersion(verb, path, &toks[0], fix) 786 if err != nil { 787 return VersionInterval{}, err 788 } 789 toks = toks[1:] 790 791 if len(toks) == 0 || toks[0] != "," { 792 return VersionInterval{}, fmt.Errorf("expected ',' after version") 793 } 794 toks = toks[1:] 795 796 if len(toks) == 0 { 797 return VersionInterval{}, fmt.Errorf("expected version after ','") 798 } 799 high, err := parseVersion(verb, path, &toks[0], fix) 800 if err != nil { 801 return VersionInterval{}, err 802 } 803 toks = toks[1:] 804 805 if len(toks) == 0 || toks[0] != "]" { 806 return VersionInterval{}, fmt.Errorf("expected ']' after version") 807 } 808 toks = toks[1:] 809 810 *args = toks 811 return VersionInterval{Low: low, High: high}, nil 812} 813 814func parseString(s *string) (string, error) { 815 t := *s 816 if strings.HasPrefix(t, `"`) { 817 var err error 818 if t, err = strconv.Unquote(t); err != nil { 819 return "", err 820 } 821 } else if strings.ContainsAny(t, "\"'`") { 822 // Other quotes are reserved both for possible future expansion 823 // and to avoid confusion. For example if someone types 'x' 824 // we want that to be a syntax error and not a literal x in literal quotation marks. 825 return "", fmt.Errorf("unquoted string cannot contain quote") 826 } 827 *s = AutoQuote(t) 828 return t, nil 829} 830 831var deprecatedRE = lazyregexp.New(`(?s)(?:^|\n\n)Deprecated: *(.*?)(?:$|\n\n)`) 832 833// parseDeprecation extracts the text of comments on a "module" directive and 834// extracts a deprecation message from that. 835// 836// A deprecation message is contained in a paragraph within a block of comments 837// that starts with "Deprecated:" (case sensitive). The message runs until the 838// end of the paragraph and does not include the "Deprecated:" prefix. If the 839// comment block has multiple paragraphs that start with "Deprecated:", 840// parseDeprecation returns the message from the first. 841func parseDeprecation(block *LineBlock, line *Line) string { 842 text := parseDirectiveComment(block, line) 843 m := deprecatedRE.FindStringSubmatch(text) 844 if m == nil { 845 return "" 846 } 847 return m[1] 848} 849 850// parseDirectiveComment extracts the text of comments on a directive. 851// If the directive's line does not have comments and is part of a block that 852// does have comments, the block's comments are used. 853func parseDirectiveComment(block *LineBlock, line *Line) string { 854 comments := line.Comment() 855 if block != nil && len(comments.Before) == 0 && len(comments.Suffix) == 0 { 856 comments = block.Comment() 857 } 858 groups := [][]Comment{comments.Before, comments.Suffix} 859 var lines []string 860 for _, g := range groups { 861 for _, c := range g { 862 if !strings.HasPrefix(c.Token, "//") { 863 continue // blank line 864 } 865 lines = append(lines, strings.TrimSpace(strings.TrimPrefix(c.Token, "//"))) 866 } 867 } 868 return strings.Join(lines, "\n") 869} 870 871type ErrorList []Error 872 873func (e ErrorList) Error() string { 874 errStrs := make([]string, len(e)) 875 for i, err := range e { 876 errStrs[i] = err.Error() 877 } 878 return strings.Join(errStrs, "\n") 879} 880 881type Error struct { 882 Filename string 883 Pos Position 884 Verb string 885 ModPath string 886 Err error 887} 888 889func (e *Error) Error() string { 890 var pos string 891 if e.Pos.LineRune > 1 { 892 // Don't print LineRune if it's 1 (beginning of line). 893 // It's always 1 except in scanner errors, which are rare. 894 pos = fmt.Sprintf("%s:%d:%d: ", e.Filename, e.Pos.Line, e.Pos.LineRune) 895 } else if e.Pos.Line > 0 { 896 pos = fmt.Sprintf("%s:%d: ", e.Filename, e.Pos.Line) 897 } else if e.Filename != "" { 898 pos = fmt.Sprintf("%s: ", e.Filename) 899 } 900 901 var directive string 902 if e.ModPath != "" { 903 directive = fmt.Sprintf("%s %s: ", e.Verb, e.ModPath) 904 } else if e.Verb != "" { 905 directive = fmt.Sprintf("%s: ", e.Verb) 906 } 907 908 return pos + directive + e.Err.Error() 909} 910 911func (e *Error) Unwrap() error { return e.Err } 912 913func parseVersion(verb string, path string, s *string, fix VersionFixer) (string, error) { 914 t, err := parseString(s) 915 if err != nil { 916 return "", &Error{ 917 Verb: verb, 918 ModPath: path, 919 Err: &module.InvalidVersionError{ 920 Version: *s, 921 Err: err, 922 }, 923 } 924 } 925 if fix != nil { 926 fixed, err := fix(path, t) 927 if err != nil { 928 if err, ok := err.(*module.ModuleError); ok { 929 return "", &Error{ 930 Verb: verb, 931 ModPath: path, 932 Err: err.Err, 933 } 934 } 935 return "", err 936 } 937 t = fixed 938 } else { 939 cv := module.CanonicalVersion(t) 940 if cv == "" { 941 return "", &Error{ 942 Verb: verb, 943 ModPath: path, 944 Err: &module.InvalidVersionError{ 945 Version: t, 946 Err: errors.New("must be of the form v1.2.3"), 947 }, 948 } 949 } 950 t = cv 951 } 952 *s = t 953 return *s, nil 954} 955 956func modulePathMajor(path string) (string, error) { 957 _, major, ok := module.SplitPathVersion(path) 958 if !ok { 959 return "", fmt.Errorf("invalid module path") 960 } 961 return major, nil 962} 963 964func (f *File) Format() ([]byte, error) { 965 return Format(f.Syntax), nil 966} 967 968// Cleanup cleans up the file f after any edit operations. 969// To avoid quadratic behavior, modifications like [File.DropRequire] 970// clear the entry but do not remove it from the slice. 971// Cleanup cleans out all the cleared entries. 972func (f *File) Cleanup() { 973 w := 0 974 for _, g := range f.Godebug { 975 if g.Key != "" { 976 f.Godebug[w] = g 977 w++ 978 } 979 } 980 f.Godebug = f.Godebug[:w] 981 982 w = 0 983 for _, r := range f.Require { 984 if r.Mod.Path != "" { 985 f.Require[w] = r 986 w++ 987 } 988 } 989 f.Require = f.Require[:w] 990 991 w = 0 992 for _, x := range f.Exclude { 993 if x.Mod.Path != "" { 994 f.Exclude[w] = x 995 w++ 996 } 997 } 998 f.Exclude = f.Exclude[:w] 999 1000 w = 0 1001 for _, r := range f.Replace { 1002 if r.Old.Path != "" { 1003 f.Replace[w] = r 1004 w++ 1005 } 1006 } 1007 f.Replace = f.Replace[:w] 1008 1009 w = 0 1010 for _, r := range f.Retract { 1011 if r.Low != "" || r.High != "" { 1012 f.Retract[w] = r 1013 w++ 1014 } 1015 } 1016 f.Retract = f.Retract[:w] 1017 1018 f.Syntax.Cleanup() 1019} 1020 1021func (f *File) AddGoStmt(version string) error { 1022 if !GoVersionRE.MatchString(version) { 1023 return fmt.Errorf("invalid language version %q", version) 1024 } 1025 if f.Go == nil { 1026 var hint Expr 1027 if f.Module != nil && f.Module.Syntax != nil { 1028 hint = f.Module.Syntax 1029 } else if f.Syntax == nil { 1030 f.Syntax = new(FileSyntax) 1031 } 1032 f.Go = &Go{ 1033 Version: version, 1034 Syntax: f.Syntax.addLine(hint, "go", version), 1035 } 1036 } else { 1037 f.Go.Version = version 1038 f.Syntax.updateLine(f.Go.Syntax, "go", version) 1039 } 1040 return nil 1041} 1042 1043// DropGoStmt deletes the go statement from the file. 1044func (f *File) DropGoStmt() { 1045 if f.Go != nil { 1046 f.Go.Syntax.markRemoved() 1047 f.Go = nil 1048 } 1049} 1050 1051// DropToolchainStmt deletes the toolchain statement from the file. 1052func (f *File) DropToolchainStmt() { 1053 if f.Toolchain != nil { 1054 f.Toolchain.Syntax.markRemoved() 1055 f.Toolchain = nil 1056 } 1057} 1058 1059func (f *File) AddToolchainStmt(name string) error { 1060 if !ToolchainRE.MatchString(name) { 1061 return fmt.Errorf("invalid toolchain name %q", name) 1062 } 1063 if f.Toolchain == nil { 1064 var hint Expr 1065 if f.Go != nil && f.Go.Syntax != nil { 1066 hint = f.Go.Syntax 1067 } else if f.Module != nil && f.Module.Syntax != nil { 1068 hint = f.Module.Syntax 1069 } 1070 f.Toolchain = &Toolchain{ 1071 Name: name, 1072 Syntax: f.Syntax.addLine(hint, "toolchain", name), 1073 } 1074 } else { 1075 f.Toolchain.Name = name 1076 f.Syntax.updateLine(f.Toolchain.Syntax, "toolchain", name) 1077 } 1078 return nil 1079} 1080 1081// AddGodebug sets the first godebug line for key to value, 1082// preserving any existing comments for that line and removing all 1083// other godebug lines for key. 1084// 1085// If no line currently exists for key, AddGodebug adds a new line 1086// at the end of the last godebug block. 1087func (f *File) AddGodebug(key, value string) error { 1088 need := true 1089 for _, g := range f.Godebug { 1090 if g.Key == key { 1091 if need { 1092 g.Value = value 1093 f.Syntax.updateLine(g.Syntax, "godebug", key+"="+value) 1094 need = false 1095 } else { 1096 g.Syntax.markRemoved() 1097 *g = Godebug{} 1098 } 1099 } 1100 } 1101 1102 if need { 1103 f.addNewGodebug(key, value) 1104 } 1105 return nil 1106} 1107 1108// addNewGodebug adds a new godebug key=value line at the end 1109// of the last godebug block, regardless of any existing godebug lines for key. 1110func (f *File) addNewGodebug(key, value string) { 1111 line := f.Syntax.addLine(nil, "godebug", key+"="+value) 1112 g := &Godebug{ 1113 Key: key, 1114 Value: value, 1115 Syntax: line, 1116 } 1117 f.Godebug = append(f.Godebug, g) 1118} 1119 1120// AddRequire sets the first require line for path to version vers, 1121// preserving any existing comments for that line and removing all 1122// other lines for path. 1123// 1124// If no line currently exists for path, AddRequire adds a new line 1125// at the end of the last require block. 1126func (f *File) AddRequire(path, vers string) error { 1127 need := true 1128 for _, r := range f.Require { 1129 if r.Mod.Path == path { 1130 if need { 1131 r.Mod.Version = vers 1132 f.Syntax.updateLine(r.Syntax, "require", AutoQuote(path), vers) 1133 need = false 1134 } else { 1135 r.Syntax.markRemoved() 1136 *r = Require{} 1137 } 1138 } 1139 } 1140 1141 if need { 1142 f.AddNewRequire(path, vers, false) 1143 } 1144 return nil 1145} 1146 1147// AddNewRequire adds a new require line for path at version vers at the end of 1148// the last require block, regardless of any existing require lines for path. 1149func (f *File) AddNewRequire(path, vers string, indirect bool) { 1150 line := f.Syntax.addLine(nil, "require", AutoQuote(path), vers) 1151 r := &Require{ 1152 Mod: module.Version{Path: path, Version: vers}, 1153 Syntax: line, 1154 } 1155 r.setIndirect(indirect) 1156 f.Require = append(f.Require, r) 1157} 1158 1159// SetRequire updates the requirements of f to contain exactly req, preserving 1160// the existing block structure and line comment contents (except for 'indirect' 1161// markings) for the first requirement on each named module path. 1162// 1163// The Syntax field is ignored for the requirements in req. 1164// 1165// Any requirements not already present in the file are added to the block 1166// containing the last require line. 1167// 1168// The requirements in req must specify at most one distinct version for each 1169// module path. 1170// 1171// If any existing requirements may be removed, the caller should call 1172// [File.Cleanup] after all edits are complete. 1173func (f *File) SetRequire(req []*Require) { 1174 type elem struct { 1175 version string 1176 indirect bool 1177 } 1178 need := make(map[string]elem) 1179 for _, r := range req { 1180 if prev, dup := need[r.Mod.Path]; dup && prev.version != r.Mod.Version { 1181 panic(fmt.Errorf("SetRequire called with conflicting versions for path %s (%s and %s)", r.Mod.Path, prev.version, r.Mod.Version)) 1182 } 1183 need[r.Mod.Path] = elem{r.Mod.Version, r.Indirect} 1184 } 1185 1186 // Update or delete the existing Require entries to preserve 1187 // only the first for each module path in req. 1188 for _, r := range f.Require { 1189 e, ok := need[r.Mod.Path] 1190 if ok { 1191 r.setVersion(e.version) 1192 r.setIndirect(e.indirect) 1193 } else { 1194 r.markRemoved() 1195 } 1196 delete(need, r.Mod.Path) 1197 } 1198 1199 // Add new entries in the last block of the file for any paths that weren't 1200 // already present. 1201 // 1202 // This step is nondeterministic, but the final result will be deterministic 1203 // because we will sort the block. 1204 for path, e := range need { 1205 f.AddNewRequire(path, e.version, e.indirect) 1206 } 1207 1208 f.SortBlocks() 1209} 1210 1211// SetRequireSeparateIndirect updates the requirements of f to contain the given 1212// requirements. Comment contents (except for 'indirect' markings) are retained 1213// from the first existing requirement for each module path. Like SetRequire, 1214// SetRequireSeparateIndirect adds requirements for new paths in req, 1215// updates the version and "// indirect" comment on existing requirements, 1216// and deletes requirements on paths not in req. Existing duplicate requirements 1217// are deleted. 1218// 1219// As its name suggests, SetRequireSeparateIndirect puts direct and indirect 1220// requirements into two separate blocks, one containing only direct 1221// requirements, and the other containing only indirect requirements. 1222// SetRequireSeparateIndirect may move requirements between these two blocks 1223// when their indirect markings change. However, SetRequireSeparateIndirect 1224// won't move requirements from other blocks, especially blocks with comments. 1225// 1226// If the file initially has one uncommented block of requirements, 1227// SetRequireSeparateIndirect will split it into a direct-only and indirect-only 1228// block. This aids in the transition to separate blocks. 1229func (f *File) SetRequireSeparateIndirect(req []*Require) { 1230 // hasComments returns whether a line or block has comments 1231 // other than "indirect". 1232 hasComments := func(c Comments) bool { 1233 return len(c.Before) > 0 || len(c.After) > 0 || len(c.Suffix) > 1 || 1234 (len(c.Suffix) == 1 && 1235 strings.TrimSpace(strings.TrimPrefix(c.Suffix[0].Token, string(slashSlash))) != "indirect") 1236 } 1237 1238 // moveReq adds r to block. If r was in another block, moveReq deletes 1239 // it from that block and transfers its comments. 1240 moveReq := func(r *Require, block *LineBlock) { 1241 var line *Line 1242 if r.Syntax == nil { 1243 line = &Line{Token: []string{AutoQuote(r.Mod.Path), r.Mod.Version}} 1244 r.Syntax = line 1245 if r.Indirect { 1246 r.setIndirect(true) 1247 } 1248 } else { 1249 line = new(Line) 1250 *line = *r.Syntax 1251 if !line.InBlock && len(line.Token) > 0 && line.Token[0] == "require" { 1252 line.Token = line.Token[1:] 1253 } 1254 r.Syntax.Token = nil // Cleanup will delete the old line. 1255 r.Syntax = line 1256 } 1257 line.InBlock = true 1258 block.Line = append(block.Line, line) 1259 } 1260 1261 // Examine existing require lines and blocks. 1262 var ( 1263 // We may insert new requirements into the last uncommented 1264 // direct-only and indirect-only blocks. We may also move requirements 1265 // to the opposite block if their indirect markings change. 1266 lastDirectIndex = -1 1267 lastIndirectIndex = -1 1268 1269 // If there are no direct-only or indirect-only blocks, a new block may 1270 // be inserted after the last require line or block. 1271 lastRequireIndex = -1 1272 1273 // If there's only one require line or block, and it's uncommented, 1274 // we'll move its requirements to the direct-only or indirect-only blocks. 1275 requireLineOrBlockCount = 0 1276 1277 // Track the block each requirement belongs to (if any) so we can 1278 // move them later. 1279 lineToBlock = make(map[*Line]*LineBlock) 1280 ) 1281 for i, stmt := range f.Syntax.Stmt { 1282 switch stmt := stmt.(type) { 1283 case *Line: 1284 if len(stmt.Token) == 0 || stmt.Token[0] != "require" { 1285 continue 1286 } 1287 lastRequireIndex = i 1288 requireLineOrBlockCount++ 1289 if !hasComments(stmt.Comments) { 1290 if isIndirect(stmt) { 1291 lastIndirectIndex = i 1292 } else { 1293 lastDirectIndex = i 1294 } 1295 } 1296 1297 case *LineBlock: 1298 if len(stmt.Token) == 0 || stmt.Token[0] != "require" { 1299 continue 1300 } 1301 lastRequireIndex = i 1302 requireLineOrBlockCount++ 1303 allDirect := len(stmt.Line) > 0 && !hasComments(stmt.Comments) 1304 allIndirect := len(stmt.Line) > 0 && !hasComments(stmt.Comments) 1305 for _, line := range stmt.Line { 1306 lineToBlock[line] = stmt 1307 if hasComments(line.Comments) { 1308 allDirect = false 1309 allIndirect = false 1310 } else if isIndirect(line) { 1311 allDirect = false 1312 } else { 1313 allIndirect = false 1314 } 1315 } 1316 if allDirect { 1317 lastDirectIndex = i 1318 } 1319 if allIndirect { 1320 lastIndirectIndex = i 1321 } 1322 } 1323 } 1324 1325 oneFlatUncommentedBlock := requireLineOrBlockCount == 1 && 1326 !hasComments(*f.Syntax.Stmt[lastRequireIndex].Comment()) 1327 1328 // Create direct and indirect blocks if needed. Convert lines into blocks 1329 // if needed. If we end up with an empty block or a one-line block, 1330 // Cleanup will delete it or convert it to a line later. 1331 insertBlock := func(i int) *LineBlock { 1332 block := &LineBlock{Token: []string{"require"}} 1333 f.Syntax.Stmt = append(f.Syntax.Stmt, nil) 1334 copy(f.Syntax.Stmt[i+1:], f.Syntax.Stmt[i:]) 1335 f.Syntax.Stmt[i] = block 1336 return block 1337 } 1338 1339 ensureBlock := func(i int) *LineBlock { 1340 switch stmt := f.Syntax.Stmt[i].(type) { 1341 case *LineBlock: 1342 return stmt 1343 case *Line: 1344 block := &LineBlock{ 1345 Token: []string{"require"}, 1346 Line: []*Line{stmt}, 1347 } 1348 stmt.Token = stmt.Token[1:] // remove "require" 1349 stmt.InBlock = true 1350 f.Syntax.Stmt[i] = block 1351 return block 1352 default: 1353 panic(fmt.Sprintf("unexpected statement: %v", stmt)) 1354 } 1355 } 1356 1357 var lastDirectBlock *LineBlock 1358 if lastDirectIndex < 0 { 1359 if lastIndirectIndex >= 0 { 1360 lastDirectIndex = lastIndirectIndex 1361 lastIndirectIndex++ 1362 } else if lastRequireIndex >= 0 { 1363 lastDirectIndex = lastRequireIndex + 1 1364 } else { 1365 lastDirectIndex = len(f.Syntax.Stmt) 1366 } 1367 lastDirectBlock = insertBlock(lastDirectIndex) 1368 } else { 1369 lastDirectBlock = ensureBlock(lastDirectIndex) 1370 } 1371 1372 var lastIndirectBlock *LineBlock 1373 if lastIndirectIndex < 0 { 1374 lastIndirectIndex = lastDirectIndex + 1 1375 lastIndirectBlock = insertBlock(lastIndirectIndex) 1376 } else { 1377 lastIndirectBlock = ensureBlock(lastIndirectIndex) 1378 } 1379 1380 // Delete requirements we don't want anymore. 1381 // Update versions and indirect comments on requirements we want to keep. 1382 // If a requirement is in last{Direct,Indirect}Block with the wrong 1383 // indirect marking after this, or if the requirement is in an single 1384 // uncommented mixed block (oneFlatUncommentedBlock), move it to the 1385 // correct block. 1386 // 1387 // Some blocks may be empty after this. Cleanup will remove them. 1388 need := make(map[string]*Require) 1389 for _, r := range req { 1390 need[r.Mod.Path] = r 1391 } 1392 have := make(map[string]*Require) 1393 for _, r := range f.Require { 1394 path := r.Mod.Path 1395 if need[path] == nil || have[path] != nil { 1396 // Requirement not needed, or duplicate requirement. Delete. 1397 r.markRemoved() 1398 continue 1399 } 1400 have[r.Mod.Path] = r 1401 r.setVersion(need[path].Mod.Version) 1402 r.setIndirect(need[path].Indirect) 1403 if need[path].Indirect && 1404 (oneFlatUncommentedBlock || lineToBlock[r.Syntax] == lastDirectBlock) { 1405 moveReq(r, lastIndirectBlock) 1406 } else if !need[path].Indirect && 1407 (oneFlatUncommentedBlock || lineToBlock[r.Syntax] == lastIndirectBlock) { 1408 moveReq(r, lastDirectBlock) 1409 } 1410 } 1411 1412 // Add new requirements. 1413 for path, r := range need { 1414 if have[path] == nil { 1415 if r.Indirect { 1416 moveReq(r, lastIndirectBlock) 1417 } else { 1418 moveReq(r, lastDirectBlock) 1419 } 1420 f.Require = append(f.Require, r) 1421 } 1422 } 1423 1424 f.SortBlocks() 1425} 1426 1427func (f *File) DropGodebug(key string) error { 1428 for _, g := range f.Godebug { 1429 if g.Key == key { 1430 g.Syntax.markRemoved() 1431 *g = Godebug{} 1432 } 1433 } 1434 return nil 1435} 1436 1437func (f *File) DropRequire(path string) error { 1438 for _, r := range f.Require { 1439 if r.Mod.Path == path { 1440 r.Syntax.markRemoved() 1441 *r = Require{} 1442 } 1443 } 1444 return nil 1445} 1446 1447// AddExclude adds a exclude statement to the mod file. Errors if the provided 1448// version is not a canonical version string 1449func (f *File) AddExclude(path, vers string) error { 1450 if err := checkCanonicalVersion(path, vers); err != nil { 1451 return err 1452 } 1453 1454 var hint *Line 1455 for _, x := range f.Exclude { 1456 if x.Mod.Path == path && x.Mod.Version == vers { 1457 return nil 1458 } 1459 if x.Mod.Path == path { 1460 hint = x.Syntax 1461 } 1462 } 1463 1464 f.Exclude = append(f.Exclude, &Exclude{Mod: module.Version{Path: path, Version: vers}, Syntax: f.Syntax.addLine(hint, "exclude", AutoQuote(path), vers)}) 1465 return nil 1466} 1467 1468func (f *File) DropExclude(path, vers string) error { 1469 for _, x := range f.Exclude { 1470 if x.Mod.Path == path && x.Mod.Version == vers { 1471 x.Syntax.markRemoved() 1472 *x = Exclude{} 1473 } 1474 } 1475 return nil 1476} 1477 1478func (f *File) AddReplace(oldPath, oldVers, newPath, newVers string) error { 1479 return addReplace(f.Syntax, &f.Replace, oldPath, oldVers, newPath, newVers) 1480} 1481 1482func addReplace(syntax *FileSyntax, replace *[]*Replace, oldPath, oldVers, newPath, newVers string) error { 1483 need := true 1484 old := module.Version{Path: oldPath, Version: oldVers} 1485 new := module.Version{Path: newPath, Version: newVers} 1486 tokens := []string{"replace", AutoQuote(oldPath)} 1487 if oldVers != "" { 1488 tokens = append(tokens, oldVers) 1489 } 1490 tokens = append(tokens, "=>", AutoQuote(newPath)) 1491 if newVers != "" { 1492 tokens = append(tokens, newVers) 1493 } 1494 1495 var hint *Line 1496 for _, r := range *replace { 1497 if r.Old.Path == oldPath && (oldVers == "" || r.Old.Version == oldVers) { 1498 if need { 1499 // Found replacement for old; update to use new. 1500 r.New = new 1501 syntax.updateLine(r.Syntax, tokens...) 1502 need = false 1503 continue 1504 } 1505 // Already added; delete other replacements for same. 1506 r.Syntax.markRemoved() 1507 *r = Replace{} 1508 } 1509 if r.Old.Path == oldPath { 1510 hint = r.Syntax 1511 } 1512 } 1513 if need { 1514 *replace = append(*replace, &Replace{Old: old, New: new, Syntax: syntax.addLine(hint, tokens...)}) 1515 } 1516 return nil 1517} 1518 1519func (f *File) DropReplace(oldPath, oldVers string) error { 1520 for _, r := range f.Replace { 1521 if r.Old.Path == oldPath && r.Old.Version == oldVers { 1522 r.Syntax.markRemoved() 1523 *r = Replace{} 1524 } 1525 } 1526 return nil 1527} 1528 1529// AddRetract adds a retract statement to the mod file. Errors if the provided 1530// version interval does not consist of canonical version strings 1531func (f *File) AddRetract(vi VersionInterval, rationale string) error { 1532 var path string 1533 if f.Module != nil { 1534 path = f.Module.Mod.Path 1535 } 1536 if err := checkCanonicalVersion(path, vi.High); err != nil { 1537 return err 1538 } 1539 if err := checkCanonicalVersion(path, vi.Low); err != nil { 1540 return err 1541 } 1542 1543 r := &Retract{ 1544 VersionInterval: vi, 1545 } 1546 if vi.Low == vi.High { 1547 r.Syntax = f.Syntax.addLine(nil, "retract", AutoQuote(vi.Low)) 1548 } else { 1549 r.Syntax = f.Syntax.addLine(nil, "retract", "[", AutoQuote(vi.Low), ",", AutoQuote(vi.High), "]") 1550 } 1551 if rationale != "" { 1552 for _, line := range strings.Split(rationale, "\n") { 1553 com := Comment{Token: "// " + line} 1554 r.Syntax.Comment().Before = append(r.Syntax.Comment().Before, com) 1555 } 1556 } 1557 return nil 1558} 1559 1560func (f *File) DropRetract(vi VersionInterval) error { 1561 for _, r := range f.Retract { 1562 if r.VersionInterval == vi { 1563 r.Syntax.markRemoved() 1564 *r = Retract{} 1565 } 1566 } 1567 return nil 1568} 1569 1570func (f *File) SortBlocks() { 1571 f.removeDups() // otherwise sorting is unsafe 1572 1573 // semanticSortForExcludeVersionV is the Go version (plus leading "v") at which 1574 // lines in exclude blocks start to use semantic sort instead of lexicographic sort. 1575 // See go.dev/issue/60028. 1576 const semanticSortForExcludeVersionV = "v1.21" 1577 useSemanticSortForExclude := f.Go != nil && semver.Compare("v"+f.Go.Version, semanticSortForExcludeVersionV) >= 0 1578 1579 for _, stmt := range f.Syntax.Stmt { 1580 block, ok := stmt.(*LineBlock) 1581 if !ok { 1582 continue 1583 } 1584 less := lineLess 1585 if block.Token[0] == "exclude" && useSemanticSortForExclude { 1586 less = lineExcludeLess 1587 } else if block.Token[0] == "retract" { 1588 less = lineRetractLess 1589 } 1590 sort.SliceStable(block.Line, func(i, j int) bool { 1591 return less(block.Line[i], block.Line[j]) 1592 }) 1593 } 1594} 1595 1596// removeDups removes duplicate exclude and replace directives. 1597// 1598// Earlier exclude directives take priority. 1599// 1600// Later replace directives take priority. 1601// 1602// require directives are not de-duplicated. That's left up to higher-level 1603// logic (MVS). 1604// 1605// retract directives are not de-duplicated since comments are 1606// meaningful, and versions may be retracted multiple times. 1607func (f *File) removeDups() { 1608 removeDups(f.Syntax, &f.Exclude, &f.Replace) 1609} 1610 1611func removeDups(syntax *FileSyntax, exclude *[]*Exclude, replace *[]*Replace) { 1612 kill := make(map[*Line]bool) 1613 1614 // Remove duplicate excludes. 1615 if exclude != nil { 1616 haveExclude := make(map[module.Version]bool) 1617 for _, x := range *exclude { 1618 if haveExclude[x.Mod] { 1619 kill[x.Syntax] = true 1620 continue 1621 } 1622 haveExclude[x.Mod] = true 1623 } 1624 var excl []*Exclude 1625 for _, x := range *exclude { 1626 if !kill[x.Syntax] { 1627 excl = append(excl, x) 1628 } 1629 } 1630 *exclude = excl 1631 } 1632 1633 // Remove duplicate replacements. 1634 // Later replacements take priority over earlier ones. 1635 haveReplace := make(map[module.Version]bool) 1636 for i := len(*replace) - 1; i >= 0; i-- { 1637 x := (*replace)[i] 1638 if haveReplace[x.Old] { 1639 kill[x.Syntax] = true 1640 continue 1641 } 1642 haveReplace[x.Old] = true 1643 } 1644 var repl []*Replace 1645 for _, x := range *replace { 1646 if !kill[x.Syntax] { 1647 repl = append(repl, x) 1648 } 1649 } 1650 *replace = repl 1651 1652 // Duplicate require and retract directives are not removed. 1653 1654 // Drop killed statements from the syntax tree. 1655 var stmts []Expr 1656 for _, stmt := range syntax.Stmt { 1657 switch stmt := stmt.(type) { 1658 case *Line: 1659 if kill[stmt] { 1660 continue 1661 } 1662 case *LineBlock: 1663 var lines []*Line 1664 for _, line := range stmt.Line { 1665 if !kill[line] { 1666 lines = append(lines, line) 1667 } 1668 } 1669 stmt.Line = lines 1670 if len(lines) == 0 { 1671 continue 1672 } 1673 } 1674 stmts = append(stmts, stmt) 1675 } 1676 syntax.Stmt = stmts 1677} 1678 1679// lineLess returns whether li should be sorted before lj. It sorts 1680// lexicographically without assigning any special meaning to tokens. 1681func lineLess(li, lj *Line) bool { 1682 for k := 0; k < len(li.Token) && k < len(lj.Token); k++ { 1683 if li.Token[k] != lj.Token[k] { 1684 return li.Token[k] < lj.Token[k] 1685 } 1686 } 1687 return len(li.Token) < len(lj.Token) 1688} 1689 1690// lineExcludeLess reports whether li should be sorted before lj for lines in 1691// an "exclude" block. 1692func lineExcludeLess(li, lj *Line) bool { 1693 if len(li.Token) != 2 || len(lj.Token) != 2 { 1694 // Not a known exclude specification. 1695 // Fall back to sorting lexicographically. 1696 return lineLess(li, lj) 1697 } 1698 // An exclude specification has two tokens: ModulePath and Version. 1699 // Compare module path by string order and version by semver rules. 1700 if pi, pj := li.Token[0], lj.Token[0]; pi != pj { 1701 return pi < pj 1702 } 1703 return semver.Compare(li.Token[1], lj.Token[1]) < 0 1704} 1705 1706// lineRetractLess returns whether li should be sorted before lj for lines in 1707// a "retract" block. It treats each line as a version interval. Single versions 1708// are compared as if they were intervals with the same low and high version. 1709// Intervals are sorted in descending order, first by low version, then by 1710// high version, using semver.Compare. 1711func lineRetractLess(li, lj *Line) bool { 1712 interval := func(l *Line) VersionInterval { 1713 if len(l.Token) == 1 { 1714 return VersionInterval{Low: l.Token[0], High: l.Token[0]} 1715 } else if len(l.Token) == 5 && l.Token[0] == "[" && l.Token[2] == "," && l.Token[4] == "]" { 1716 return VersionInterval{Low: l.Token[1], High: l.Token[3]} 1717 } else { 1718 // Line in unknown format. Treat as an invalid version. 1719 return VersionInterval{} 1720 } 1721 } 1722 vii := interval(li) 1723 vij := interval(lj) 1724 if cmp := semver.Compare(vii.Low, vij.Low); cmp != 0 { 1725 return cmp > 0 1726 } 1727 return semver.Compare(vii.High, vij.High) > 0 1728} 1729 1730// checkCanonicalVersion returns a non-nil error if vers is not a canonical 1731// version string or does not match the major version of path. 1732// 1733// If path is non-empty, the error text suggests a format with a major version 1734// corresponding to the path. 1735func checkCanonicalVersion(path, vers string) error { 1736 _, pathMajor, pathMajorOk := module.SplitPathVersion(path) 1737 1738 if vers == "" || vers != module.CanonicalVersion(vers) { 1739 if pathMajor == "" { 1740 return &module.InvalidVersionError{ 1741 Version: vers, 1742 Err: fmt.Errorf("must be of the form v1.2.3"), 1743 } 1744 } 1745 return &module.InvalidVersionError{ 1746 Version: vers, 1747 Err: fmt.Errorf("must be of the form %s.2.3", module.PathMajorPrefix(pathMajor)), 1748 } 1749 } 1750 1751 if pathMajorOk { 1752 if err := module.CheckPathMajor(vers, pathMajor); err != nil { 1753 if pathMajor == "" { 1754 // In this context, the user probably wrote "v2.3.4" when they meant 1755 // "v2.3.4+incompatible". Suggest that instead of "v0 or v1". 1756 return &module.InvalidVersionError{ 1757 Version: vers, 1758 Err: fmt.Errorf("should be %s+incompatible (or module %s/%v)", vers, path, semver.Major(vers)), 1759 } 1760 } 1761 return err 1762 } 1763 } 1764 1765 return nil 1766} 1767