modfile: support retract directive and version intervals
This CL adds support for parsing and programmatically adding and
removing a new directive, "retract", as described in golang/go#24031.
The "retract" directive comes in two forms:
retract v1.0.0 // single version
retract [v1.1.0, v1.2.0] // closed interval
Updates golang/go#24031
Change-Id: I1236c7d89e7674abf694e49e9b4869b14a59fac0
Reviewed-on: https://go-review.googlesource.com/c/mod/+/228039
Run-TryBot: Jay Conrod <jayconrod@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Michael Matloob <matloob@golang.org>
diff --git a/modfile/rule.go b/modfile/rule.go
index 91ca682..83398dd 100644
--- a/modfile/rule.go
+++ b/modfile/rule.go
@@ -30,6 +30,7 @@
"golang.org/x/mod/internal/lazyregexp"
"golang.org/x/mod/module"
+ "golang.org/x/mod/semver"
)
// A File is the parsed, interpreted form of a go.mod file.
@@ -39,6 +40,7 @@
Require []*Require
Exclude []*Exclude
Replace []*Replace
+ Retract []*Retract
Syntax *FileSyntax
}
@@ -75,6 +77,21 @@
Syntax *Line
}
+// A Retract is a single retract statement.
+type Retract struct {
+ VersionInterval
+ Rationale string
+ Syntax *Line
+}
+
+// A VersionInterval represents a range of versions with upper and lower bounds.
+// Intervals are closed: both bounds are included. When Low is equal to High,
+// the interval may refer to a single version ('v1.2.3') or an interval
+// ('[v1.2.3, v1.2.3]'); both have the same representation.
+type VersionInterval struct {
+ Low, High string
+}
+
func (f *File) AddModuleStmt(path string) error {
if f.Syntax == nil {
f.Syntax = new(FileSyntax)
@@ -138,7 +155,7 @@
for _, x := range fs.Stmt {
switch x := x.(type) {
case *Line:
- f.add(&errs, x, x.Token[0], x.Token[1:], fix, strict)
+ f.add(&errs, nil, x, x.Token[0], x.Token[1:], fix, strict)
case *LineBlock:
if len(x.Token) > 1 {
@@ -161,9 +178,9 @@
})
}
continue
- case "module", "require", "exclude", "replace":
+ case "module", "require", "exclude", "replace", "retract":
for _, l := range x.Line {
- f.add(&errs, l, x.Token[0], l.Token, fix, strict)
+ f.add(&errs, x, l, x.Token[0], l.Token, fix, strict)
}
}
}
@@ -177,7 +194,7 @@
var GoVersionRE = lazyregexp.New(`^([1-9][0-9]*)\.(0|[1-9][0-9]*)$`)
-func (f *File) add(errs *ErrorList, line *Line, verb string, args []string, fix VersionFixer, strict bool) {
+func (f *File) add(errs *ErrorList, block *LineBlock, line *Line, verb string, args []string, fix VersionFixer, strict bool) {
// If strict is false, this module is a dependency.
// We ignore all unknown directives as well as main-module-only
// directives like replace and exclude. It will work better for
@@ -186,7 +203,7 @@
// and simply ignore those statements.
if !strict {
switch verb {
- case "module", "require", "go":
+ case "go", "module", "retract", "require":
// want these even for dependency go.mods
default:
return
@@ -232,6 +249,7 @@
f.Go = &Go{Syntax: line}
f.Go.Version = args[0]
+
case "module":
if f.Module != nil {
errorf("repeated module statement")
@@ -248,6 +266,7 @@
return
}
f.Module.Mod = module.Version{Path: s}
+
case "require", "exclude":
if len(args) != 2 {
errorf("usage: %s module/path v1.2.3", verb)
@@ -284,6 +303,7 @@
Syntax: line,
})
}
+
case "replace":
arrow := 2
if len(args) >= 2 && args[1] == "=>" {
@@ -347,6 +367,33 @@
New: module.Version{Path: ns, Version: nv},
Syntax: line,
})
+
+ case "retract":
+ rationale := parseRetractRationale(block, line)
+ vi, err := parseVersionInterval(verb, &args, fix)
+ if err != nil {
+ if strict {
+ wrapError(err)
+ return
+ } else {
+ // Only report errors parsing intervals in the main module. We may
+ // support additional syntax in the future, such as open and half-open
+ // intervals. Those can't be supported now, because they break the
+ // go.mod parser, even in lax mode.
+ return
+ }
+ }
+ if len(args) > 0 && strict {
+ // In the future, there may be additional information after the version.
+ errorf("unexpected token after version: %q", args[0])
+ return
+ }
+ retract := &Retract{
+ VersionInterval: vi,
+ Rationale: rationale,
+ Syntax: line,
+ }
+ f.Retract = append(f.Retract, retract)
}
}
@@ -444,6 +491,53 @@
return s
}
+func parseVersionInterval(verb string, args *[]string, fix VersionFixer) (VersionInterval, error) {
+ toks := *args
+ if len(toks) == 0 || toks[0] == "(" {
+ return VersionInterval{}, fmt.Errorf("expected '[' or version")
+ }
+ if toks[0] != "[" {
+ v, err := parseVersion(verb, "", &toks[0], fix)
+ if err != nil {
+ return VersionInterval{}, err
+ }
+ *args = toks[1:]
+ return VersionInterval{Low: v, High: v}, nil
+ }
+ toks = toks[1:]
+
+ if len(toks) == 0 {
+ return VersionInterval{}, fmt.Errorf("expected version after '['")
+ }
+ low, err := parseVersion(verb, "", &toks[0], fix)
+ if err != nil {
+ return VersionInterval{}, err
+ }
+ toks = toks[1:]
+
+ if len(toks) == 0 || toks[0] != "," {
+ return VersionInterval{}, fmt.Errorf("expected ',' after version")
+ }
+ toks = toks[1:]
+
+ if len(toks) == 0 {
+ return VersionInterval{}, fmt.Errorf("expected version after ','")
+ }
+ high, err := parseVersion(verb, "", &toks[0], fix)
+ if err != nil {
+ return VersionInterval{}, err
+ }
+ toks = toks[1:]
+
+ if len(toks) == 0 || toks[0] != "]" {
+ return VersionInterval{}, fmt.Errorf("expected ']' after version")
+ }
+ toks = toks[1:]
+
+ *args = toks
+ return VersionInterval{Low: low, High: high}, nil
+}
+
func parseString(s *string) (string, error) {
t := *s
if strings.HasPrefix(t, `"`) {
@@ -461,6 +555,27 @@
return t, nil
}
+// parseRetractRationale extracts the rationale for a retract directive from the
+// surrounding comments. If the line does not have comments and is part of a
+// block that does have comments, the block's comments are used.
+func parseRetractRationale(block *LineBlock, line *Line) string {
+ comments := line.Comment()
+ if block != nil && len(comments.Before) == 0 && len(comments.Suffix) == 0 {
+ comments = block.Comment()
+ }
+ groups := [][]Comment{comments.Before, comments.Suffix}
+ var lines []string
+ for _, g := range groups {
+ for _, c := range g {
+ if !strings.HasPrefix(c.Token, "//") {
+ continue // blank line
+ }
+ lines = append(lines, strings.TrimSpace(strings.TrimPrefix(c.Token, "//")))
+ }
+ }
+ return strings.Join(lines, "\n")
+}
+
type ErrorList []Error
func (e ErrorList) Error() string {
@@ -494,6 +609,8 @@
var directive string
if e.ModPath != "" {
directive = fmt.Sprintf("%s %s: ", e.Verb, e.ModPath)
+ } else if e.Verb != "" {
+ directive = fmt.Sprintf("%s: ", e.Verb)
}
return pos + directive + e.Err.Error()
@@ -585,6 +702,15 @@
}
f.Replace = f.Replace[:w]
+ w = 0
+ for _, r := range f.Retract {
+ if r.Low != "" || r.High != "" {
+ f.Retract[w] = r
+ w++
+ }
+ }
+ f.Retract = f.Retract[:w]
+
f.Syntax.Cleanup()
}
@@ -778,6 +904,34 @@
return nil
}
+func (f *File) AddRetract(vi VersionInterval, rationale string) error {
+ r := &Retract{
+ VersionInterval: vi,
+ }
+ if vi.Low == vi.High {
+ r.Syntax = f.Syntax.addLine(nil, "retract", AutoQuote(vi.Low))
+ } else {
+ r.Syntax = f.Syntax.addLine(nil, "retract", "[", AutoQuote(vi.Low), ",", AutoQuote(vi.High), "]")
+ }
+ if rationale != "" {
+ for _, line := range strings.Split(rationale, "\n") {
+ com := Comment{Token: "// " + line}
+ r.Syntax.Comment().Before = append(r.Syntax.Comment().Before, com)
+ }
+ }
+ return nil
+}
+
+func (f *File) DropRetract(vi VersionInterval) error {
+ for _, r := range f.Retract {
+ if r.VersionInterval == vi {
+ f.Syntax.removeLine(r.Syntax)
+ *r = Retract{}
+ }
+ }
+ return nil
+}
+
func (f *File) SortBlocks() {
f.removeDups() // otherwise sorting is unsafe
@@ -786,28 +940,38 @@
if !ok {
continue
}
- sort.Slice(block.Line, func(i, j int) bool {
- li := block.Line[i]
- lj := block.Line[j]
- for k := 0; k < len(li.Token) && k < len(lj.Token); k++ {
- if li.Token[k] != lj.Token[k] {
- return li.Token[k] < lj.Token[k]
- }
- }
- return len(li.Token) < len(lj.Token)
+ less := lineLess
+ if block.Token[0] == "retract" {
+ less = lineRetractLess
+ }
+ sort.SliceStable(block.Line, func(i, j int) bool {
+ return less(block.Line[i], block.Line[j])
})
}
}
+// removeDups removes duplicate exclude and replace directives.
+//
+// Earlier exclude directives take priority.
+//
+// Later replace directives take priority.
+//
+// require directives are not de-duplicated. That's left up to higher-level
+// logic (MVS).
+//
+// retract directives are not de-duplicated since comments are
+// meaningful, and versions may be retracted multiple times.
func (f *File) removeDups() {
- have := make(map[module.Version]bool)
kill := make(map[*Line]bool)
+
+ // Remove duplicate excludes.
+ haveExclude := make(map[module.Version]bool)
for _, x := range f.Exclude {
- if have[x.Mod] {
+ if haveExclude[x.Mod] {
kill[x.Syntax] = true
continue
}
- have[x.Mod] = true
+ haveExclude[x.Mod] = true
}
var excl []*Exclude
for _, x := range f.Exclude {
@@ -817,15 +981,16 @@
}
f.Exclude = excl
- have = make(map[module.Version]bool)
+ // Remove duplicate replacements.
// Later replacements take priority over earlier ones.
+ haveReplace := make(map[module.Version]bool)
for i := len(f.Replace) - 1; i >= 0; i-- {
x := f.Replace[i]
- if have[x.Old] {
+ if haveReplace[x.Old] {
kill[x.Syntax] = true
continue
}
- have[x.Old] = true
+ haveReplace[x.Old] = true
}
var repl []*Replace
for _, x := range f.Replace {
@@ -835,6 +1000,9 @@
}
f.Replace = repl
+ // Duplicate require and retract directives are not removed.
+
+ // Drop killed statements from the syntax tree.
var stmts []Expr
for _, stmt := range f.Syntax.Stmt {
switch stmt := stmt.(type) {
@@ -858,3 +1026,38 @@
}
f.Syntax.Stmt = stmts
}
+
+// lineLess returns whether li should be sorted before lj. It sorts
+// lexicographically without assigning any special meaning to tokens.
+func lineLess(li, lj *Line) bool {
+ for k := 0; k < len(li.Token) && k < len(lj.Token); k++ {
+ if li.Token[k] != lj.Token[k] {
+ return li.Token[k] < lj.Token[k]
+ }
+ }
+ return len(li.Token) < len(lj.Token)
+}
+
+// lineRetractLess returns whether li should be sorted before lj for lines in
+// a "retract" block. It treats each line as a version interval. Single versions
+// are compared as if they were intervals with the same low and high version.
+// Intervals are sorted in descending order, first by low version, then by
+// high version, using semver.Compare.
+func lineRetractLess(li, lj *Line) bool {
+ interval := func(l *Line) VersionInterval {
+ if len(l.Token) == 1 {
+ return VersionInterval{Low: l.Token[0], High: l.Token[0]}
+ } else if len(l.Token) == 5 && l.Token[0] == "[" && l.Token[2] == "," && l.Token[4] == "]" {
+ return VersionInterval{Low: l.Token[1], High: l.Token[3]}
+ } else {
+ // Line in unknown format. Treat as an invalid version.
+ return VersionInterval{}
+ }
+ }
+ vii := interval(li)
+ vij := interval(lj)
+ if cmp := semver.Compare(vii.Low, vij.Low); cmp != 0 {
+ return cmp > 0
+ }
+ return semver.Compare(vii.High, vij.High) > 0
+}
diff --git a/modfile/rule_test.go b/modfile/rule_test.go
index c2c28f9..fbf144d 100644
--- a/modfile/rule_test.go
+++ b/modfile/rule_test.go
@@ -6,19 +6,20 @@
import (
"bytes"
- "fmt"
"testing"
"golang.org/x/mod/module"
)
var addRequireTests = []struct {
+ desc string
in string
path string
vers string
out string
}{
{
+ `existing`,
`
module m
require x.y/z v1.2.3
@@ -30,6 +31,7 @@
`,
},
{
+ `new`,
`
module m
require x.y/z v1.2.3
@@ -44,6 +46,7 @@
`,
},
{
+ `new2`,
`
module m
require x.y/z v1.2.3
@@ -62,6 +65,7 @@
}
var setRequireTests = []struct {
+ desc string
in string
mods []struct {
path string
@@ -71,6 +75,7 @@
out string
}{
{
+ `existing`,
`module m
require (
x.y/b v1.2.3
@@ -97,6 +102,7 @@
`,
},
{
+ `existing_indirect`,
`module m
require (
x.y/a v1.2.3
@@ -136,18 +142,23 @@
}
var addGoTests = []struct {
+ desc string
in string
version string
out string
}{
- {`module m
+ {
+ `module_only`,
+ `module m
`,
`1.14`,
`module m
go 1.14
`,
},
- {`module m
+ {
+ `module_before_require`,
+ `module m
require x.y/a v1.2.3
`,
`1.14`,
@@ -157,6 +168,7 @@
`,
},
{
+ `require_before_module`,
`require x.y/a v1.2.3
module example.com/inverted
`,
@@ -167,6 +179,7 @@
`,
},
{
+ `require_only`,
`require x.y/a v1.2.3
`,
`1.14`,
@@ -176,51 +189,398 @@
},
}
-func TestAddRequire(t *testing.T) {
- for i, tt := range addRequireTests {
- t.Run(fmt.Sprintf("#%d", i), func(t *testing.T) {
- f, err := Parse("in", []byte(tt.in), nil)
- if err != nil {
- t.Fatal(err)
- }
- g, err := Parse("out", []byte(tt.out), nil)
- if err != nil {
- t.Fatal(err)
- }
- golden, err := g.Format()
- if err != nil {
- t.Fatal(err)
- }
+var addRetractTests = []struct {
+ desc string
+ in string
+ low string
+ high string
+ rationale string
+ out string
+}{
+ {
+ `new_singleton`,
+ `module m
+ `,
+ `v1.2.3`,
+ `v1.2.3`,
+ ``,
+ `module m
+ retract v1.2.3
+ `,
+ },
+ {
+ `new_interval`,
+ `module m
+ `,
+ `v1.0.0`,
+ `v1.1.0`,
+ ``,
+ `module m
+ retract [v1.0.0, v1.1.0]`,
+ },
+ {
+ `duplicate_with_rationale`,
+ `module m
+ retract v1.2.3
+ `,
+ `v1.2.3`,
+ `v1.2.3`,
+ `bad`,
+ `module m
+ retract (
+ v1.2.3
+ // bad
+ v1.2.3
+ )
+ `,
+ },
+ {
+ `duplicate_multiline_rationale`,
+ `module m
+ retract [v1.2.3, v1.2.3]
+ `,
+ `v1.2.3`,
+ `v1.2.3`,
+ `multi
+line`,
+ `module m
+ retract (
+ [v1.2.3, v1.2.3]
+ // multi
+ // line
+ v1.2.3
+ )
+ `,
+ },
+ {
+ `duplicate_interval`,
+ `module m
+ retract [v1.0.0, v1.1.0]
+ `,
+ `v1.0.0`,
+ `v1.1.0`,
+ ``,
+ `module m
+ retract (
+ [v1.0.0, v1.1.0]
+ [v1.0.0, v1.1.0]
+ )
+ `,
+ },
+ {
+ `duplicate_singleton`,
+ `module m
+ retract v1.2.3
+ `,
+ `v1.2.3`,
+ `v1.2.3`,
+ ``,
+ `module m
+ retract (
+ v1.2.3
+ v1.2.3
+ )
+ `,
+ },
+}
- if err := f.AddRequire(tt.path, tt.vers); err != nil {
- t.Fatal(err)
- }
- out, err := f.Format()
- if err != nil {
- t.Fatal(err)
- }
- if !bytes.Equal(out, golden) {
- t.Errorf("have:\n%s\nwant:\n%s", out, golden)
- }
+var dropRetractTests = []struct {
+ desc string
+ in string
+ low string
+ high string
+ out string
+}{
+ {
+ `singleton_no_match`,
+ `module m
+ retract v1.2.3
+ `,
+ `v1.0.0`,
+ `v1.0.0`,
+ `module m
+ retract v1.2.3
+ `,
+ },
+ {
+ `singleton_match_one`,
+ `module m
+ retract v1.2.2
+ retract v1.2.3
+ retract v1.2.4
+ `,
+ `v1.2.3`,
+ `v1.2.3`,
+ `module m
+ retract v1.2.2
+ retract v1.2.4
+ `,
+ },
+ {
+ `singleton_match_all`,
+ `module m
+ retract v1.2.3 // first
+ retract v1.2.3 // second
+ `,
+ `v1.2.3`,
+ `v1.2.3`,
+ `module m
+ `,
+ },
+ {
+ `interval_match`,
+ `module m
+ retract [v1.2.3, v1.2.3]
+ `,
+ `v1.2.3`,
+ `v1.2.3`,
+ `module m
+ `,
+ },
+ {
+ `interval_superset_no_match`,
+ `module m
+ retract [v1.0.0, v1.1.0]
+ `,
+ `v1.0.0`,
+ `v1.2.0`,
+ `module m
+ retract [v1.0.0, v1.1.0]
+ `,
+ },
+ {
+ `singleton_match_middle`,
+ `module m
+ retract v1.2.3
+ `,
+ `v1.2.3`,
+ `v1.2.3`,
+ `module m
+ `,
+ },
+ {
+ `interval_match_middle_block`,
+ `module m
+ retract (
+ v1.0.0
+ [v1.1.0, v1.2.0]
+ v1.3.0
+ )
+ `,
+ `v1.1.0`,
+ `v1.2.0`,
+ `module m
+ retract (
+ v1.0.0
+ v1.3.0
+ )
+ `,
+ },
+ {
+ `interval_match_all`,
+ `module m
+ retract [v1.0.0, v1.1.0]
+ retract [v1.0.0, v1.1.0]
+ `,
+ `v1.0.0`,
+ `v1.1.0`,
+ `module m
+ `,
+ },
+}
+
+var retractRationaleTests = []struct {
+ desc, in, want string
+}{
+ {
+ `no_comment`,
+ `module m
+ retract v1.0.0`,
+ ``,
+ },
+ {
+ `prefix_one`,
+ `module m
+ // prefix
+ retract v1.0.0
+ `,
+ `prefix`,
+ },
+ {
+ `prefix_multiline`,
+ `module m
+ // one
+ //
+ // two
+ //
+ // three
+ retract v1.0.0`,
+ `one
+
+two
+
+three`,
+ },
+ {
+ `suffix`,
+ `module m
+ retract v1.0.0 // suffix
+ `,
+ `suffix`,
+ },
+ {
+ `prefix_suffix_after`,
+ `module m
+ // prefix
+ retract v1.0.0 // suffix
+ `,
+ `prefix
+suffix`,
+ },
+ {
+ `block_only`,
+ `// block
+ retract (
+ v1.0.0
+ )
+ `,
+ `block`,
+ },
+ {
+ `block_and_line`,
+ `// block
+ retract (
+ // line
+ v1.0.0
+ )
+ `,
+ `line`,
+ },
+}
+
+var sortBlocksTests = []struct {
+ desc, in, out string
+ strict bool
+}{
+ {
+ `exclude_duplicates_removed`,
+ `module m
+ exclude x.y/z v1.0.0 // a
+ exclude x.y/z v1.0.0 // b
+ exclude (
+ x.y/w v1.1.0
+ x.y/z v1.0.0 // c
+ )
+ `,
+ `module m
+ exclude x.y/z v1.0.0 // a
+ exclude (
+ x.y/w v1.1.0
+ )`,
+ true,
+ },
+ {
+ `replace_duplicates_removed`,
+ `module m
+ replace x.y/z v1.0.0 => ./a
+ replace x.y/z v1.1.0 => ./b
+ replace (
+ x.y/z v1.0.0 => ./c
+ )
+ `,
+ `module m
+ replace x.y/z v1.1.0 => ./b
+ replace (
+ x.y/z v1.0.0 => ./c
+ )
+ `,
+ true,
+ },
+ {
+ `retract_duplicates_not_removed`,
+ `module m
+ // block
+ retract (
+ v1.0.0 // one
+ v1.0.0 // two
+ )`,
+ `module m
+ // block
+ retract (
+ v1.0.0 // one
+ v1.0.0 // two
+ )`,
+ true,
+ },
+ // Tests below this point just check sort order.
+ // Non-retract blocks are sorted lexicographically in ascending order.
+ // retract blocks are sorted using semver in descending order.
+ {
+ `sort_lexicographically`,
+ `module m
+ sort (
+ aa
+ cc
+ bb
+ zz
+ v1.2.0
+ v1.11.0
+ )`,
+ `module m
+ sort (
+ aa
+ bb
+ cc
+ v1.11.0
+ v1.2.0
+ zz
+ )
+ `,
+ false,
+ },
+ {
+ `sort_retract`,
+ `module m
+ retract (
+ [v1.2.0, v1.3.0]
+ [v1.1.0, v1.3.0]
+ [v1.1.0, v1.2.0]
+ v1.0.0
+ v1.1.0
+ v1.2.0
+ v1.3.0
+ v1.4.0
+ )
+ `,
+ `module m
+ retract (
+ v1.4.0
+ v1.3.0
+ [v1.2.0, v1.3.0]
+ v1.2.0
+ [v1.1.0, v1.3.0]
+ [v1.1.0, v1.2.0]
+ v1.1.0
+ v1.0.0
+ )
+ `,
+ false,
+ },
+}
+
+func TestAddRequire(t *testing.T) {
+ for _, tt := range addRequireTests {
+ t.Run(tt.desc, func(t *testing.T) {
+ testEdit(t, tt.in, tt.out, true, func(f *File) error {
+ return f.AddRequire(tt.path, tt.vers)
+ })
})
}
}
func TestSetRequire(t *testing.T) {
- for i, tt := range setRequireTests {
- t.Run(fmt.Sprintf("#%d", i), func(t *testing.T) {
- f, err := Parse("in", []byte(tt.in), nil)
- if err != nil {
- t.Fatal(err)
- }
- g, err := Parse("out", []byte(tt.out), nil)
- if err != nil {
- t.Fatal(err)
- }
- golden, err := g.Format()
- if err != nil {
- t.Fatal(err)
- }
+ for _, tt := range setRequireTests {
+ t.Run(tt.desc, func(t *testing.T) {
var mods []*Require
for _, mod := range tt.mods {
mods = append(mods, &Require{
@@ -232,14 +592,10 @@
})
}
- f.SetRequire(mods)
- out, err := f.Format()
- if err != nil {
- t.Fatal(err)
- }
- if !bytes.Equal(out, golden) {
- t.Errorf("have:\n%s\nwant:\n%s", out, golden)
- }
+ f := testEdit(t, tt.in, tt.out, true, func(f *File) error {
+ f.SetRequire(mods)
+ return nil
+ })
f.Cleanup()
if len(f.Require) != len(mods) {
@@ -250,31 +606,96 @@
}
func TestAddGo(t *testing.T) {
- for i, tt := range addGoTests {
- t.Run(fmt.Sprintf("#%d", i), func(t *testing.T) {
+ for _, tt := range addGoTests {
+ t.Run(tt.desc, func(t *testing.T) {
+ testEdit(t, tt.in, tt.out, true, func(f *File) error {
+ return f.AddGoStmt(tt.version)
+ })
+ })
+ }
+}
+
+func TestAddRetract(t *testing.T) {
+ for _, tt := range addRetractTests {
+ t.Run(tt.desc, func(t *testing.T) {
+ testEdit(t, tt.in, tt.out, true, func(f *File) error {
+ return f.AddRetract(VersionInterval{Low: tt.low, High: tt.high}, tt.rationale)
+ })
+ })
+ }
+}
+
+func TestDropRetract(t *testing.T) {
+ for _, tt := range dropRetractTests {
+ t.Run(tt.desc, func(t *testing.T) {
+ testEdit(t, tt.in, tt.out, true, func(f *File) error {
+ if err := f.DropRetract(VersionInterval{Low: tt.low, High: tt.high}); err != nil {
+ return err
+ }
+ f.Cleanup()
+ return nil
+ })
+ })
+ }
+}
+
+func TestRetractRationale(t *testing.T) {
+ for _, tt := range retractRationaleTests {
+ t.Run(tt.desc, func(t *testing.T) {
f, err := Parse("in", []byte(tt.in), nil)
if err != nil {
t.Fatal(err)
}
- g, err := Parse("out", []byte(tt.out), nil)
- if err != nil {
- t.Fatal(err)
+ if len(f.Retract) != 1 {
+ t.Fatalf("got %d retract directives; want 1", len(f.Retract))
}
- golden, err := g.Format()
- if err != nil {
- t.Fatal(err)
- }
-
- if err := f.AddGoStmt(tt.version); err != nil {
- t.Fatal(err)
- }
- out, err := f.Format()
- if err != nil {
- t.Fatal(err)
- }
- if !bytes.Equal(out, golden) {
- t.Errorf("have:\n%s\nwant:\n%s", out, golden)
+ if got := f.Retract[0].Rationale; got != tt.want {
+ t.Errorf("got %q; want %q", got, tt.want)
}
})
}
}
+
+func TestSortBlocks(t *testing.T) {
+ for _, tt := range sortBlocksTests {
+ t.Run(tt.desc, func(t *testing.T) {
+ testEdit(t, tt.in, tt.out, tt.strict, func(f *File) error {
+ f.SortBlocks()
+ return nil
+ })
+ })
+ }
+}
+
+func testEdit(t *testing.T, in, want string, strict bool, transform func(f *File) error) *File {
+ t.Helper()
+ parse := Parse
+ if !strict {
+ parse = ParseLax
+ }
+ f, err := parse("in", []byte(in), nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ g, err := parse("out", []byte(want), nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ golden, err := g.Format()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := transform(f); err != nil {
+ t.Fatal(err)
+ }
+ out, err := f.Format()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(out, golden) {
+ t.Errorf("have:\n%s\nwant:\n%s", out, golden)
+ }
+
+ return f
+}
diff --git a/modfile/testdata/retract.golden b/modfile/testdata/retract.golden
new file mode 100644
index 0000000..f5d709e
--- /dev/null
+++ b/modfile/testdata/retract.golden
@@ -0,0 +1,11 @@
+module abc
+
+retract v1.2.3
+
+retract [v1.2.3, v1.2.4]
+
+retract (
+ v1.2.3
+
+ [v1.2.3, v1.2.4]
+)
diff --git a/modfile/testdata/retract.in b/modfile/testdata/retract.in
new file mode 100644
index 0000000..fa4a1f4
--- /dev/null
+++ b/modfile/testdata/retract.in
@@ -0,0 +1,11 @@
+module abc
+
+retract "v1.2.3"
+
+retract [ "v1.2.3" , "v1.2.4" ]
+
+retract (
+ "v1.2.3"
+
+ [ "v1.2.3" , "v1.2.4" ]
+)