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://golang.org/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 AddNewRequire and DropReplace 16// 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 Require []*Require 41 Exclude []*Exclude 42 Replace []*Replace 43 Retract []*Retract 44 45 Syntax *FileSyntax 46} 47 48// A Module is the module statement. 49type Module struct { 50 Mod module.Version 51 Deprecated string 52 Syntax *Line 53} 54 55// A Go is the go statement. 56type Go struct { 57 Version string // "1.23" 58 Syntax *Line 59} 60 61// An Exclude is a single exclude statement. 62type Exclude struct { 63 Mod module.Version 64 Syntax *Line 65} 66 67// A Replace is a single replace statement. 68type Replace struct { 69 Old module.Version 70 New module.Version 71 Syntax *Line 72} 73 74// A Retract is a single retract statement. 75type Retract struct { 76 VersionInterval 77 Rationale string 78 Syntax *Line 79} 80 81// A VersionInterval represents a range of versions with upper and lower bounds. 82// Intervals are closed: both bounds are included. When Low is equal to High, 83// the interval may refer to a single version ('v1.2.3') or an interval 84// ('[v1.2.3, v1.2.3]'); both have the same representation. 85type VersionInterval struct { 86 Low, High string 87} 88 89// A Require is a single require statement. 90type Require struct { 91 Mod module.Version 92 Indirect bool // has "// indirect" comment 93 Syntax *Line 94} 95 96func (r *Require) markRemoved() { 97 r.Syntax.markRemoved() 98 *r = Require{} 99} 100 101func (r *Require) setVersion(v string) { 102 r.Mod.Version = v 103 104 if line := r.Syntax; len(line.Token) > 0 { 105 if line.InBlock { 106 // If the line is preceded by an empty line, remove it; see 107 // https://golang.org/issue/33779. 108 if len(line.Comments.Before) == 1 && len(line.Comments.Before[0].Token) == 0 { 109 line.Comments.Before = line.Comments.Before[:0] 110 } 111 if len(line.Token) >= 2 { // example.com v1.2.3 112 line.Token[1] = v 113 } 114 } else { 115 if len(line.Token) >= 3 { // require example.com v1.2.3 116 line.Token[2] = v 117 } 118 } 119 } 120} 121 122// setIndirect sets line to have (or not have) a "// indirect" comment. 123func (r *Require) setIndirect(indirect bool) { 124 r.Indirect = indirect 125 line := r.Syntax 126 if isIndirect(line) == indirect { 127 return 128 } 129 if indirect { 130 // Adding comment. 131 if len(line.Suffix) == 0 { 132 // New comment. 133 line.Suffix = []Comment{{Token: "// indirect", Suffix: true}} 134 return 135 } 136 137 com := &line.Suffix[0] 138 text := strings.TrimSpace(strings.TrimPrefix(com.Token, string(slashSlash))) 139 if text == "" { 140 // Empty comment. 141 com.Token = "// indirect" 142 return 143 } 144 145 // Insert at beginning of existing comment. 146 com.Token = "// indirect; " + text 147 return 148 } 149 150 // Removing comment. 151 f := strings.TrimSpace(strings.TrimPrefix(line.Suffix[0].Token, string(slashSlash))) 152 if f == "indirect" { 153 // Remove whole comment. 154 line.Suffix = nil 155 return 156 } 157 158 // Remove comment prefix. 159 com := &line.Suffix[0] 160 i := strings.Index(com.Token, "indirect;") 161 com.Token = "//" + com.Token[i+len("indirect;"):] 162} 163 164// isIndirect reports whether line has a "// indirect" comment, 165// meaning it is in go.mod only for its effect on indirect dependencies, 166// so that it can be dropped entirely once the effective version of the 167// indirect dependency reaches the given minimum version. 168func isIndirect(line *Line) bool { 169 if len(line.Suffix) == 0 { 170 return false 171 } 172 f := strings.Fields(strings.TrimPrefix(line.Suffix[0].Token, string(slashSlash))) 173 return (len(f) == 1 && f[0] == "indirect" || len(f) > 1 && f[0] == "indirect;") 174} 175 176func (f *File) AddModuleStmt(path string) error { 177 if f.Syntax == nil { 178 f.Syntax = new(FileSyntax) 179 } 180 if f.Module == nil { 181 f.Module = &Module{ 182 Mod: module.Version{Path: path}, 183 Syntax: f.Syntax.addLine(nil, "module", AutoQuote(path)), 184 } 185 } else { 186 f.Module.Mod.Path = path 187 f.Syntax.updateLine(f.Module.Syntax, "module", AutoQuote(path)) 188 } 189 return nil 190} 191 192func (f *File) AddComment(text string) { 193 if f.Syntax == nil { 194 f.Syntax = new(FileSyntax) 195 } 196 f.Syntax.Stmt = append(f.Syntax.Stmt, &CommentBlock{ 197 Comments: Comments{ 198 Before: []Comment{ 199 { 200 Token: text, 201 }, 202 }, 203 }, 204 }) 205} 206 207type VersionFixer func(path, version string) (string, error) 208 209// errDontFix is returned by a VersionFixer to indicate the version should be 210// left alone, even if it's not canonical. 211var dontFixRetract VersionFixer = func(_, vers string) (string, error) { 212 return vers, nil 213} 214 215// Parse parses and returns a go.mod file. 216// 217// file is the name of the file, used in positions and errors. 218// 219// data is the content of the file. 220// 221// fix is an optional function that canonicalizes module versions. 222// If fix is nil, all module versions must be canonical (module.CanonicalVersion 223// must return the same string). 224func Parse(file string, data []byte, fix VersionFixer) (*File, error) { 225 return parseToFile(file, data, fix, true) 226} 227 228// ParseLax is like Parse but ignores unknown statements. 229// It is used when parsing go.mod files other than the main module, 230// under the theory that most statement types we add in the future will 231// only apply in the main module, like exclude and replace, 232// and so we get better gradual deployments if old go commands 233// simply ignore those statements when found in go.mod files 234// in dependencies. 235func ParseLax(file string, data []byte, fix VersionFixer) (*File, error) { 236 return parseToFile(file, data, fix, false) 237} 238 239func parseToFile(file string, data []byte, fix VersionFixer, strict bool) (parsed *File, err error) { 240 fs, err := parse(file, data) 241 if err != nil { 242 return nil, err 243 } 244 f := &File{ 245 Syntax: fs, 246 } 247 var errs ErrorList 248 249 // fix versions in retract directives after the file is parsed. 250 // We need the module path to fix versions, and it might be at the end. 251 defer func() { 252 oldLen := len(errs) 253 f.fixRetract(fix, &errs) 254 if len(errs) > oldLen { 255 parsed, err = nil, errs 256 } 257 }() 258 259 for _, x := range fs.Stmt { 260 switch x := x.(type) { 261 case *Line: 262 f.add(&errs, nil, x, x.Token[0], x.Token[1:], fix, strict) 263 264 case *LineBlock: 265 if len(x.Token) > 1 { 266 if strict { 267 errs = append(errs, Error{ 268 Filename: file, 269 Pos: x.Start, 270 Err: fmt.Errorf("unknown block type: %s", strings.Join(x.Token, " ")), 271 }) 272 } 273 continue 274 } 275 switch x.Token[0] { 276 default: 277 if strict { 278 errs = append(errs, Error{ 279 Filename: file, 280 Pos: x.Start, 281 Err: fmt.Errorf("unknown block type: %s", strings.Join(x.Token, " ")), 282 }) 283 } 284 continue 285 case "module", "require", "exclude", "replace", "retract": 286 for _, l := range x.Line { 287 f.add(&errs, x, l, x.Token[0], l.Token, fix, strict) 288 } 289 } 290 } 291 } 292 293 if len(errs) > 0 { 294 return nil, errs 295 } 296 return f, nil 297} 298 299var GoVersionRE = lazyregexp.New(`^([1-9][0-9]*)\.(0|[1-9][0-9]*)$`) 300var laxGoVersionRE = lazyregexp.New(`^v?(([1-9][0-9]*)\.(0|[1-9][0-9]*))([^0-9].*)$`) 301 302func (f *File) add(errs *ErrorList, block *LineBlock, line *Line, verb string, args []string, fix VersionFixer, strict bool) { 303 // If strict is false, this module is a dependency. 304 // We ignore all unknown directives as well as main-module-only 305 // directives like replace and exclude. It will work better for 306 // forward compatibility if we can depend on modules that have unknown 307 // statements (presumed relevant only when acting as the main module) 308 // and simply ignore those statements. 309 if !strict { 310 switch verb { 311 case "go", "module", "retract", "require": 312 // want these even for dependency go.mods 313 default: 314 return 315 } 316 } 317 318 wrapModPathError := func(modPath string, err error) { 319 *errs = append(*errs, Error{ 320 Filename: f.Syntax.Name, 321 Pos: line.Start, 322 ModPath: modPath, 323 Verb: verb, 324 Err: err, 325 }) 326 } 327 wrapError := func(err error) { 328 *errs = append(*errs, Error{ 329 Filename: f.Syntax.Name, 330 Pos: line.Start, 331 Err: err, 332 }) 333 } 334 errorf := func(format string, args ...interface{}) { 335 wrapError(fmt.Errorf(format, args...)) 336 } 337 338 switch verb { 339 default: 340 errorf("unknown directive: %s", verb) 341 342 case "go": 343 if f.Go != nil { 344 errorf("repeated go statement") 345 return 346 } 347 if len(args) != 1 { 348 errorf("go directive expects exactly one argument") 349 return 350 } else if !GoVersionRE.MatchString(args[0]) { 351 fixed := false 352 if !strict { 353 if m := laxGoVersionRE.FindStringSubmatch(args[0]); m != nil { 354 args[0] = m[1] 355 fixed = true 356 } 357 } 358 if !fixed { 359 errorf("invalid go version '%s': must match format 1.23", args[0]) 360 return 361 } 362 } 363 364 f.Go = &Go{Syntax: line} 365 f.Go.Version = args[0] 366 367 case "module": 368 if f.Module != nil { 369 errorf("repeated module statement") 370 return 371 } 372 deprecated := parseDeprecation(block, line) 373 f.Module = &Module{ 374 Syntax: line, 375 Deprecated: deprecated, 376 } 377 if len(args) != 1 { 378 errorf("usage: module module/path") 379 return 380 } 381 s, err := parseString(&args[0]) 382 if err != nil { 383 errorf("invalid quoted string: %v", err) 384 return 385 } 386 f.Module.Mod = module.Version{Path: s} 387 388 case "require", "exclude": 389 if len(args) != 2 { 390 errorf("usage: %s module/path v1.2.3", verb) 391 return 392 } 393 s, err := parseString(&args[0]) 394 if err != nil { 395 errorf("invalid quoted string: %v", err) 396 return 397 } 398 v, err := parseVersion(verb, s, &args[1], fix) 399 if err != nil { 400 wrapError(err) 401 return 402 } 403 pathMajor, err := modulePathMajor(s) 404 if err != nil { 405 wrapError(err) 406 return 407 } 408 if err := module.CheckPathMajor(v, pathMajor); err != nil { 409 wrapModPathError(s, err) 410 return 411 } 412 if verb == "require" { 413 f.Require = append(f.Require, &Require{ 414 Mod: module.Version{Path: s, Version: v}, 415 Syntax: line, 416 Indirect: isIndirect(line), 417 }) 418 } else { 419 f.Exclude = append(f.Exclude, &Exclude{ 420 Mod: module.Version{Path: s, Version: v}, 421 Syntax: line, 422 }) 423 } 424 425 case "replace": 426 arrow := 2 427 if len(args) >= 2 && args[1] == "=>" { 428 arrow = 1 429 } 430 if len(args) < arrow+2 || len(args) > arrow+3 || args[arrow] != "=>" { 431 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) 432 return 433 } 434 s, err := parseString(&args[0]) 435 if err != nil { 436 errorf("invalid quoted string: %v", err) 437 return 438 } 439 pathMajor, err := modulePathMajor(s) 440 if err != nil { 441 wrapModPathError(s, err) 442 return 443 } 444 var v string 445 if arrow == 2 { 446 v, err = parseVersion(verb, s, &args[1], fix) 447 if err != nil { 448 wrapError(err) 449 return 450 } 451 if err := module.CheckPathMajor(v, pathMajor); err != nil { 452 wrapModPathError(s, err) 453 return 454 } 455 } 456 ns, err := parseString(&args[arrow+1]) 457 if err != nil { 458 errorf("invalid quoted string: %v", err) 459 return 460 } 461 nv := "" 462 if len(args) == arrow+2 { 463 if !IsDirectoryPath(ns) { 464 errorf("replacement module without version must be directory path (rooted or starting with ./ or ../)") 465 return 466 } 467 if filepath.Separator == '/' && strings.Contains(ns, `\`) { 468 errorf("replacement directory appears to be Windows path (on a non-windows system)") 469 return 470 } 471 } 472 if len(args) == arrow+3 { 473 nv, err = parseVersion(verb, ns, &args[arrow+2], fix) 474 if err != nil { 475 wrapError(err) 476 return 477 } 478 if IsDirectoryPath(ns) { 479 errorf("replacement module directory path %q cannot have version", ns) 480 return 481 } 482 } 483 f.Replace = append(f.Replace, &Replace{ 484 Old: module.Version{Path: s, Version: v}, 485 New: module.Version{Path: ns, Version: nv}, 486 Syntax: line, 487 }) 488 489 case "retract": 490 rationale := parseDirectiveComment(block, line) 491 vi, err := parseVersionInterval(verb, "", &args, dontFixRetract) 492 if err != nil { 493 if strict { 494 wrapError(err) 495 return 496 } else { 497 // Only report errors parsing intervals in the main module. We may 498 // support additional syntax in the future, such as open and half-open 499 // intervals. Those can't be supported now, because they break the 500 // go.mod parser, even in lax mode. 501 return 502 } 503 } 504 if len(args) > 0 && strict { 505 // In the future, there may be additional information after the version. 506 errorf("unexpected token after version: %q", args[0]) 507 return 508 } 509 retract := &Retract{ 510 VersionInterval: vi, 511 Rationale: rationale, 512 Syntax: line, 513 } 514 f.Retract = append(f.Retract, retract) 515 } 516} 517 518// fixRetract applies fix to each retract directive in f, appending any errors 519// to errs. 520// 521// Most versions are fixed as we parse the file, but for retract directives, 522// the relevant module path is the one specified with the module directive, 523// and that might appear at the end of the file (or not at all). 524func (f *File) fixRetract(fix VersionFixer, errs *ErrorList) { 525 if fix == nil { 526 return 527 } 528 path := "" 529 if f.Module != nil { 530 path = f.Module.Mod.Path 531 } 532 var r *Retract 533 wrapError := func(err error) { 534 *errs = append(*errs, Error{ 535 Filename: f.Syntax.Name, 536 Pos: r.Syntax.Start, 537 Err: err, 538 }) 539 } 540 541 for _, r = range f.Retract { 542 if path == "" { 543 wrapError(errors.New("no module directive found, so retract cannot be used")) 544 return // only print the first one of these 545 } 546 547 args := r.Syntax.Token 548 if args[0] == "retract" { 549 args = args[1:] 550 } 551 vi, err := parseVersionInterval("retract", path, &args, fix) 552 if err != nil { 553 wrapError(err) 554 } 555 r.VersionInterval = vi 556 } 557} 558 559// IsDirectoryPath reports whether the given path should be interpreted 560// as a directory path. Just like on the go command line, relative paths 561// and rooted paths are directory paths; the rest are module paths. 562func IsDirectoryPath(ns string) bool { 563 // Because go.mod files can move from one system to another, 564 // we check all known path syntaxes, both Unix and Windows. 565 return strings.HasPrefix(ns, "./") || strings.HasPrefix(ns, "../") || strings.HasPrefix(ns, "/") || 566 strings.HasPrefix(ns, `.\`) || strings.HasPrefix(ns, `..\`) || strings.HasPrefix(ns, `\`) || 567 len(ns) >= 2 && ('A' <= ns[0] && ns[0] <= 'Z' || 'a' <= ns[0] && ns[0] <= 'z') && ns[1] == ':' 568} 569 570// MustQuote reports whether s must be quoted in order to appear as 571// a single token in a go.mod line. 572func MustQuote(s string) bool { 573 for _, r := range s { 574 switch r { 575 case ' ', '"', '\'', '`': 576 return true 577 578 case '(', ')', '[', ']', '{', '}', ',': 579 if len(s) > 1 { 580 return true 581 } 582 583 default: 584 if !unicode.IsPrint(r) { 585 return true 586 } 587 } 588 } 589 return s == "" || strings.Contains(s, "//") || strings.Contains(s, "/*") 590} 591 592// AutoQuote returns s or, if quoting is required for s to appear in a go.mod, 593// the quotation of s. 594func AutoQuote(s string) string { 595 if MustQuote(s) { 596 return strconv.Quote(s) 597 } 598 return s 599} 600 601func parseVersionInterval(verb string, path string, args *[]string, fix VersionFixer) (VersionInterval, error) { 602 toks := *args 603 if len(toks) == 0 || toks[0] == "(" { 604 return VersionInterval{}, fmt.Errorf("expected '[' or version") 605 } 606 if toks[0] != "[" { 607 v, err := parseVersion(verb, path, &toks[0], fix) 608 if err != nil { 609 return VersionInterval{}, err 610 } 611 *args = toks[1:] 612 return VersionInterval{Low: v, High: v}, nil 613 } 614 toks = toks[1:] 615 616 if len(toks) == 0 { 617 return VersionInterval{}, fmt.Errorf("expected version after '['") 618 } 619 low, err := parseVersion(verb, path, &toks[0], fix) 620 if err != nil { 621 return VersionInterval{}, err 622 } 623 toks = toks[1:] 624 625 if len(toks) == 0 || toks[0] != "," { 626 return VersionInterval{}, fmt.Errorf("expected ',' after version") 627 } 628 toks = toks[1:] 629 630 if len(toks) == 0 { 631 return VersionInterval{}, fmt.Errorf("expected version after ','") 632 } 633 high, err := parseVersion(verb, path, &toks[0], fix) 634 if err != nil { 635 return VersionInterval{}, err 636 } 637 toks = toks[1:] 638 639 if len(toks) == 0 || toks[0] != "]" { 640 return VersionInterval{}, fmt.Errorf("expected ']' after version") 641 } 642 toks = toks[1:] 643 644 *args = toks 645 return VersionInterval{Low: low, High: high}, nil 646} 647 648func parseString(s *string) (string, error) { 649 t := *s 650 if strings.HasPrefix(t, `"`) { 651 var err error 652 if t, err = strconv.Unquote(t); err != nil { 653 return "", err 654 } 655 } else if strings.ContainsAny(t, "\"'`") { 656 // Other quotes are reserved both for possible future expansion 657 // and to avoid confusion. For example if someone types 'x' 658 // we want that to be a syntax error and not a literal x in literal quotation marks. 659 return "", fmt.Errorf("unquoted string cannot contain quote") 660 } 661 *s = AutoQuote(t) 662 return t, nil 663} 664 665var deprecatedRE = lazyregexp.New(`(?s)(?:^|\n\n)Deprecated: *(.*?)(?:$|\n\n)`) 666 667// parseDeprecation extracts the text of comments on a "module" directive and 668// extracts a deprecation message from that. 669// 670// A deprecation message is contained in a paragraph within a block of comments 671// that starts with "Deprecated:" (case sensitive). The message runs until the 672// end of the paragraph and does not include the "Deprecated:" prefix. If the 673// comment block has multiple paragraphs that start with "Deprecated:", 674// parseDeprecation returns the message from the first. 675func parseDeprecation(block *LineBlock, line *Line) string { 676 text := parseDirectiveComment(block, line) 677 m := deprecatedRE.FindStringSubmatch(text) 678 if m == nil { 679 return "" 680 } 681 return m[1] 682} 683 684// parseDirectiveComment extracts the text of comments on a directive. 685// If the directive's line does not have comments and is part of a block that 686// does have comments, the block's comments are used. 687func parseDirectiveComment(block *LineBlock, line *Line) string { 688 comments := line.Comment() 689 if block != nil && len(comments.Before) == 0 && len(comments.Suffix) == 0 { 690 comments = block.Comment() 691 } 692 groups := [][]Comment{comments.Before, comments.Suffix} 693 var lines []string 694 for _, g := range groups { 695 for _, c := range g { 696 if !strings.HasPrefix(c.Token, "//") { 697 continue // blank line 698 } 699 lines = append(lines, strings.TrimSpace(strings.TrimPrefix(c.Token, "//"))) 700 } 701 } 702 return strings.Join(lines, "\n") 703} 704 705type ErrorList []Error 706 707func (e ErrorList) Error() string { 708 errStrs := make([]string, len(e)) 709 for i, err := range e { 710 errStrs[i] = err.Error() 711 } 712 return strings.Join(errStrs, "\n") 713} 714 715type Error struct { 716 Filename string 717 Pos Position 718 Verb string 719 ModPath string 720 Err error 721} 722 723func (e *Error) Error() string { 724 var pos string 725 if e.Pos.LineRune > 1 { 726 // Don't print LineRune if it's 1 (beginning of line). 727 // It's always 1 except in scanner errors, which are rare. 728 pos = fmt.Sprintf("%s:%d:%d: ", e.Filename, e.Pos.Line, e.Pos.LineRune) 729 } else if e.Pos.Line > 0 { 730 pos = fmt.Sprintf("%s:%d: ", e.Filename, e.Pos.Line) 731 } else if e.Filename != "" { 732 pos = fmt.Sprintf("%s: ", e.Filename) 733 } 734 735 var directive string 736 if e.ModPath != "" { 737 directive = fmt.Sprintf("%s %s: ", e.Verb, e.ModPath) 738 } else if e.Verb != "" { 739 directive = fmt.Sprintf("%s: ", e.Verb) 740 } 741 742 return pos + directive + e.Err.Error() 743} 744 745func (e *Error) Unwrap() error { return e.Err } 746 747func parseVersion(verb string, path string, s *string, fix VersionFixer) (string, error) { 748 t, err := parseString(s) 749 if err != nil { 750 return "", &Error{ 751 Verb: verb, 752 ModPath: path, 753 Err: &module.InvalidVersionError{ 754 Version: *s, 755 Err: err, 756 }, 757 } 758 } 759 if fix != nil { 760 fixed, err := fix(path, t) 761 if err != nil { 762 if err, ok := err.(*module.ModuleError); ok { 763 return "", &Error{ 764 Verb: verb, 765 ModPath: path, 766 Err: err.Err, 767 } 768 } 769 return "", err 770 } 771 t = fixed 772 } else { 773 cv := module.CanonicalVersion(t) 774 if cv == "" { 775 return "", &Error{ 776 Verb: verb, 777 ModPath: path, 778 Err: &module.InvalidVersionError{ 779 Version: t, 780 Err: errors.New("must be of the form v1.2.3"), 781 }, 782 } 783 } 784 t = cv 785 } 786 *s = t 787 return *s, nil 788} 789 790func modulePathMajor(path string) (string, error) { 791 _, major, ok := module.SplitPathVersion(path) 792 if !ok { 793 return "", fmt.Errorf("invalid module path") 794 } 795 return major, nil 796} 797 798func (f *File) Format() ([]byte, error) { 799 return Format(f.Syntax), nil 800} 801 802// Cleanup cleans up the file f after any edit operations. 803// To avoid quadratic behavior, modifications like DropRequire 804// clear the entry but do not remove it from the slice. 805// Cleanup cleans out all the cleared entries. 806func (f *File) Cleanup() { 807 w := 0 808 for _, r := range f.Require { 809 if r.Mod.Path != "" { 810 f.Require[w] = r 811 w++ 812 } 813 } 814 f.Require = f.Require[:w] 815 816 w = 0 817 for _, x := range f.Exclude { 818 if x.Mod.Path != "" { 819 f.Exclude[w] = x 820 w++ 821 } 822 } 823 f.Exclude = f.Exclude[:w] 824 825 w = 0 826 for _, r := range f.Replace { 827 if r.Old.Path != "" { 828 f.Replace[w] = r 829 w++ 830 } 831 } 832 f.Replace = f.Replace[:w] 833 834 w = 0 835 for _, r := range f.Retract { 836 if r.Low != "" || r.High != "" { 837 f.Retract[w] = r 838 w++ 839 } 840 } 841 f.Retract = f.Retract[:w] 842 843 f.Syntax.Cleanup() 844} 845 846func (f *File) AddGoStmt(version string) error { 847 if !GoVersionRE.MatchString(version) { 848 return fmt.Errorf("invalid language version string %q", version) 849 } 850 if f.Go == nil { 851 var hint Expr 852 if f.Module != nil && f.Module.Syntax != nil { 853 hint = f.Module.Syntax 854 } 855 f.Go = &Go{ 856 Version: version, 857 Syntax: f.Syntax.addLine(hint, "go", version), 858 } 859 } else { 860 f.Go.Version = version 861 f.Syntax.updateLine(f.Go.Syntax, "go", version) 862 } 863 return nil 864} 865 866// AddRequire sets the first require line for path to version vers, 867// preserving any existing comments for that line and removing all 868// other lines for path. 869// 870// If no line currently exists for path, AddRequire adds a new line 871// at the end of the last require block. 872func (f *File) AddRequire(path, vers string) error { 873 need := true 874 for _, r := range f.Require { 875 if r.Mod.Path == path { 876 if need { 877 r.Mod.Version = vers 878 f.Syntax.updateLine(r.Syntax, "require", AutoQuote(path), vers) 879 need = false 880 } else { 881 r.Syntax.markRemoved() 882 *r = Require{} 883 } 884 } 885 } 886 887 if need { 888 f.AddNewRequire(path, vers, false) 889 } 890 return nil 891} 892 893// AddNewRequire adds a new require line for path at version vers at the end of 894// the last require block, regardless of any existing require lines for path. 895func (f *File) AddNewRequire(path, vers string, indirect bool) { 896 line := f.Syntax.addLine(nil, "require", AutoQuote(path), vers) 897 r := &Require{ 898 Mod: module.Version{Path: path, Version: vers}, 899 Syntax: line, 900 } 901 r.setIndirect(indirect) 902 f.Require = append(f.Require, r) 903} 904 905// SetRequire updates the requirements of f to contain exactly req, preserving 906// the existing block structure and line comment contents (except for 'indirect' 907// markings) for the first requirement on each named module path. 908// 909// The Syntax field is ignored for the requirements in req. 910// 911// Any requirements not already present in the file are added to the block 912// containing the last require line. 913// 914// The requirements in req must specify at most one distinct version for each 915// module path. 916// 917// If any existing requirements may be removed, the caller should call Cleanup 918// after all edits are complete. 919func (f *File) SetRequire(req []*Require) { 920 type elem struct { 921 version string 922 indirect bool 923 } 924 need := make(map[string]elem) 925 for _, r := range req { 926 if prev, dup := need[r.Mod.Path]; dup && prev.version != r.Mod.Version { 927 panic(fmt.Errorf("SetRequire called with conflicting versions for path %s (%s and %s)", r.Mod.Path, prev.version, r.Mod.Version)) 928 } 929 need[r.Mod.Path] = elem{r.Mod.Version, r.Indirect} 930 } 931 932 // Update or delete the existing Require entries to preserve 933 // only the first for each module path in req. 934 for _, r := range f.Require { 935 e, ok := need[r.Mod.Path] 936 if ok { 937 r.setVersion(e.version) 938 r.setIndirect(e.indirect) 939 } else { 940 r.markRemoved() 941 } 942 delete(need, r.Mod.Path) 943 } 944 945 // Add new entries in the last block of the file for any paths that weren't 946 // already present. 947 // 948 // This step is nondeterministic, but the final result will be deterministic 949 // because we will sort the block. 950 for path, e := range need { 951 f.AddNewRequire(path, e.version, e.indirect) 952 } 953 954 f.SortBlocks() 955} 956 957// SetRequireSeparateIndirect updates the requirements of f to contain the given 958// requirements. Comment contents (except for 'indirect' markings) are retained 959// from the first existing requirement for each module path, and block structure 960// is maintained as long as the indirect markings match. 961// 962// Any requirements on paths not already present in the file are added. Direct 963// requirements are added to the last block containing *any* other direct 964// requirement. Indirect requirements are added to the last block containing 965// *only* other indirect requirements. If no suitable block exists, a new one is 966// added, with the last block containing a direct dependency (if any) 967// immediately before the first block containing only indirect dependencies. 968// 969// The Syntax field is ignored for requirements in the given blocks. 970func (f *File) SetRequireSeparateIndirect(req []*Require) { 971 type modKey struct { 972 path string 973 indirect bool 974 } 975 need := make(map[modKey]string) 976 for _, r := range req { 977 need[modKey{r.Mod.Path, r.Indirect}] = r.Mod.Version 978 } 979 980 comments := make(map[string]Comments) 981 for _, r := range f.Require { 982 v, ok := need[modKey{r.Mod.Path, r.Indirect}] 983 if !ok { 984 if _, ok := need[modKey{r.Mod.Path, !r.Indirect}]; ok { 985 if _, dup := comments[r.Mod.Path]; !dup { 986 comments[r.Mod.Path] = r.Syntax.Comments 987 } 988 } 989 r.markRemoved() 990 continue 991 } 992 r.setVersion(v) 993 delete(need, modKey{r.Mod.Path, r.Indirect}) 994 } 995 996 var ( 997 lastDirectOrMixedBlock Expr 998 firstIndirectOnlyBlock Expr 999 lastIndirectOnlyBlock Expr 1000 ) 1001 for _, stmt := range f.Syntax.Stmt { 1002 switch stmt := stmt.(type) { 1003 case *Line: 1004 if len(stmt.Token) == 0 || stmt.Token[0] != "require" { 1005 continue 1006 } 1007 if isIndirect(stmt) { 1008 lastIndirectOnlyBlock = stmt 1009 } else { 1010 lastDirectOrMixedBlock = stmt 1011 } 1012 case *LineBlock: 1013 if len(stmt.Token) == 0 || stmt.Token[0] != "require" { 1014 continue 1015 } 1016 indirectOnly := true 1017 for _, line := range stmt.Line { 1018 if len(line.Token) == 0 { 1019 continue 1020 } 1021 if !isIndirect(line) { 1022 indirectOnly = false 1023 break 1024 } 1025 } 1026 if indirectOnly { 1027 lastIndirectOnlyBlock = stmt 1028 if firstIndirectOnlyBlock == nil { 1029 firstIndirectOnlyBlock = stmt 1030 } 1031 } else { 1032 lastDirectOrMixedBlock = stmt 1033 } 1034 } 1035 } 1036 1037 isOrContainsStmt := func(stmt Expr, target Expr) bool { 1038 if stmt == target { 1039 return true 1040 } 1041 if stmt, ok := stmt.(*LineBlock); ok { 1042 if target, ok := target.(*Line); ok { 1043 for _, line := range stmt.Line { 1044 if line == target { 1045 return true 1046 } 1047 } 1048 } 1049 } 1050 return false 1051 } 1052 1053 addRequire := func(path, vers string, indirect bool, comments Comments) { 1054 var line *Line 1055 if indirect { 1056 if lastIndirectOnlyBlock != nil { 1057 line = f.Syntax.addLine(lastIndirectOnlyBlock, "require", path, vers) 1058 } else { 1059 // Add a new require block after the last direct-only or mixed "require" 1060 // block (if any). 1061 // 1062 // (f.Syntax.addLine would add the line to an existing "require" block if 1063 // present, but here the existing "require" blocks are all direct-only, so 1064 // we know we need to add a new block instead.) 1065 line = &Line{Token: []string{"require", path, vers}} 1066 lastIndirectOnlyBlock = line 1067 firstIndirectOnlyBlock = line // only block implies first block 1068 if lastDirectOrMixedBlock == nil { 1069 f.Syntax.Stmt = append(f.Syntax.Stmt, line) 1070 } else { 1071 for i, stmt := range f.Syntax.Stmt { 1072 if isOrContainsStmt(stmt, lastDirectOrMixedBlock) { 1073 f.Syntax.Stmt = append(f.Syntax.Stmt, nil) // increase size 1074 copy(f.Syntax.Stmt[i+2:], f.Syntax.Stmt[i+1:]) // shuffle elements up 1075 f.Syntax.Stmt[i+1] = line 1076 break 1077 } 1078 } 1079 } 1080 } 1081 } else { 1082 if lastDirectOrMixedBlock != nil { 1083 line = f.Syntax.addLine(lastDirectOrMixedBlock, "require", path, vers) 1084 } else { 1085 // Add a new require block before the first indirect block (if any). 1086 // 1087 // That way if the file initially contains only indirect lines, 1088 // the direct lines still appear before it: we preserve existing 1089 // structure, but only to the extent that that structure already 1090 // reflects the direct/indirect split. 1091 line = &Line{Token: []string{"require", path, vers}} 1092 lastDirectOrMixedBlock = line 1093 if firstIndirectOnlyBlock == nil { 1094 f.Syntax.Stmt = append(f.Syntax.Stmt, line) 1095 } else { 1096 for i, stmt := range f.Syntax.Stmt { 1097 if isOrContainsStmt(stmt, firstIndirectOnlyBlock) { 1098 f.Syntax.Stmt = append(f.Syntax.Stmt, nil) // increase size 1099 copy(f.Syntax.Stmt[i+1:], f.Syntax.Stmt[i:]) // shuffle elements up 1100 f.Syntax.Stmt[i] = line 1101 break 1102 } 1103 } 1104 } 1105 } 1106 } 1107 1108 line.Comments.Before = commentsAdd(line.Comments.Before, comments.Before) 1109 line.Comments.Suffix = commentsAdd(line.Comments.Suffix, comments.Suffix) 1110 1111 r := &Require{ 1112 Mod: module.Version{Path: path, Version: vers}, 1113 Indirect: indirect, 1114 Syntax: line, 1115 } 1116 r.setIndirect(indirect) 1117 f.Require = append(f.Require, r) 1118 } 1119 1120 for k, vers := range need { 1121 addRequire(k.path, vers, k.indirect, comments[k.path]) 1122 } 1123 f.SortBlocks() 1124} 1125 1126func (f *File) DropRequire(path string) error { 1127 for _, r := range f.Require { 1128 if r.Mod.Path == path { 1129 r.Syntax.markRemoved() 1130 *r = Require{} 1131 } 1132 } 1133 return nil 1134} 1135 1136// AddExclude adds a exclude statement to the mod file. Errors if the provided 1137// version is not a canonical version string 1138func (f *File) AddExclude(path, vers string) error { 1139 if err := checkCanonicalVersion(path, vers); err != nil { 1140 return err 1141 } 1142 1143 var hint *Line 1144 for _, x := range f.Exclude { 1145 if x.Mod.Path == path && x.Mod.Version == vers { 1146 return nil 1147 } 1148 if x.Mod.Path == path { 1149 hint = x.Syntax 1150 } 1151 } 1152 1153 f.Exclude = append(f.Exclude, &Exclude{Mod: module.Version{Path: path, Version: vers}, Syntax: f.Syntax.addLine(hint, "exclude", AutoQuote(path), vers)}) 1154 return nil 1155} 1156 1157func (f *File) DropExclude(path, vers string) error { 1158 for _, x := range f.Exclude { 1159 if x.Mod.Path == path && x.Mod.Version == vers { 1160 x.Syntax.markRemoved() 1161 *x = Exclude{} 1162 } 1163 } 1164 return nil 1165} 1166 1167func (f *File) AddReplace(oldPath, oldVers, newPath, newVers string) error { 1168 need := true 1169 old := module.Version{Path: oldPath, Version: oldVers} 1170 new := module.Version{Path: newPath, Version: newVers} 1171 tokens := []string{"replace", AutoQuote(oldPath)} 1172 if oldVers != "" { 1173 tokens = append(tokens, oldVers) 1174 } 1175 tokens = append(tokens, "=>", AutoQuote(newPath)) 1176 if newVers != "" { 1177 tokens = append(tokens, newVers) 1178 } 1179 1180 var hint *Line 1181 for _, r := range f.Replace { 1182 if r.Old.Path == oldPath && (oldVers == "" || r.Old.Version == oldVers) { 1183 if need { 1184 // Found replacement for old; update to use new. 1185 r.New = new 1186 f.Syntax.updateLine(r.Syntax, tokens...) 1187 need = false 1188 continue 1189 } 1190 // Already added; delete other replacements for same. 1191 r.Syntax.markRemoved() 1192 *r = Replace{} 1193 } 1194 if r.Old.Path == oldPath { 1195 hint = r.Syntax 1196 } 1197 } 1198 if need { 1199 f.Replace = append(f.Replace, &Replace{Old: old, New: new, Syntax: f.Syntax.addLine(hint, tokens...)}) 1200 } 1201 return nil 1202} 1203 1204func (f *File) DropReplace(oldPath, oldVers string) error { 1205 for _, r := range f.Replace { 1206 if r.Old.Path == oldPath && r.Old.Version == oldVers { 1207 r.Syntax.markRemoved() 1208 *r = Replace{} 1209 } 1210 } 1211 return nil 1212} 1213 1214// AddRetract adds a retract statement to the mod file. Errors if the provided 1215// version interval does not consist of canonical version strings 1216func (f *File) AddRetract(vi VersionInterval, rationale string) error { 1217 var path string 1218 if f.Module != nil { 1219 path = f.Module.Mod.Path 1220 } 1221 if err := checkCanonicalVersion(path, vi.High); err != nil { 1222 return err 1223 } 1224 if err := checkCanonicalVersion(path, vi.Low); err != nil { 1225 return err 1226 } 1227 1228 r := &Retract{ 1229 VersionInterval: vi, 1230 } 1231 if vi.Low == vi.High { 1232 r.Syntax = f.Syntax.addLine(nil, "retract", AutoQuote(vi.Low)) 1233 } else { 1234 r.Syntax = f.Syntax.addLine(nil, "retract", "[", AutoQuote(vi.Low), ",", AutoQuote(vi.High), "]") 1235 } 1236 if rationale != "" { 1237 for _, line := range strings.Split(rationale, "\n") { 1238 com := Comment{Token: "// " + line} 1239 r.Syntax.Comment().Before = append(r.Syntax.Comment().Before, com) 1240 } 1241 } 1242 return nil 1243} 1244 1245func (f *File) DropRetract(vi VersionInterval) error { 1246 for _, r := range f.Retract { 1247 if r.VersionInterval == vi { 1248 r.Syntax.markRemoved() 1249 *r = Retract{} 1250 } 1251 } 1252 return nil 1253} 1254 1255func (f *File) SortBlocks() { 1256 f.removeDups() // otherwise sorting is unsafe 1257 1258 for _, stmt := range f.Syntax.Stmt { 1259 block, ok := stmt.(*LineBlock) 1260 if !ok { 1261 continue 1262 } 1263 less := lineLess 1264 if block.Token[0] == "retract" { 1265 less = lineRetractLess 1266 } 1267 sort.SliceStable(block.Line, func(i, j int) bool { 1268 return less(block.Line[i], block.Line[j]) 1269 }) 1270 } 1271} 1272 1273// removeDups removes duplicate exclude and replace directives. 1274// 1275// Earlier exclude directives take priority. 1276// 1277// Later replace directives take priority. 1278// 1279// require directives are not de-duplicated. That's left up to higher-level 1280// logic (MVS). 1281// 1282// retract directives are not de-duplicated since comments are 1283// meaningful, and versions may be retracted multiple times. 1284func (f *File) removeDups() { 1285 kill := make(map[*Line]bool) 1286 1287 // Remove duplicate excludes. 1288 haveExclude := make(map[module.Version]bool) 1289 for _, x := range f.Exclude { 1290 if haveExclude[x.Mod] { 1291 kill[x.Syntax] = true 1292 continue 1293 } 1294 haveExclude[x.Mod] = true 1295 } 1296 var excl []*Exclude 1297 for _, x := range f.Exclude { 1298 if !kill[x.Syntax] { 1299 excl = append(excl, x) 1300 } 1301 } 1302 f.Exclude = excl 1303 1304 // Remove duplicate replacements. 1305 // Later replacements take priority over earlier ones. 1306 haveReplace := make(map[module.Version]bool) 1307 for i := len(f.Replace) - 1; i >= 0; i-- { 1308 x := f.Replace[i] 1309 if haveReplace[x.Old] { 1310 kill[x.Syntax] = true 1311 continue 1312 } 1313 haveReplace[x.Old] = true 1314 } 1315 var repl []*Replace 1316 for _, x := range f.Replace { 1317 if !kill[x.Syntax] { 1318 repl = append(repl, x) 1319 } 1320 } 1321 f.Replace = repl 1322 1323 // Duplicate require and retract directives are not removed. 1324 1325 // Drop killed statements from the syntax tree. 1326 var stmts []Expr 1327 for _, stmt := range f.Syntax.Stmt { 1328 switch stmt := stmt.(type) { 1329 case *Line: 1330 if kill[stmt] { 1331 continue 1332 } 1333 case *LineBlock: 1334 var lines []*Line 1335 for _, line := range stmt.Line { 1336 if !kill[line] { 1337 lines = append(lines, line) 1338 } 1339 } 1340 stmt.Line = lines 1341 if len(lines) == 0 { 1342 continue 1343 } 1344 } 1345 stmts = append(stmts, stmt) 1346 } 1347 f.Syntax.Stmt = stmts 1348} 1349 1350// lineLess returns whether li should be sorted before lj. It sorts 1351// lexicographically without assigning any special meaning to tokens. 1352func lineLess(li, lj *Line) bool { 1353 for k := 0; k < len(li.Token) && k < len(lj.Token); k++ { 1354 if li.Token[k] != lj.Token[k] { 1355 return li.Token[k] < lj.Token[k] 1356 } 1357 } 1358 return len(li.Token) < len(lj.Token) 1359} 1360 1361// lineRetractLess returns whether li should be sorted before lj for lines in 1362// a "retract" block. It treats each line as a version interval. Single versions 1363// are compared as if they were intervals with the same low and high version. 1364// Intervals are sorted in descending order, first by low version, then by 1365// high version, using semver.Compare. 1366func lineRetractLess(li, lj *Line) bool { 1367 interval := func(l *Line) VersionInterval { 1368 if len(l.Token) == 1 { 1369 return VersionInterval{Low: l.Token[0], High: l.Token[0]} 1370 } else if len(l.Token) == 5 && l.Token[0] == "[" && l.Token[2] == "," && l.Token[4] == "]" { 1371 return VersionInterval{Low: l.Token[1], High: l.Token[3]} 1372 } else { 1373 // Line in unknown format. Treat as an invalid version. 1374 return VersionInterval{} 1375 } 1376 } 1377 vii := interval(li) 1378 vij := interval(lj) 1379 if cmp := semver.Compare(vii.Low, vij.Low); cmp != 0 { 1380 return cmp > 0 1381 } 1382 return semver.Compare(vii.High, vij.High) > 0 1383} 1384 1385// checkCanonicalVersion returns a non-nil error if vers is not a canonical 1386// version string or does not match the major version of path. 1387// 1388// If path is non-empty, the error text suggests a format with a major version 1389// corresponding to the path. 1390func checkCanonicalVersion(path, vers string) error { 1391 _, pathMajor, pathMajorOk := module.SplitPathVersion(path) 1392 1393 if vers == "" || vers != module.CanonicalVersion(vers) { 1394 if pathMajor == "" { 1395 return &module.InvalidVersionError{ 1396 Version: vers, 1397 Err: fmt.Errorf("must be of the form v1.2.3"), 1398 } 1399 } 1400 return &module.InvalidVersionError{ 1401 Version: vers, 1402 Err: fmt.Errorf("must be of the form %s.2.3", module.PathMajorPrefix(pathMajor)), 1403 } 1404 } 1405 1406 if pathMajorOk { 1407 if err := module.CheckPathMajor(vers, pathMajor); err != nil { 1408 if pathMajor == "" { 1409 // In this context, the user probably wrote "v2.3.4" when they meant 1410 // "v2.3.4+incompatible". Suggest that instead of "v0 or v1". 1411 return &module.InvalidVersionError{ 1412 Version: vers, 1413 Err: fmt.Errorf("should be %s+incompatible (or module %s/%v)", vers, path, semver.Major(vers)), 1414 } 1415 } 1416 return err 1417 } 1418 } 1419 1420 return nil 1421} 1422