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