@@ -30,6 +30,7 @@ import (
30
30
31
31
"golang.org/x/mod/internal/lazyregexp"
32
32
"golang.org/x/mod/module"
33
+ "golang.org/x/mod/semver"
33
34
)
34
35
35
36
// A File is the parsed, interpreted form of a go.mod file.
@@ -39,6 +40,7 @@ type File struct {
39
40
Require []* Require
40
41
Exclude []* Exclude
41
42
Replace []* Replace
43
+ Retract []* Retract
42
44
43
45
Syntax * FileSyntax
44
46
}
@@ -75,6 +77,21 @@ type Replace struct {
75
77
Syntax * Line
76
78
}
77
79
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
+
78
95
func (f * File ) AddModuleStmt (path string ) error {
79
96
if f .Syntax == nil {
80
97
f .Syntax = new (FileSyntax )
@@ -138,7 +155,7 @@ func parseToFile(file string, data []byte, fix VersionFixer, strict bool) (*File
138
155
for _ , x := range fs .Stmt {
139
156
switch x := x .(type ) {
140
157
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 )
142
159
143
160
case * LineBlock :
144
161
if len (x .Token ) > 1 {
@@ -161,9 +178,9 @@ func parseToFile(file string, data []byte, fix VersionFixer, strict bool) (*File
161
178
})
162
179
}
163
180
continue
164
- case "module" , "require" , "exclude" , "replace" :
181
+ case "module" , "require" , "exclude" , "replace" , "retract" :
165
182
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 )
167
184
}
168
185
}
169
186
}
@@ -177,7 +194,7 @@ func parseToFile(file string, data []byte, fix VersionFixer, strict bool) (*File
177
194
178
195
var GoVersionRE = lazyregexp .New (`^([1-9][0-9]*)\.(0|[1-9][0-9]*)$` )
179
196
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 ) {
181
198
// If strict is false, this module is a dependency.
182
199
// We ignore all unknown directives as well as main-module-only
183
200
// 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
186
203
// and simply ignore those statements.
187
204
if ! strict {
188
205
switch verb {
189
- case "module" , "require " , "go " :
206
+ case "go" , " module" , "retract " , "require " :
190
207
// want these even for dependency go.mods
191
208
default :
192
209
return
@@ -232,6 +249,7 @@ func (f *File) add(errs *ErrorList, line *Line, verb string, args []string, fix
232
249
233
250
f .Go = & Go {Syntax : line }
234
251
f .Go .Version = args [0 ]
252
+
235
253
case "module" :
236
254
if f .Module != nil {
237
255
errorf ("repeated module statement" )
@@ -248,6 +266,7 @@ func (f *File) add(errs *ErrorList, line *Line, verb string, args []string, fix
248
266
return
249
267
}
250
268
f .Module .Mod = module.Version {Path : s }
269
+
251
270
case "require" , "exclude" :
252
271
if len (args ) != 2 {
253
272
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
284
303
Syntax : line ,
285
304
})
286
305
}
306
+
287
307
case "replace" :
288
308
arrow := 2
289
309
if len (args ) >= 2 && args [1 ] == "=>" {
@@ -347,6 +367,33 @@ func (f *File) add(errs *ErrorList, line *Line, verb string, args []string, fix
347
367
New : module.Version {Path : ns , Version : nv },
348
368
Syntax : line ,
349
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 )
350
397
}
351
398
}
352
399
@@ -444,6 +491,53 @@ func AutoQuote(s string) string {
444
491
return s
445
492
}
446
493
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
+
447
541
func parseString (s * string ) (string , error ) {
448
542
t := * s
449
543
if strings .HasPrefix (t , `"` ) {
@@ -461,6 +555,27 @@ func parseString(s *string) (string, error) {
461
555
return t , nil
462
556
}
463
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.
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
+
464
579
type ErrorList []Error
465
580
466
581
func (e ErrorList ) Error () string {
@@ -494,6 +609,8 @@ func (e *Error) Error() string {
494
609
var directive string
495
610
if e .ModPath != "" {
496
611
directive = fmt .Sprintf ("%s %s: " , e .Verb , e .ModPath )
612
+ } else if e .Verb != "" {
613
+ directive = fmt .Sprintf ("%s: " , e .Verb )
497
614
}
498
615
499
616
return pos + directive + e .Err .Error ()
@@ -585,6 +702,15 @@ func (f *File) Cleanup() {
585
702
}
586
703
f .Replace = f .Replace [:w ]
587
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
+
588
714
f .Syntax .Cleanup ()
589
715
}
590
716
@@ -778,6 +904,34 @@ func (f *File) DropReplace(oldPath, oldVers string) error {
778
904
return nil
779
905
}
780
906
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
+
781
935
func (f * File ) SortBlocks () {
782
936
f .removeDups () // otherwise sorting is unsafe
783
937
@@ -786,28 +940,38 @@ func (f *File) SortBlocks() {
786
940
if ! ok {
787
941
continue
788
942
}
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 ])
798
949
})
799
950
}
800
951
}
801
952
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.
802
964
func (f * File ) removeDups () {
803
- have := make (map [module.Version ]bool )
804
965
kill := make (map [* Line ]bool )
966
+
967
+ // Remove duplicate excludes.
968
+ haveExclude := make (map [module.Version ]bool )
805
969
for _ , x := range f .Exclude {
806
- if have [x .Mod ] {
970
+ if haveExclude [x .Mod ] {
807
971
kill [x .Syntax ] = true
808
972
continue
809
973
}
810
- have [x .Mod ] = true
974
+ haveExclude [x .Mod ] = true
811
975
}
812
976
var excl []* Exclude
813
977
for _ , x := range f .Exclude {
@@ -817,15 +981,16 @@ func (f *File) removeDups() {
817
981
}
818
982
f .Exclude = excl
819
983
820
- have = make ( map [module. Version ] bool )
984
+ // Remove duplicate replacements.
821
985
// Later replacements take priority over earlier ones.
986
+ haveReplace := make (map [module.Version ]bool )
822
987
for i := len (f .Replace ) - 1 ; i >= 0 ; i -- {
823
988
x := f .Replace [i ]
824
- if have [x .Old ] {
989
+ if haveReplace [x .Old ] {
825
990
kill [x .Syntax ] = true
826
991
continue
827
992
}
828
- have [x .Old ] = true
993
+ haveReplace [x .Old ] = true
829
994
}
830
995
var repl []* Replace
831
996
for _ , x := range f .Replace {
@@ -835,6 +1000,9 @@ func (f *File) removeDups() {
835
1000
}
836
1001
f .Replace = repl
837
1002
1003
+ // Duplicate require and retract directives are not removed.
1004
+
1005
+ // Drop killed statements from the syntax tree.
838
1006
var stmts []Expr
839
1007
for _ , stmt := range f .Syntax .Stmt {
840
1008
switch stmt := stmt .(type ) {
@@ -858,3 +1026,38 @@ func (f *File) removeDups() {
858
1026
}
859
1027
f .Syntax .Stmt = stmts
860
1028
}
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