Skip to content

Commit c0d644d

Browse files
author
Jay Conrod
committed
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 <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Michael Matloob <[email protected]>
1 parent 89ce4c7 commit c0d644d

File tree

4 files changed

+734
-88
lines changed

4 files changed

+734
-88
lines changed

modfile/rule.go

+223-20
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030

3131
"golang.org/x/mod/internal/lazyregexp"
3232
"golang.org/x/mod/module"
33+
"golang.org/x/mod/semver"
3334
)
3435

3536
// A File is the parsed, interpreted form of a go.mod file.
@@ -39,6 +40,7 @@ type File struct {
3940
Require []*Require
4041
Exclude []*Exclude
4142
Replace []*Replace
43+
Retract []*Retract
4244

4345
Syntax *FileSyntax
4446
}
@@ -75,6 +77,21 @@ type Replace struct {
7577
Syntax *Line
7678
}
7779

80+
// A Retract is a single retract statement.
81+
type 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.
91+
type VersionInterval struct {
92+
Low, High string
93+
}
94+
7895
func (f *File) AddModuleStmt(path string) error {
7996
if f.Syntax == nil {
8097
f.Syntax = new(FileSyntax)
@@ -138,7 +155,7 @@ func parseToFile(file string, data []byte, fix VersionFixer, strict bool) (*File
138155
for _, x := range fs.Stmt {
139156
switch x := x.(type) {
140157
case *Line:
141-
f.add(&errs, x, x.Token[0], x.Token[1:], fix, strict)
158+
f.add(&errs, nil, x, x.Token[0], x.Token[1:], fix, strict)
142159

143160
case *LineBlock:
144161
if len(x.Token) > 1 {
@@ -161,9 +178,9 @@ func parseToFile(file string, data []byte, fix VersionFixer, strict bool) (*File
161178
})
162179
}
163180
continue
164-
case "module", "require", "exclude", "replace":
181+
case "module", "require", "exclude", "replace", "retract":
165182
for _, l := range x.Line {
166-
f.add(&errs, l, x.Token[0], l.Token, fix, strict)
183+
f.add(&errs, x, l, x.Token[0], l.Token, fix, strict)
167184
}
168185
}
169186
}
@@ -177,7 +194,7 @@ func parseToFile(file string, data []byte, fix VersionFixer, strict bool) (*File
177194

178195
var GoVersionRE = lazyregexp.New(`^([1-9][0-9]*)\.(0|[1-9][0-9]*)$`)
179196

180-
func (f *File) add(errs *ErrorList, line *Line, verb string, args []string, fix VersionFixer, strict bool) {
197+
func (f *File) add(errs *ErrorList, block *LineBlock, line *Line, verb string, args []string, fix VersionFixer, strict bool) {
181198
// If strict is false, this module is a dependency.
182199
// We ignore all unknown directives as well as main-module-only
183200
// 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
186203
// and simply ignore those statements.
187204
if !strict {
188205
switch verb {
189-
case "module", "require", "go":
206+
case "go", "module", "retract", "require":
190207
// want these even for dependency go.mods
191208
default:
192209
return
@@ -232,6 +249,7 @@ func (f *File) add(errs *ErrorList, line *Line, verb string, args []string, fix
232249

233250
f.Go = &Go{Syntax: line}
234251
f.Go.Version = args[0]
252+
235253
case "module":
236254
if f.Module != nil {
237255
errorf("repeated module statement")
@@ -248,6 +266,7 @@ func (f *File) add(errs *ErrorList, line *Line, verb string, args []string, fix
248266
return
249267
}
250268
f.Module.Mod = module.Version{Path: s}
269+
251270
case "require", "exclude":
252271
if len(args) != 2 {
253272
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
284303
Syntax: line,
285304
})
286305
}
306+
287307
case "replace":
288308
arrow := 2
289309
if len(args) >= 2 && args[1] == "=>" {
@@ -347,6 +367,33 @@ func (f *File) add(errs *ErrorList, line *Line, verb string, args []string, fix
347367
New: module.Version{Path: ns, Version: nv},
348368
Syntax: line,
349369
})
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)
350397
}
351398
}
352399

@@ -444,6 +491,53 @@ func AutoQuote(s string) string {
444491
return s
445492
}
446493

494+
func 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+
447541
func parseString(s *string) (string, error) {
448542
t := *s
449543
if strings.HasPrefix(t, `"`) {
@@ -461,6 +555,27 @@ func parseString(s *string) (string, error) {
461555
return t, nil
462556
}
463557

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.
561+
func 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+
464579
type ErrorList []Error
465580

466581
func (e ErrorList) Error() string {
@@ -494,6 +609,8 @@ func (e *Error) Error() string {
494609
var directive string
495610
if e.ModPath != "" {
496611
directive = fmt.Sprintf("%s %s: ", e.Verb, e.ModPath)
612+
} else if e.Verb != "" {
613+
directive = fmt.Sprintf("%s: ", e.Verb)
497614
}
498615

499616
return pos + directive + e.Err.Error()
@@ -585,6 +702,15 @@ func (f *File) Cleanup() {
585702
}
586703
f.Replace = f.Replace[:w]
587704

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+
588714
f.Syntax.Cleanup()
589715
}
590716

@@ -778,6 +904,34 @@ func (f *File) DropReplace(oldPath, oldVers string) error {
778904
return nil
779905
}
780906

907+
func (f *File) AddRetract(vi VersionInterval, rationale string) error {
908+
r := &Retract{
909+
VersionInterval: vi,
910+
}
911+
if vi.Low == vi.High {
912+
r.Syntax = f.Syntax.addLine(nil, "retract", AutoQuote(vi.Low))
913+
} else {
914+
r.Syntax = f.Syntax.addLine(nil, "retract", "[", AutoQuote(vi.Low), ",", AutoQuote(vi.High), "]")
915+
}
916+
if rationale != "" {
917+
for _, line := range strings.Split(rationale, "\n") {
918+
com := Comment{Token: "// " + line}
919+
r.Syntax.Comment().Before = append(r.Syntax.Comment().Before, com)
920+
}
921+
}
922+
return nil
923+
}
924+
925+
func (f *File) DropRetract(vi VersionInterval) error {
926+
for _, r := range f.Retract {
927+
if r.VersionInterval == vi {
928+
f.Syntax.removeLine(r.Syntax)
929+
*r = Retract{}
930+
}
931+
}
932+
return nil
933+
}
934+
781935
func (f *File) SortBlocks() {
782936
f.removeDups() // otherwise sorting is unsafe
783937

@@ -786,28 +940,38 @@ func (f *File) SortBlocks() {
786940
if !ok {
787941
continue
788942
}
789-
sort.Slice(block.Line, func(i, j int) bool {
790-
li := block.Line[i]
791-
lj := block.Line[j]
792-
for k := 0; k < len(li.Token) && k < len(lj.Token); k++ {
793-
if li.Token[k] != lj.Token[k] {
794-
return li.Token[k] < lj.Token[k]
795-
}
796-
}
797-
return len(li.Token) < len(lj.Token)
943+
less := lineLess
944+
if block.Token[0] == "retract" {
945+
less = lineRetractLess
946+
}
947+
sort.SliceStable(block.Line, func(i, j int) bool {
948+
return less(block.Line[i], block.Line[j])
798949
})
799950
}
800951
}
801952

953+
// removeDups removes duplicate exclude and replace directives.
954+
//
955+
// Earlier exclude directives take priority.
956+
//
957+
// Later replace directives take priority.
958+
//
959+
// require directives are not de-duplicated. That's left up to higher-level
960+
// logic (MVS).
961+
//
962+
// retract directives are not de-duplicated since comments are
963+
// meaningful, and versions may be retracted multiple times.
802964
func (f *File) removeDups() {
803-
have := make(map[module.Version]bool)
804965
kill := make(map[*Line]bool)
966+
967+
// Remove duplicate excludes.
968+
haveExclude := make(map[module.Version]bool)
805969
for _, x := range f.Exclude {
806-
if have[x.Mod] {
970+
if haveExclude[x.Mod] {
807971
kill[x.Syntax] = true
808972
continue
809973
}
810-
have[x.Mod] = true
974+
haveExclude[x.Mod] = true
811975
}
812976
var excl []*Exclude
813977
for _, x := range f.Exclude {
@@ -817,15 +981,16 @@ func (f *File) removeDups() {
817981
}
818982
f.Exclude = excl
819983

820-
have = make(map[module.Version]bool)
984+
// Remove duplicate replacements.
821985
// Later replacements take priority over earlier ones.
986+
haveReplace := make(map[module.Version]bool)
822987
for i := len(f.Replace) - 1; i >= 0; i-- {
823988
x := f.Replace[i]
824-
if have[x.Old] {
989+
if haveReplace[x.Old] {
825990
kill[x.Syntax] = true
826991
continue
827992
}
828-
have[x.Old] = true
993+
haveReplace[x.Old] = true
829994
}
830995
var repl []*Replace
831996
for _, x := range f.Replace {
@@ -835,6 +1000,9 @@ func (f *File) removeDups() {
8351000
}
8361001
f.Replace = repl
8371002

1003+
// Duplicate require and retract directives are not removed.
1004+
1005+
// Drop killed statements from the syntax tree.
8381006
var stmts []Expr
8391007
for _, stmt := range f.Syntax.Stmt {
8401008
switch stmt := stmt.(type) {
@@ -858,3 +1026,38 @@ func (f *File) removeDups() {
8581026
}
8591027
f.Syntax.Stmt = stmts
8601028
}
1029+
1030+
// lineLess returns whether li should be sorted before lj. It sorts
1031+
// lexicographically without assigning any special meaning to tokens.
1032+
func lineLess(li, lj *Line) bool {
1033+
for k := 0; k < len(li.Token) && k < len(lj.Token); k++ {
1034+
if li.Token[k] != lj.Token[k] {
1035+
return li.Token[k] < lj.Token[k]
1036+
}
1037+
}
1038+
return len(li.Token) < len(lj.Token)
1039+
}
1040+
1041+
// lineRetractLess returns whether li should be sorted before lj for lines in
1042+
// a "retract" block. It treats each line as a version interval. Single versions
1043+
// are compared as if they were intervals with the same low and high version.
1044+
// Intervals are sorted in descending order, first by low version, then by
1045+
// high version, using semver.Compare.
1046+
func lineRetractLess(li, lj *Line) bool {
1047+
interval := func(l *Line) VersionInterval {
1048+
if len(l.Token) == 1 {
1049+
return VersionInterval{Low: l.Token[0], High: l.Token[0]}
1050+
} else if len(l.Token) == 5 && l.Token[0] == "[" && l.Token[2] == "," && l.Token[4] == "]" {
1051+
return VersionInterval{Low: l.Token[1], High: l.Token[3]}
1052+
} else {
1053+
// Line in unknown format. Treat as an invalid version.
1054+
return VersionInterval{}
1055+
}
1056+
}
1057+
vii := interval(li)
1058+
vij := interval(lj)
1059+
if cmp := semver.Compare(vii.Low, vij.Low); cmp != 0 {
1060+
return cmp > 0
1061+
}
1062+
return semver.Compare(vii.High, vij.High) > 0
1063+
}

0 commit comments

Comments
 (0)