1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package modfile
21
22 import (
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
37 type File struct {
38 Module *Module
39 Go *Go
40 Toolchain *Toolchain
41 Godebug []*Godebug
42 Require []*Require
43 Exclude []*Exclude
44 Replace []*Replace
45 Retract []*Retract
46 Tool []*Tool
47
48 Syntax *FileSyntax
49 }
50
51
52 type Module struct {
53 Mod module.Version
54 Deprecated string
55 Syntax *Line
56 }
57
58
59 type Go struct {
60 Version string
61 Syntax *Line
62 }
63
64
65 type Toolchain struct {
66 Name string
67 Syntax *Line
68 }
69
70
71 type Godebug struct {
72 Key string
73 Value string
74 Syntax *Line
75 }
76
77
78 type Exclude struct {
79 Mod module.Version
80 Syntax *Line
81 }
82
83
84 type Replace struct {
85 Old module.Version
86 New module.Version
87 Syntax *Line
88 }
89
90
91 type Retract struct {
92 VersionInterval
93 Rationale string
94 Syntax *Line
95 }
96
97
98 type Tool struct {
99 Path string
100 Syntax *Line
101 }
102
103
104
105
106
107 type VersionInterval struct {
108 Low, High string
109 }
110
111
112 type Require struct {
113 Mod module.Version
114 Indirect bool
115 Syntax *Line
116 }
117
118 func (r *Require) markRemoved() {
119 r.Syntax.markRemoved()
120 *r = Require{}
121 }
122
123 func (r *Require) setVersion(v string) {
124 r.Mod.Version = v
125
126 if line := r.Syntax; len(line.Token) > 0 {
127 if line.InBlock {
128
129
130 if len(line.Comments.Before) == 1 && len(line.Comments.Before[0].Token) == 0 {
131 line.Comments.Before = line.Comments.Before[:0]
132 }
133 if len(line.Token) >= 2 {
134 line.Token[1] = v
135 }
136 } else {
137 if len(line.Token) >= 3 {
138 line.Token[2] = v
139 }
140 }
141 }
142 }
143
144
145 func (r *Require) setIndirect(indirect bool) {
146 r.Indirect = indirect
147 line := r.Syntax
148 if isIndirect(line) == indirect {
149 return
150 }
151 if indirect {
152
153 if len(line.Suffix) == 0 {
154
155 line.Suffix = []Comment{{Token: "// indirect", Suffix: true}}
156 return
157 }
158
159 com := &line.Suffix[0]
160 text := strings.TrimSpace(strings.TrimPrefix(com.Token, string(slashSlash)))
161 if text == "" {
162
163 com.Token = "// indirect"
164 return
165 }
166
167
168 com.Token = "// indirect; " + text
169 return
170 }
171
172
173 f := strings.TrimSpace(strings.TrimPrefix(line.Suffix[0].Token, string(slashSlash)))
174 if f == "indirect" {
175
176 line.Suffix = nil
177 return
178 }
179
180
181 com := &line.Suffix[0]
182 i := strings.Index(com.Token, "indirect;")
183 com.Token = "//" + com.Token[i+len("indirect;"):]
184 }
185
186
187
188
189
190 func isIndirect(line *Line) bool {
191 if len(line.Suffix) == 0 {
192 return false
193 }
194 f := strings.Fields(strings.TrimPrefix(line.Suffix[0].Token, string(slashSlash)))
195 return (len(f) == 1 && f[0] == "indirect" || len(f) > 1 && f[0] == "indirect;")
196 }
197
198 func (f *File) AddModuleStmt(path string) error {
199 if f.Syntax == nil {
200 f.Syntax = new(FileSyntax)
201 }
202 if f.Module == nil {
203 f.Module = &Module{
204 Mod: module.Version{Path: path},
205 Syntax: f.Syntax.addLine(nil, "module", AutoQuote(path)),
206 }
207 } else {
208 f.Module.Mod.Path = path
209 f.Syntax.updateLine(f.Module.Syntax, "module", AutoQuote(path))
210 }
211 return nil
212 }
213
214 func (f *File) AddComment(text string) {
215 if f.Syntax == nil {
216 f.Syntax = new(FileSyntax)
217 }
218 f.Syntax.Stmt = append(f.Syntax.Stmt, &CommentBlock{
219 Comments: Comments{
220 Before: []Comment{
221 {
222 Token: text,
223 },
224 },
225 },
226 })
227 }
228
229 type VersionFixer func(path, version string) (string, error)
230
231
232
233 var dontFixRetract VersionFixer = func(_, vers string) (string, error) {
234 return vers, nil
235 }
236
237
238
239
240
241
242
243
244
245
246 func Parse(file string, data []byte, fix VersionFixer) (*File, error) {
247 return parseToFile(file, data, fix, true)
248 }
249
250
251
252
253
254
255
256
257 func ParseLax(file string, data []byte, fix VersionFixer) (*File, error) {
258 return parseToFile(file, data, fix, false)
259 }
260
261 func parseToFile(file string, data []byte, fix VersionFixer, strict bool) (parsed *File, err error) {
262 fs, err := parse(file, data)
263 if err != nil {
264 return nil, err
265 }
266 f := &File{
267 Syntax: fs,
268 }
269 var errs ErrorList
270
271
272
273 defer func() {
274 oldLen := len(errs)
275 f.fixRetract(fix, &errs)
276 if len(errs) > oldLen {
277 parsed, err = nil, errs
278 }
279 }()
280
281 for _, x := range fs.Stmt {
282 switch x := x.(type) {
283 case *Line:
284 f.add(&errs, nil, x, x.Token[0], x.Token[1:], fix, strict)
285
286 case *LineBlock:
287 if len(x.Token) > 1 {
288 if strict {
289 errs = append(errs, Error{
290 Filename: file,
291 Pos: x.Start,
292 Err: fmt.Errorf("unknown block type: %s", strings.Join(x.Token, " ")),
293 })
294 }
295 continue
296 }
297 switch x.Token[0] {
298 default:
299 if strict {
300 errs = append(errs, Error{
301 Filename: file,
302 Pos: x.Start,
303 Err: fmt.Errorf("unknown block type: %s", strings.Join(x.Token, " ")),
304 })
305 }
306 continue
307 case "module", "godebug", "require", "exclude", "replace", "retract", "tool":
308 for _, l := range x.Line {
309 f.add(&errs, x, l, x.Token[0], l.Token, fix, strict)
310 }
311 }
312 }
313 }
314
315 if len(errs) > 0 {
316 return nil, errs
317 }
318 return f, nil
319 }
320
321 var GoVersionRE = lazyregexp.New(`^([1-9][0-9]*)\.(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))?([a-z]+[0-9]+)?$`)
322 var laxGoVersionRE = lazyregexp.New(`^v?(([1-9][0-9]*)\.(0|[1-9][0-9]*))([^0-9].*)$`)
323
324
325
326
327
328
329 var ToolchainRE = lazyregexp.New(`^default$|^go1($|\.)`)
330
331 func (f *File) add(errs *ErrorList, block *LineBlock, line *Line, verb string, args []string, fix VersionFixer, strict bool) {
332
333
334
335
336
337
338 if !strict {
339 switch verb {
340 case "go", "module", "retract", "require":
341
342 default:
343 return
344 }
345 }
346
347 wrapModPathError := func(modPath string, err error) {
348 *errs = append(*errs, Error{
349 Filename: f.Syntax.Name,
350 Pos: line.Start,
351 ModPath: modPath,
352 Verb: verb,
353 Err: err,
354 })
355 }
356 wrapError := func(err error) {
357 *errs = append(*errs, Error{
358 Filename: f.Syntax.Name,
359 Pos: line.Start,
360 Err: err,
361 })
362 }
363 errorf := func(format string, args ...interface{}) {
364 wrapError(fmt.Errorf(format, args...))
365 }
366
367 switch verb {
368 default:
369 errorf("unknown directive: %s", verb)
370
371 case "go":
372 if f.Go != nil {
373 errorf("repeated go statement")
374 return
375 }
376 if len(args) != 1 {
377 errorf("go directive expects exactly one argument")
378 return
379 } else if !GoVersionRE.MatchString(args[0]) {
380 fixed := false
381 if !strict {
382 if m := laxGoVersionRE.FindStringSubmatch(args[0]); m != nil {
383 args[0] = m[1]
384 fixed = true
385 }
386 }
387 if !fixed {
388 errorf("invalid go version '%s': must match format 1.23.0", args[0])
389 return
390 }
391 }
392
393 f.Go = &Go{Syntax: line}
394 f.Go.Version = args[0]
395
396 case "toolchain":
397 if f.Toolchain != nil {
398 errorf("repeated toolchain statement")
399 return
400 }
401 if len(args) != 1 {
402 errorf("toolchain directive expects exactly one argument")
403 return
404 } else if !ToolchainRE.MatchString(args[0]) {
405 errorf("invalid toolchain version '%s': must match format go1.23.0 or default", args[0])
406 return
407 }
408 f.Toolchain = &Toolchain{Syntax: line}
409 f.Toolchain.Name = args[0]
410
411 case "module":
412 if f.Module != nil {
413 errorf("repeated module statement")
414 return
415 }
416 deprecated := parseDeprecation(block, line)
417 f.Module = &Module{
418 Syntax: line,
419 Deprecated: deprecated,
420 }
421 if len(args) != 1 {
422 errorf("usage: module module/path")
423 return
424 }
425 s, err := parseString(&args[0])
426 if err != nil {
427 errorf("invalid quoted string: %v", err)
428 return
429 }
430 f.Module.Mod = module.Version{Path: s}
431
432 case "godebug":
433 if len(args) != 1 || strings.ContainsAny(args[0], "\"`',") {
434 errorf("usage: godebug key=value")
435 return
436 }
437 key, value, ok := strings.Cut(args[0], "=")
438 if !ok {
439 errorf("usage: godebug key=value")
440 return
441 }
442 f.Godebug = append(f.Godebug, &Godebug{
443 Key: key,
444 Value: value,
445 Syntax: line,
446 })
447
448 case "require", "exclude":
449 if len(args) != 2 {
450 errorf("usage: %s module/path v1.2.3", verb)
451 return
452 }
453 s, err := parseString(&args[0])
454 if err != nil {
455 errorf("invalid quoted string: %v", err)
456 return
457 }
458 v, err := parseVersion(verb, s, &args[1], fix)
459 if err != nil {
460 wrapError(err)
461 return
462 }
463 pathMajor, err := modulePathMajor(s)
464 if err != nil {
465 wrapError(err)
466 return
467 }
468 if err := module.CheckPathMajor(v, pathMajor); err != nil {
469 wrapModPathError(s, err)
470 return
471 }
472 if verb == "require" {
473 f.Require = append(f.Require, &Require{
474 Mod: module.Version{Path: s, Version: v},
475 Syntax: line,
476 Indirect: isIndirect(line),
477 })
478 } else {
479 f.Exclude = append(f.Exclude, &Exclude{
480 Mod: module.Version{Path: s, Version: v},
481 Syntax: line,
482 })
483 }
484
485 case "replace":
486 replace, wrappederr := parseReplace(f.Syntax.Name, line, verb, args, fix)
487 if wrappederr != nil {
488 *errs = append(*errs, *wrappederr)
489 return
490 }
491 f.Replace = append(f.Replace, replace)
492
493 case "retract":
494 rationale := parseDirectiveComment(block, line)
495 vi, err := parseVersionInterval(verb, "", &args, dontFixRetract)
496 if err != nil {
497 if strict {
498 wrapError(err)
499 return
500 } else {
501
502
503
504
505 return
506 }
507 }
508 if len(args) > 0 && strict {
509
510 errorf("unexpected token after version: %q", args[0])
511 return
512 }
513 retract := &Retract{
514 VersionInterval: vi,
515 Rationale: rationale,
516 Syntax: line,
517 }
518 f.Retract = append(f.Retract, retract)
519
520 case "tool":
521 if len(args) != 1 {
522 errorf("tool directive expects exactly one argument")
523 return
524 }
525 s, err := parseString(&args[0])
526 if err != nil {
527 errorf("invalid quoted string: %v", err)
528 return
529 }
530 f.Tool = append(f.Tool, &Tool{
531 Path: s,
532 Syntax: line,
533 })
534 }
535 }
536
537 func parseReplace(filename string, line *Line, verb string, args []string, fix VersionFixer) (*Replace, *Error) {
538 wrapModPathError := func(modPath string, err error) *Error {
539 return &Error{
540 Filename: filename,
541 Pos: line.Start,
542 ModPath: modPath,
543 Verb: verb,
544 Err: err,
545 }
546 }
547 wrapError := func(err error) *Error {
548 return &Error{
549 Filename: filename,
550 Pos: line.Start,
551 Err: err,
552 }
553 }
554 errorf := func(format string, args ...interface{}) *Error {
555 return wrapError(fmt.Errorf(format, args...))
556 }
557
558 arrow := 2
559 if len(args) >= 2 && args[1] == "=>" {
560 arrow = 1
561 }
562 if len(args) < arrow+2 || len(args) > arrow+3 || args[arrow] != "=>" {
563 return nil, 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)
564 }
565 s, err := parseString(&args[0])
566 if err != nil {
567 return nil, errorf("invalid quoted string: %v", err)
568 }
569 pathMajor, err := modulePathMajor(s)
570 if err != nil {
571 return nil, wrapModPathError(s, err)
572
573 }
574 var v string
575 if arrow == 2 {
576 v, err = parseVersion(verb, s, &args[1], fix)
577 if err != nil {
578 return nil, wrapError(err)
579 }
580 if err := module.CheckPathMajor(v, pathMajor); err != nil {
581 return nil, wrapModPathError(s, err)
582 }
583 }
584 ns, err := parseString(&args[arrow+1])
585 if err != nil {
586 return nil, errorf("invalid quoted string: %v", err)
587 }
588 nv := ""
589 if len(args) == arrow+2 {
590 if !IsDirectoryPath(ns) {
591 if strings.Contains(ns, "@") {
592 return nil, errorf("replacement module must match format 'path version', not 'path@version'")
593 }
594 return nil, errorf("replacement module without version must be directory path (rooted or starting with . or ..)")
595 }
596 if filepath.Separator == '/' && strings.Contains(ns, `\`) {
597 return nil, errorf("replacement directory appears to be Windows path (on a non-windows system)")
598 }
599 }
600 if len(args) == arrow+3 {
601 nv, err = parseVersion(verb, ns, &args[arrow+2], fix)
602 if err != nil {
603 return nil, wrapError(err)
604 }
605 if IsDirectoryPath(ns) {
606 return nil, errorf("replacement module directory path %q cannot have version", ns)
607 }
608 }
609 return &Replace{
610 Old: module.Version{Path: s, Version: v},
611 New: module.Version{Path: ns, Version: nv},
612 Syntax: line,
613 }, nil
614 }
615
616
617
618
619
620
621
622 func (f *File) fixRetract(fix VersionFixer, errs *ErrorList) {
623 if fix == nil {
624 return
625 }
626 path := ""
627 if f.Module != nil {
628 path = f.Module.Mod.Path
629 }
630 var r *Retract
631 wrapError := func(err error) {
632 *errs = append(*errs, Error{
633 Filename: f.Syntax.Name,
634 Pos: r.Syntax.Start,
635 Err: err,
636 })
637 }
638
639 for _, r = range f.Retract {
640 if path == "" {
641 wrapError(errors.New("no module directive found, so retract cannot be used"))
642 return
643 }
644
645 args := r.Syntax.Token
646 if args[0] == "retract" {
647 args = args[1:]
648 }
649 vi, err := parseVersionInterval("retract", path, &args, fix)
650 if err != nil {
651 wrapError(err)
652 }
653 r.VersionInterval = vi
654 }
655 }
656
657 func (f *WorkFile) add(errs *ErrorList, line *Line, verb string, args []string, fix VersionFixer) {
658 wrapError := func(err error) {
659 *errs = append(*errs, Error{
660 Filename: f.Syntax.Name,
661 Pos: line.Start,
662 Err: err,
663 })
664 }
665 errorf := func(format string, args ...interface{}) {
666 wrapError(fmt.Errorf(format, args...))
667 }
668
669 switch verb {
670 default:
671 errorf("unknown directive: %s", verb)
672
673 case "go":
674 if f.Go != nil {
675 errorf("repeated go statement")
676 return
677 }
678 if len(args) != 1 {
679 errorf("go directive expects exactly one argument")
680 return
681 } else if !GoVersionRE.MatchString(args[0]) {
682 errorf("invalid go version '%s': must match format 1.23.0", args[0])
683 return
684 }
685
686 f.Go = &Go{Syntax: line}
687 f.Go.Version = args[0]
688
689 case "toolchain":
690 if f.Toolchain != nil {
691 errorf("repeated toolchain statement")
692 return
693 }
694 if len(args) != 1 {
695 errorf("toolchain directive expects exactly one argument")
696 return
697 } else if !ToolchainRE.MatchString(args[0]) {
698 errorf("invalid toolchain version '%s': must match format go1.23.0 or default", args[0])
699 return
700 }
701
702 f.Toolchain = &Toolchain{Syntax: line}
703 f.Toolchain.Name = args[0]
704
705 case "godebug":
706 if len(args) != 1 || strings.ContainsAny(args[0], "\"`',") {
707 errorf("usage: godebug key=value")
708 return
709 }
710 key, value, ok := strings.Cut(args[0], "=")
711 if !ok {
712 errorf("usage: godebug key=value")
713 return
714 }
715 f.Godebug = append(f.Godebug, &Godebug{
716 Key: key,
717 Value: value,
718 Syntax: line,
719 })
720
721 case "use":
722 if len(args) != 1 {
723 errorf("usage: %s local/dir", verb)
724 return
725 }
726 s, err := parseString(&args[0])
727 if err != nil {
728 errorf("invalid quoted string: %v", err)
729 return
730 }
731 f.Use = append(f.Use, &Use{
732 Path: s,
733 Syntax: line,
734 })
735
736 case "replace":
737 replace, wrappederr := parseReplace(f.Syntax.Name, line, verb, args, fix)
738 if wrappederr != nil {
739 *errs = append(*errs, *wrappederr)
740 return
741 }
742 f.Replace = append(f.Replace, replace)
743 }
744 }
745
746
747
748
749 func IsDirectoryPath(ns string) bool {
750
751
752 return ns == "." || strings.HasPrefix(ns, "./") || strings.HasPrefix(ns, `.\`) ||
753 ns == ".." || strings.HasPrefix(ns, "../") || strings.HasPrefix(ns, `..\`) ||
754 strings.HasPrefix(ns, "/") || strings.HasPrefix(ns, `\`) ||
755 len(ns) >= 2 && ('A' <= ns[0] && ns[0] <= 'Z' || 'a' <= ns[0] && ns[0] <= 'z') && ns[1] == ':'
756 }
757
758
759
760 func MustQuote(s string) bool {
761 for _, r := range s {
762 switch r {
763 case ' ', '"', '\'', '`':
764 return true
765
766 case '(', ')', '[', ']', '{', '}', ',':
767 if len(s) > 1 {
768 return true
769 }
770
771 default:
772 if !unicode.IsPrint(r) {
773 return true
774 }
775 }
776 }
777 return s == "" || strings.Contains(s, "//") || strings.Contains(s, "/*")
778 }
779
780
781
782 func AutoQuote(s string) string {
783 if MustQuote(s) {
784 return strconv.Quote(s)
785 }
786 return s
787 }
788
789 func parseVersionInterval(verb string, path string, args *[]string, fix VersionFixer) (VersionInterval, error) {
790 toks := *args
791 if len(toks) == 0 || toks[0] == "(" {
792 return VersionInterval{}, fmt.Errorf("expected '[' or version")
793 }
794 if toks[0] != "[" {
795 v, err := parseVersion(verb, path, &toks[0], fix)
796 if err != nil {
797 return VersionInterval{}, err
798 }
799 *args = toks[1:]
800 return VersionInterval{Low: v, High: v}, nil
801 }
802 toks = toks[1:]
803
804 if len(toks) == 0 {
805 return VersionInterval{}, fmt.Errorf("expected version after '['")
806 }
807 low, err := parseVersion(verb, path, &toks[0], fix)
808 if err != nil {
809 return VersionInterval{}, err
810 }
811 toks = toks[1:]
812
813 if len(toks) == 0 || toks[0] != "," {
814 return VersionInterval{}, fmt.Errorf("expected ',' after version")
815 }
816 toks = toks[1:]
817
818 if len(toks) == 0 {
819 return VersionInterval{}, fmt.Errorf("expected version after ','")
820 }
821 high, err := parseVersion(verb, path, &toks[0], fix)
822 if err != nil {
823 return VersionInterval{}, err
824 }
825 toks = toks[1:]
826
827 if len(toks) == 0 || toks[0] != "]" {
828 return VersionInterval{}, fmt.Errorf("expected ']' after version")
829 }
830 toks = toks[1:]
831
832 *args = toks
833 return VersionInterval{Low: low, High: high}, nil
834 }
835
836 func parseString(s *string) (string, error) {
837 t := *s
838 if strings.HasPrefix(t, `"`) {
839 var err error
840 if t, err = strconv.Unquote(t); err != nil {
841 return "", err
842 }
843 } else if strings.ContainsAny(t, "\"'`") {
844
845
846
847 return "", fmt.Errorf("unquoted string cannot contain quote")
848 }
849 *s = AutoQuote(t)
850 return t, nil
851 }
852
853 var deprecatedRE = lazyregexp.New(`(?s)(?:^|\n\n)Deprecated: *(.*?)(?:$|\n\n)`)
854
855
856
857
858
859
860
861
862
863 func parseDeprecation(block *LineBlock, line *Line) string {
864 text := parseDirectiveComment(block, line)
865 m := deprecatedRE.FindStringSubmatch(text)
866 if m == nil {
867 return ""
868 }
869 return m[1]
870 }
871
872
873
874
875 func parseDirectiveComment(block *LineBlock, line *Line) string {
876 comments := line.Comment()
877 if block != nil && len(comments.Before) == 0 && len(comments.Suffix) == 0 {
878 comments = block.Comment()
879 }
880 groups := [][]Comment{comments.Before, comments.Suffix}
881 var lines []string
882 for _, g := range groups {
883 for _, c := range g {
884 if !strings.HasPrefix(c.Token, "//") {
885 continue
886 }
887 lines = append(lines, strings.TrimSpace(strings.TrimPrefix(c.Token, "//")))
888 }
889 }
890 return strings.Join(lines, "\n")
891 }
892
893 type ErrorList []Error
894
895 func (e ErrorList) Error() string {
896 errStrs := make([]string, len(e))
897 for i, err := range e {
898 errStrs[i] = err.Error()
899 }
900 return strings.Join(errStrs, "\n")
901 }
902
903 type Error struct {
904 Filename string
905 Pos Position
906 Verb string
907 ModPath string
908 Err error
909 }
910
911 func (e *Error) Error() string {
912 var pos string
913 if e.Pos.LineRune > 1 {
914
915
916 pos = fmt.Sprintf("%s:%d:%d: ", e.Filename, e.Pos.Line, e.Pos.LineRune)
917 } else if e.Pos.Line > 0 {
918 pos = fmt.Sprintf("%s:%d: ", e.Filename, e.Pos.Line)
919 } else if e.Filename != "" {
920 pos = fmt.Sprintf("%s: ", e.Filename)
921 }
922
923 var directive string
924 if e.ModPath != "" {
925 directive = fmt.Sprintf("%s %s: ", e.Verb, e.ModPath)
926 } else if e.Verb != "" {
927 directive = fmt.Sprintf("%s: ", e.Verb)
928 }
929
930 return pos + directive + e.Err.Error()
931 }
932
933 func (e *Error) Unwrap() error { return e.Err }
934
935 func parseVersion(verb string, path string, s *string, fix VersionFixer) (string, error) {
936 t, err := parseString(s)
937 if err != nil {
938 return "", &Error{
939 Verb: verb,
940 ModPath: path,
941 Err: &module.InvalidVersionError{
942 Version: *s,
943 Err: err,
944 },
945 }
946 }
947 if fix != nil {
948 fixed, err := fix(path, t)
949 if err != nil {
950 if err, ok := err.(*module.ModuleError); ok {
951 return "", &Error{
952 Verb: verb,
953 ModPath: path,
954 Err: err.Err,
955 }
956 }
957 return "", err
958 }
959 t = fixed
960 } else {
961 cv := module.CanonicalVersion(t)
962 if cv == "" {
963 return "", &Error{
964 Verb: verb,
965 ModPath: path,
966 Err: &module.InvalidVersionError{
967 Version: t,
968 Err: errors.New("must be of the form v1.2.3"),
969 },
970 }
971 }
972 t = cv
973 }
974 *s = t
975 return *s, nil
976 }
977
978 func modulePathMajor(path string) (string, error) {
979 _, major, ok := module.SplitPathVersion(path)
980 if !ok {
981 return "", fmt.Errorf("invalid module path")
982 }
983 return major, nil
984 }
985
986 func (f *File) Format() ([]byte, error) {
987 return Format(f.Syntax), nil
988 }
989
990
991
992
993
994 func (f *File) Cleanup() {
995 w := 0
996 for _, g := range f.Godebug {
997 if g.Key != "" {
998 f.Godebug[w] = g
999 w++
1000 }
1001 }
1002 f.Godebug = f.Godebug[:w]
1003
1004 w = 0
1005 for _, r := range f.Require {
1006 if r.Mod.Path != "" {
1007 f.Require[w] = r
1008 w++
1009 }
1010 }
1011 f.Require = f.Require[:w]
1012
1013 w = 0
1014 for _, x := range f.Exclude {
1015 if x.Mod.Path != "" {
1016 f.Exclude[w] = x
1017 w++
1018 }
1019 }
1020 f.Exclude = f.Exclude[:w]
1021
1022 w = 0
1023 for _, r := range f.Replace {
1024 if r.Old.Path != "" {
1025 f.Replace[w] = r
1026 w++
1027 }
1028 }
1029 f.Replace = f.Replace[:w]
1030
1031 w = 0
1032 for _, r := range f.Retract {
1033 if r.Low != "" || r.High != "" {
1034 f.Retract[w] = r
1035 w++
1036 }
1037 }
1038 f.Retract = f.Retract[:w]
1039
1040 f.Syntax.Cleanup()
1041 }
1042
1043 func (f *File) AddGoStmt(version string) error {
1044 if !GoVersionRE.MatchString(version) {
1045 return fmt.Errorf("invalid language version %q", version)
1046 }
1047 if f.Go == nil {
1048 var hint Expr
1049 if f.Module != nil && f.Module.Syntax != nil {
1050 hint = f.Module.Syntax
1051 } else if f.Syntax == nil {
1052 f.Syntax = new(FileSyntax)
1053 }
1054 f.Go = &Go{
1055 Version: version,
1056 Syntax: f.Syntax.addLine(hint, "go", version),
1057 }
1058 } else {
1059 f.Go.Version = version
1060 f.Syntax.updateLine(f.Go.Syntax, "go", version)
1061 }
1062 return nil
1063 }
1064
1065
1066 func (f *File) DropGoStmt() {
1067 if f.Go != nil {
1068 f.Go.Syntax.markRemoved()
1069 f.Go = nil
1070 }
1071 }
1072
1073
1074 func (f *File) DropToolchainStmt() {
1075 if f.Toolchain != nil {
1076 f.Toolchain.Syntax.markRemoved()
1077 f.Toolchain = nil
1078 }
1079 }
1080
1081 func (f *File) AddToolchainStmt(name string) error {
1082 if !ToolchainRE.MatchString(name) {
1083 return fmt.Errorf("invalid toolchain name %q", name)
1084 }
1085 if f.Toolchain == nil {
1086 var hint Expr
1087 if f.Go != nil && f.Go.Syntax != nil {
1088 hint = f.Go.Syntax
1089 } else if f.Module != nil && f.Module.Syntax != nil {
1090 hint = f.Module.Syntax
1091 }
1092 f.Toolchain = &Toolchain{
1093 Name: name,
1094 Syntax: f.Syntax.addLine(hint, "toolchain", name),
1095 }
1096 } else {
1097 f.Toolchain.Name = name
1098 f.Syntax.updateLine(f.Toolchain.Syntax, "toolchain", name)
1099 }
1100 return nil
1101 }
1102
1103
1104
1105
1106
1107
1108
1109 func (f *File) AddGodebug(key, value string) error {
1110 need := true
1111 for _, g := range f.Godebug {
1112 if g.Key == key {
1113 if need {
1114 g.Value = value
1115 f.Syntax.updateLine(g.Syntax, "godebug", key+"="+value)
1116 need = false
1117 } else {
1118 g.Syntax.markRemoved()
1119 *g = Godebug{}
1120 }
1121 }
1122 }
1123
1124 if need {
1125 f.addNewGodebug(key, value)
1126 }
1127 return nil
1128 }
1129
1130
1131
1132 func (f *File) addNewGodebug(key, value string) {
1133 line := f.Syntax.addLine(nil, "godebug", key+"="+value)
1134 g := &Godebug{
1135 Key: key,
1136 Value: value,
1137 Syntax: line,
1138 }
1139 f.Godebug = append(f.Godebug, g)
1140 }
1141
1142
1143
1144
1145
1146
1147
1148 func (f *File) AddRequire(path, vers string) error {
1149 need := true
1150 for _, r := range f.Require {
1151 if r.Mod.Path == path {
1152 if need {
1153 r.Mod.Version = vers
1154 f.Syntax.updateLine(r.Syntax, "require", AutoQuote(path), vers)
1155 need = false
1156 } else {
1157 r.Syntax.markRemoved()
1158 *r = Require{}
1159 }
1160 }
1161 }
1162
1163 if need {
1164 f.AddNewRequire(path, vers, false)
1165 }
1166 return nil
1167 }
1168
1169
1170
1171 func (f *File) AddNewRequire(path, vers string, indirect bool) {
1172 line := f.Syntax.addLine(nil, "require", AutoQuote(path), vers)
1173 r := &Require{
1174 Mod: module.Version{Path: path, Version: vers},
1175 Syntax: line,
1176 }
1177 r.setIndirect(indirect)
1178 f.Require = append(f.Require, r)
1179 }
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195 func (f *File) SetRequire(req []*Require) {
1196 type elem struct {
1197 version string
1198 indirect bool
1199 }
1200 need := make(map[string]elem)
1201 for _, r := range req {
1202 if prev, dup := need[r.Mod.Path]; dup && prev.version != r.Mod.Version {
1203 panic(fmt.Errorf("SetRequire called with conflicting versions for path %s (%s and %s)", r.Mod.Path, prev.version, r.Mod.Version))
1204 }
1205 need[r.Mod.Path] = elem{r.Mod.Version, r.Indirect}
1206 }
1207
1208
1209
1210 for _, r := range f.Require {
1211 e, ok := need[r.Mod.Path]
1212 if ok {
1213 r.setVersion(e.version)
1214 r.setIndirect(e.indirect)
1215 } else {
1216 r.markRemoved()
1217 }
1218 delete(need, r.Mod.Path)
1219 }
1220
1221
1222
1223
1224
1225
1226 for path, e := range need {
1227 f.AddNewRequire(path, e.version, e.indirect)
1228 }
1229
1230 f.SortBlocks()
1231 }
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251 func (f *File) SetRequireSeparateIndirect(req []*Require) {
1252
1253
1254 hasComments := func(c Comments) bool {
1255 return len(c.Before) > 0 || len(c.After) > 0 || len(c.Suffix) > 1 ||
1256 (len(c.Suffix) == 1 &&
1257 strings.TrimSpace(strings.TrimPrefix(c.Suffix[0].Token, string(slashSlash))) != "indirect")
1258 }
1259
1260
1261
1262 moveReq := func(r *Require, block *LineBlock) {
1263 var line *Line
1264 if r.Syntax == nil {
1265 line = &Line{Token: []string{AutoQuote(r.Mod.Path), r.Mod.Version}}
1266 r.Syntax = line
1267 if r.Indirect {
1268 r.setIndirect(true)
1269 }
1270 } else {
1271 line = new(Line)
1272 *line = *r.Syntax
1273 if !line.InBlock && len(line.Token) > 0 && line.Token[0] == "require" {
1274 line.Token = line.Token[1:]
1275 }
1276 r.Syntax.Token = nil
1277 r.Syntax = line
1278 }
1279 line.InBlock = true
1280 block.Line = append(block.Line, line)
1281 }
1282
1283
1284 var (
1285
1286
1287
1288 lastDirectIndex = -1
1289 lastIndirectIndex = -1
1290
1291
1292
1293 lastRequireIndex = -1
1294
1295
1296
1297 requireLineOrBlockCount = 0
1298
1299
1300
1301 lineToBlock = make(map[*Line]*LineBlock)
1302 )
1303 for i, stmt := range f.Syntax.Stmt {
1304 switch stmt := stmt.(type) {
1305 case *Line:
1306 if len(stmt.Token) == 0 || stmt.Token[0] != "require" {
1307 continue
1308 }
1309 lastRequireIndex = i
1310 requireLineOrBlockCount++
1311 if !hasComments(stmt.Comments) {
1312 if isIndirect(stmt) {
1313 lastIndirectIndex = i
1314 } else {
1315 lastDirectIndex = i
1316 }
1317 }
1318
1319 case *LineBlock:
1320 if len(stmt.Token) == 0 || stmt.Token[0] != "require" {
1321 continue
1322 }
1323 lastRequireIndex = i
1324 requireLineOrBlockCount++
1325 allDirect := len(stmt.Line) > 0 && !hasComments(stmt.Comments)
1326 allIndirect := len(stmt.Line) > 0 && !hasComments(stmt.Comments)
1327 for _, line := range stmt.Line {
1328 lineToBlock[line] = stmt
1329 if hasComments(line.Comments) {
1330 allDirect = false
1331 allIndirect = false
1332 } else if isIndirect(line) {
1333 allDirect = false
1334 } else {
1335 allIndirect = false
1336 }
1337 }
1338 if allDirect {
1339 lastDirectIndex = i
1340 }
1341 if allIndirect {
1342 lastIndirectIndex = i
1343 }
1344 }
1345 }
1346
1347 oneFlatUncommentedBlock := requireLineOrBlockCount == 1 &&
1348 !hasComments(*f.Syntax.Stmt[lastRequireIndex].Comment())
1349
1350
1351
1352
1353 insertBlock := func(i int) *LineBlock {
1354 block := &LineBlock{Token: []string{"require"}}
1355 f.Syntax.Stmt = append(f.Syntax.Stmt, nil)
1356 copy(f.Syntax.Stmt[i+1:], f.Syntax.Stmt[i:])
1357 f.Syntax.Stmt[i] = block
1358 return block
1359 }
1360
1361 ensureBlock := func(i int) *LineBlock {
1362 switch stmt := f.Syntax.Stmt[i].(type) {
1363 case *LineBlock:
1364 return stmt
1365 case *Line:
1366 block := &LineBlock{
1367 Token: []string{"require"},
1368 Line: []*Line{stmt},
1369 }
1370 stmt.Token = stmt.Token[1:]
1371 stmt.InBlock = true
1372 f.Syntax.Stmt[i] = block
1373 return block
1374 default:
1375 panic(fmt.Sprintf("unexpected statement: %v", stmt))
1376 }
1377 }
1378
1379 var lastDirectBlock *LineBlock
1380 if lastDirectIndex < 0 {
1381 if lastIndirectIndex >= 0 {
1382 lastDirectIndex = lastIndirectIndex
1383 lastIndirectIndex++
1384 } else if lastRequireIndex >= 0 {
1385 lastDirectIndex = lastRequireIndex + 1
1386 } else {
1387 lastDirectIndex = len(f.Syntax.Stmt)
1388 }
1389 lastDirectBlock = insertBlock(lastDirectIndex)
1390 } else {
1391 lastDirectBlock = ensureBlock(lastDirectIndex)
1392 }
1393
1394 var lastIndirectBlock *LineBlock
1395 if lastIndirectIndex < 0 {
1396 lastIndirectIndex = lastDirectIndex + 1
1397 lastIndirectBlock = insertBlock(lastIndirectIndex)
1398 } else {
1399 lastIndirectBlock = ensureBlock(lastIndirectIndex)
1400 }
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410 need := make(map[string]*Require)
1411 for _, r := range req {
1412 need[r.Mod.Path] = r
1413 }
1414 have := make(map[string]*Require)
1415 for _, r := range f.Require {
1416 path := r.Mod.Path
1417 if need[path] == nil || have[path] != nil {
1418
1419 r.markRemoved()
1420 continue
1421 }
1422 have[r.Mod.Path] = r
1423 r.setVersion(need[path].Mod.Version)
1424 r.setIndirect(need[path].Indirect)
1425 if need[path].Indirect &&
1426 (oneFlatUncommentedBlock || lineToBlock[r.Syntax] == lastDirectBlock) {
1427 moveReq(r, lastIndirectBlock)
1428 } else if !need[path].Indirect &&
1429 (oneFlatUncommentedBlock || lineToBlock[r.Syntax] == lastIndirectBlock) {
1430 moveReq(r, lastDirectBlock)
1431 }
1432 }
1433
1434
1435 for path, r := range need {
1436 if have[path] == nil {
1437 if r.Indirect {
1438 moveReq(r, lastIndirectBlock)
1439 } else {
1440 moveReq(r, lastDirectBlock)
1441 }
1442 f.Require = append(f.Require, r)
1443 }
1444 }
1445
1446 f.SortBlocks()
1447 }
1448
1449 func (f *File) DropGodebug(key string) error {
1450 for _, g := range f.Godebug {
1451 if g.Key == key {
1452 g.Syntax.markRemoved()
1453 *g = Godebug{}
1454 }
1455 }
1456 return nil
1457 }
1458
1459 func (f *File) DropRequire(path string) error {
1460 for _, r := range f.Require {
1461 if r.Mod.Path == path {
1462 r.Syntax.markRemoved()
1463 *r = Require{}
1464 }
1465 }
1466 return nil
1467 }
1468
1469
1470
1471 func (f *File) AddExclude(path, vers string) error {
1472 if err := checkCanonicalVersion(path, vers); err != nil {
1473 return err
1474 }
1475
1476 var hint *Line
1477 for _, x := range f.Exclude {
1478 if x.Mod.Path == path && x.Mod.Version == vers {
1479 return nil
1480 }
1481 if x.Mod.Path == path {
1482 hint = x.Syntax
1483 }
1484 }
1485
1486 f.Exclude = append(f.Exclude, &Exclude{Mod: module.Version{Path: path, Version: vers}, Syntax: f.Syntax.addLine(hint, "exclude", AutoQuote(path), vers)})
1487 return nil
1488 }
1489
1490 func (f *File) DropExclude(path, vers string) error {
1491 for _, x := range f.Exclude {
1492 if x.Mod.Path == path && x.Mod.Version == vers {
1493 x.Syntax.markRemoved()
1494 *x = Exclude{}
1495 }
1496 }
1497 return nil
1498 }
1499
1500 func (f *File) AddReplace(oldPath, oldVers, newPath, newVers string) error {
1501 return addReplace(f.Syntax, &f.Replace, oldPath, oldVers, newPath, newVers)
1502 }
1503
1504 func addReplace(syntax *FileSyntax, replace *[]*Replace, oldPath, oldVers, newPath, newVers string) error {
1505 need := true
1506 old := module.Version{Path: oldPath, Version: oldVers}
1507 new := module.Version{Path: newPath, Version: newVers}
1508 tokens := []string{"replace", AutoQuote(oldPath)}
1509 if oldVers != "" {
1510 tokens = append(tokens, oldVers)
1511 }
1512 tokens = append(tokens, "=>", AutoQuote(newPath))
1513 if newVers != "" {
1514 tokens = append(tokens, newVers)
1515 }
1516
1517 var hint *Line
1518 for _, r := range *replace {
1519 if r.Old.Path == oldPath && (oldVers == "" || r.Old.Version == oldVers) {
1520 if need {
1521
1522 r.New = new
1523 syntax.updateLine(r.Syntax, tokens...)
1524 need = false
1525 continue
1526 }
1527
1528 r.Syntax.markRemoved()
1529 *r = Replace{}
1530 }
1531 if r.Old.Path == oldPath {
1532 hint = r.Syntax
1533 }
1534 }
1535 if need {
1536 *replace = append(*replace, &Replace{Old: old, New: new, Syntax: syntax.addLine(hint, tokens...)})
1537 }
1538 return nil
1539 }
1540
1541 func (f *File) DropReplace(oldPath, oldVers string) error {
1542 for _, r := range f.Replace {
1543 if r.Old.Path == oldPath && r.Old.Version == oldVers {
1544 r.Syntax.markRemoved()
1545 *r = Replace{}
1546 }
1547 }
1548 return nil
1549 }
1550
1551
1552
1553 func (f *File) AddRetract(vi VersionInterval, rationale string) error {
1554 var path string
1555 if f.Module != nil {
1556 path = f.Module.Mod.Path
1557 }
1558 if err := checkCanonicalVersion(path, vi.High); err != nil {
1559 return err
1560 }
1561 if err := checkCanonicalVersion(path, vi.Low); err != nil {
1562 return err
1563 }
1564
1565 r := &Retract{
1566 VersionInterval: vi,
1567 }
1568 if vi.Low == vi.High {
1569 r.Syntax = f.Syntax.addLine(nil, "retract", AutoQuote(vi.Low))
1570 } else {
1571 r.Syntax = f.Syntax.addLine(nil, "retract", "[", AutoQuote(vi.Low), ",", AutoQuote(vi.High), "]")
1572 }
1573 if rationale != "" {
1574 for _, line := range strings.Split(rationale, "\n") {
1575 com := Comment{Token: "// " + line}
1576 r.Syntax.Comment().Before = append(r.Syntax.Comment().Before, com)
1577 }
1578 }
1579 return nil
1580 }
1581
1582 func (f *File) DropRetract(vi VersionInterval) error {
1583 for _, r := range f.Retract {
1584 if r.VersionInterval == vi {
1585 r.Syntax.markRemoved()
1586 *r = Retract{}
1587 }
1588 }
1589 return nil
1590 }
1591
1592
1593
1594 func (f *File) AddTool(path string) error {
1595 for _, t := range f.Tool {
1596 if t.Path == path {
1597 return nil
1598 }
1599 }
1600
1601 f.Tool = append(f.Tool, &Tool{
1602 Path: path,
1603 Syntax: f.Syntax.addLine(nil, "tool", path),
1604 })
1605
1606 f.SortBlocks()
1607 return nil
1608 }
1609
1610
1611
1612 func (f *File) DropTool(path string) error {
1613 for _, t := range f.Tool {
1614 if t.Path == path {
1615 t.Syntax.markRemoved()
1616 *t = Tool{}
1617 }
1618 }
1619 return nil
1620 }
1621
1622 func (f *File) SortBlocks() {
1623 f.removeDups()
1624
1625
1626
1627
1628 const semanticSortForExcludeVersionV = "v1.21"
1629 useSemanticSortForExclude := f.Go != nil && semver.Compare("v"+f.Go.Version, semanticSortForExcludeVersionV) >= 0
1630
1631 for _, stmt := range f.Syntax.Stmt {
1632 block, ok := stmt.(*LineBlock)
1633 if !ok {
1634 continue
1635 }
1636 less := lineLess
1637 if block.Token[0] == "exclude" && useSemanticSortForExclude {
1638 less = lineExcludeLess
1639 } else if block.Token[0] == "retract" {
1640 less = lineRetractLess
1641 }
1642 sort.SliceStable(block.Line, func(i, j int) bool {
1643 return less(block.Line[i], block.Line[j])
1644 })
1645 }
1646 }
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659 func (f *File) removeDups() {
1660 removeDups(f.Syntax, &f.Exclude, &f.Replace, &f.Tool)
1661 }
1662
1663 func removeDups(syntax *FileSyntax, exclude *[]*Exclude, replace *[]*Replace, tool *[]*Tool) {
1664 kill := make(map[*Line]bool)
1665
1666
1667 if exclude != nil {
1668 haveExclude := make(map[module.Version]bool)
1669 for _, x := range *exclude {
1670 if haveExclude[x.Mod] {
1671 kill[x.Syntax] = true
1672 continue
1673 }
1674 haveExclude[x.Mod] = true
1675 }
1676 var excl []*Exclude
1677 for _, x := range *exclude {
1678 if !kill[x.Syntax] {
1679 excl = append(excl, x)
1680 }
1681 }
1682 *exclude = excl
1683 }
1684
1685
1686
1687 haveReplace := make(map[module.Version]bool)
1688 for i := len(*replace) - 1; i >= 0; i-- {
1689 x := (*replace)[i]
1690 if haveReplace[x.Old] {
1691 kill[x.Syntax] = true
1692 continue
1693 }
1694 haveReplace[x.Old] = true
1695 }
1696 var repl []*Replace
1697 for _, x := range *replace {
1698 if !kill[x.Syntax] {
1699 repl = append(repl, x)
1700 }
1701 }
1702 *replace = repl
1703
1704 if tool != nil {
1705 haveTool := make(map[string]bool)
1706 for _, t := range *tool {
1707 if haveTool[t.Path] {
1708 kill[t.Syntax] = true
1709 continue
1710 }
1711 haveTool[t.Path] = true
1712 }
1713 var newTool []*Tool
1714 for _, t := range *tool {
1715 if !kill[t.Syntax] {
1716 newTool = append(newTool, t)
1717 }
1718 }
1719 *tool = newTool
1720 }
1721
1722
1723
1724
1725 var stmts []Expr
1726 for _, stmt := range syntax.Stmt {
1727 switch stmt := stmt.(type) {
1728 case *Line:
1729 if kill[stmt] {
1730 continue
1731 }
1732 case *LineBlock:
1733 var lines []*Line
1734 for _, line := range stmt.Line {
1735 if !kill[line] {
1736 lines = append(lines, line)
1737 }
1738 }
1739 stmt.Line = lines
1740 if len(lines) == 0 {
1741 continue
1742 }
1743 }
1744 stmts = append(stmts, stmt)
1745 }
1746 syntax.Stmt = stmts
1747 }
1748
1749
1750
1751 func lineLess(li, lj *Line) bool {
1752 for k := 0; k < len(li.Token) && k < len(lj.Token); k++ {
1753 if li.Token[k] != lj.Token[k] {
1754 return li.Token[k] < lj.Token[k]
1755 }
1756 }
1757 return len(li.Token) < len(lj.Token)
1758 }
1759
1760
1761
1762 func lineExcludeLess(li, lj *Line) bool {
1763 if len(li.Token) != 2 || len(lj.Token) != 2 {
1764
1765
1766 return lineLess(li, lj)
1767 }
1768
1769
1770 if pi, pj := li.Token[0], lj.Token[0]; pi != pj {
1771 return pi < pj
1772 }
1773 return semver.Compare(li.Token[1], lj.Token[1]) < 0
1774 }
1775
1776
1777
1778
1779
1780
1781 func lineRetractLess(li, lj *Line) bool {
1782 interval := func(l *Line) VersionInterval {
1783 if len(l.Token) == 1 {
1784 return VersionInterval{Low: l.Token[0], High: l.Token[0]}
1785 } else if len(l.Token) == 5 && l.Token[0] == "[" && l.Token[2] == "," && l.Token[4] == "]" {
1786 return VersionInterval{Low: l.Token[1], High: l.Token[3]}
1787 } else {
1788
1789 return VersionInterval{}
1790 }
1791 }
1792 vii := interval(li)
1793 vij := interval(lj)
1794 if cmp := semver.Compare(vii.Low, vij.Low); cmp != 0 {
1795 return cmp > 0
1796 }
1797 return semver.Compare(vii.High, vij.High) > 0
1798 }
1799
1800
1801
1802
1803
1804
1805 func checkCanonicalVersion(path, vers string) error {
1806 _, pathMajor, pathMajorOk := module.SplitPathVersion(path)
1807
1808 if vers == "" || vers != module.CanonicalVersion(vers) {
1809 if pathMajor == "" {
1810 return &module.InvalidVersionError{
1811 Version: vers,
1812 Err: fmt.Errorf("must be of the form v1.2.3"),
1813 }
1814 }
1815 return &module.InvalidVersionError{
1816 Version: vers,
1817 Err: fmt.Errorf("must be of the form %s.2.3", module.PathMajorPrefix(pathMajor)),
1818 }
1819 }
1820
1821 if pathMajorOk {
1822 if err := module.CheckPathMajor(vers, pathMajor); err != nil {
1823 if pathMajor == "" {
1824
1825
1826 return &module.InvalidVersionError{
1827 Version: vers,
1828 Err: fmt.Errorf("should be %s+incompatible (or module %s/%v)", vers, path, semver.Major(vers)),
1829 }
1830 }
1831 return err
1832 }
1833 }
1834
1835 return nil
1836 }
1837
View as plain text