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