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 @@ import ( "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 @@ type File struct { Require []*Require Exclude []*Exclude Replace []*Replace + Retract []*Retract Syntax *FileSyntax } @@ -75,6 +77,21 @@ type Replace struct { 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 @@ func parseToFile(file string, data []byte, fix VersionFixer, strict bool) (*File 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 @@ func parseToFile(file string, data []byte, fix VersionFixer, strict bool) (*File }) } 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 @@ func parseToFile(file string, data []byte, fix VersionFixer, strict bool) (*File 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 @@ func (f *File) add(errs *ErrorList, line *Line, verb string, args []string, fix // 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 @@ func (f *File) add(errs *ErrorList, line *Line, verb string, args []string, fix f.Go = &Go{Syntax: line} f.Go.Version = args[0] + case "module": if f.Module != nil { errorf("repeated module statement") @@ -248,6 +266,7 @@ func (f *File) add(errs *ErrorList, line *Line, verb string, args []string, fix 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 @@ func (f *File) add(errs *ErrorList, line *Line, verb string, args []string, fix Syntax: line, }) } + case "replace": arrow := 2 if len(args) >= 2 && args[1] == "=>" { @@ -347,6 +367,33 @@ func (f *File) add(errs *ErrorList, line *Line, verb string, args []string, fix 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 @@ func AutoQuote(s string) string { 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 @@ func parseString(s *string) (string, error) { 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 @@ func (e *Error) Error() string { 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 @@ func (f *File) Cleanup() { } 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 @@ func (f *File) DropReplace(oldPath, oldVers string) error { 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 @@ func (f *File) SortBlocks() { 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 @@ func (f *File) removeDups() { } 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 @@ func (f *File) removeDups() { } 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 @@ func (f *File) removeDups() { } 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 @@ package modfile 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 @@ var addRequireTests = []struct { `, }, { + `new`, ` module m require x.y/z v1.2.3 @@ -44,6 +46,7 @@ var addRequireTests = []struct { `, }, { + `new2`, ` module m require x.y/z v1.2.3 @@ -62,6 +65,7 @@ var addRequireTests = []struct { } var setRequireTests = []struct { + desc string in string mods []struct { path string @@ -71,6 +75,7 @@ var setRequireTests = []struct { out string }{ { + `existing`, `module m require ( x.y/b v1.2.3 @@ -97,6 +102,7 @@ var setRequireTests = []struct { `, }, { + `existing_indirect`, `module m require ( x.y/a v1.2.3 @@ -136,18 +142,23 @@ var setRequireTests = []struct { } 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 @@ var addGoTests = []struct { `, }, { + `require_before_module`, `require x.y/a v1.2.3 module example.com/inverted `, @@ -167,6 +179,7 @@ var addGoTests = []struct { `, }, { + `require_only`, `require x.y/a v1.2.3 `, `1.14`, @@ -176,51 +189,398 @@ var addGoTests = []struct { }, } -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 @@ func TestSetRequire(t *testing.T) { }) } - 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 TestSetRequire(t *testing.T) { } 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 got := f.Retract[0].Rationale; got != tt.want { + t.Errorf("got %q; want %q", got, tt.want) } + }) + } +} - 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) - } +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" ] +)