1// Copyright 2018 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5// Package modfile implements a parser and formatter for go.mod files.
6//
7// The go.mod syntax is described in
8// https://golang.org/cmd/go/#hdr-The_go_mod_file.
9//
10// The Parse and ParseLax functions both parse a go.mod file and return an
11// abstract syntax tree. ParseLax ignores unknown statements and may be used to
12// parse go.mod files that may have been developed with newer versions of Go.
13//
14// The File struct returned by Parse and ParseLax represent an abstract
15// go.mod file. File has several methods like AddNewRequire and DropReplace
16// that can be used to programmatically edit a file.
17//
18// The Format function formats a File back to a byte slice which can be
19// written to a file.
20package modfile
21
22import (
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// A File is the parsed, interpreted form of a go.mod file.
37type File struct {
38	Module  *Module
39	Go      *Go
40	Require []*Require
41	Exclude []*Exclude
42	Replace []*Replace
43	Retract []*Retract
44
45	Syntax *FileSyntax
46}
47
48// A Module is the module statement.
49type Module struct {
50	Mod    module.Version
51	Syntax *Line
52}
53
54// A Go is the go statement.
55type Go struct {
56	Version string // "1.23"
57	Syntax  *Line
58}
59
60// A Require is a single require statement.
61type Require struct {
62	Mod      module.Version
63	Indirect bool // has "// indirect" comment
64	Syntax   *Line
65}
66
67// An Exclude is a single exclude statement.
68type Exclude struct {
69	Mod    module.Version
70	Syntax *Line
71}
72
73// A Replace is a single replace statement.
74type Replace struct {
75	Old    module.Version
76	New    module.Version
77	Syntax *Line
78}
79
80// A Retract is a single retract statement.
81type 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.
91type VersionInterval struct {
92	Low, High string
93}
94
95func (f *File) AddModuleStmt(path string) error {
96	if f.Syntax == nil {
97		f.Syntax = new(FileSyntax)
98	}
99	if f.Module == nil {
100		f.Module = &Module{
101			Mod:    module.Version{Path: path},
102			Syntax: f.Syntax.addLine(nil, "module", AutoQuote(path)),
103		}
104	} else {
105		f.Module.Mod.Path = path
106		f.Syntax.updateLine(f.Module.Syntax, "module", AutoQuote(path))
107	}
108	return nil
109}
110
111func (f *File) AddComment(text string) {
112	if f.Syntax == nil {
113		f.Syntax = new(FileSyntax)
114	}
115	f.Syntax.Stmt = append(f.Syntax.Stmt, &CommentBlock{
116		Comments: Comments{
117			Before: []Comment{
118				{
119					Token: text,
120				},
121			},
122		},
123	})
124}
125
126type VersionFixer func(path, version string) (string, error)
127
128// errDontFix is returned by a VersionFixer to indicate the version should be
129// left alone, even if it's not canonical.
130var dontFixRetract VersionFixer = func(_, vers string) (string, error) {
131	return vers, nil
132}
133
134// Parse parses the data, reported in errors as being from file,
135// into a File struct. It applies fix, if non-nil, to canonicalize all module versions found.
136func Parse(file string, data []byte, fix VersionFixer) (*File, error) {
137	return parseToFile(file, data, fix, true)
138}
139
140// ParseLax is like Parse but ignores unknown statements.
141// It is used when parsing go.mod files other than the main module,
142// under the theory that most statement types we add in the future will
143// only apply in the main module, like exclude and replace,
144// and so we get better gradual deployments if old go commands
145// simply ignore those statements when found in go.mod files
146// in dependencies.
147func ParseLax(file string, data []byte, fix VersionFixer) (*File, error) {
148	return parseToFile(file, data, fix, false)
149}
150
151func parseToFile(file string, data []byte, fix VersionFixer, strict bool) (parsed *File, err error) {
152	fs, err := parse(file, data)
153	if err != nil {
154		return nil, err
155	}
156	f := &File{
157		Syntax: fs,
158	}
159	var errs ErrorList
160
161	// fix versions in retract directives after the file is parsed.
162	// We need the module path to fix versions, and it might be at the end.
163	defer func() {
164		oldLen := len(errs)
165		f.fixRetract(fix, &errs)
166		if len(errs) > oldLen {
167			parsed, err = nil, errs
168		}
169	}()
170
171	for _, x := range fs.Stmt {
172		switch x := x.(type) {
173		case *Line:
174			f.add(&errs, nil, x, x.Token[0], x.Token[1:], fix, strict)
175
176		case *LineBlock:
177			if len(x.Token) > 1 {
178				if strict {
179					errs = append(errs, Error{
180						Filename: file,
181						Pos:      x.Start,
182						Err:      fmt.Errorf("unknown block type: %s", strings.Join(x.Token, " ")),
183					})
184				}
185				continue
186			}
187			switch x.Token[0] {
188			default:
189				if strict {
190					errs = append(errs, Error{
191						Filename: file,
192						Pos:      x.Start,
193						Err:      fmt.Errorf("unknown block type: %s", strings.Join(x.Token, " ")),
194					})
195				}
196				continue
197			case "module", "require", "exclude", "replace", "retract":
198				for _, l := range x.Line {
199					f.add(&errs, x, l, x.Token[0], l.Token, fix, strict)
200				}
201			}
202		}
203	}
204
205	if len(errs) > 0 {
206		return nil, errs
207	}
208	return f, nil
209}
210
211var GoVersionRE = lazyregexp.New(`^([1-9][0-9]*)\.(0|[1-9][0-9]*)$`)
212
213func (f *File) add(errs *ErrorList, block *LineBlock, line *Line, verb string, args []string, fix VersionFixer, strict bool) {
214	// If strict is false, this module is a dependency.
215	// We ignore all unknown directives as well as main-module-only
216	// directives like replace and exclude. It will work better for
217	// forward compatibility if we can depend on modules that have unknown
218	// statements (presumed relevant only when acting as the main module)
219	// and simply ignore those statements.
220	if !strict {
221		switch verb {
222		case "go", "module", "retract", "require":
223			// want these even for dependency go.mods
224		default:
225			return
226		}
227	}
228
229	wrapModPathError := func(modPath string, err error) {
230		*errs = append(*errs, Error{
231			Filename: f.Syntax.Name,
232			Pos:      line.Start,
233			ModPath:  modPath,
234			Verb:     verb,
235			Err:      err,
236		})
237	}
238	wrapError := func(err error) {
239		*errs = append(*errs, Error{
240			Filename: f.Syntax.Name,
241			Pos:      line.Start,
242			Err:      err,
243		})
244	}
245	errorf := func(format string, args ...interface{}) {
246		wrapError(fmt.Errorf(format, args...))
247	}
248
249	switch verb {
250	default:
251		errorf("unknown directive: %s", verb)
252
253	case "go":
254		if f.Go != nil {
255			errorf("repeated go statement")
256			return
257		}
258		if len(args) != 1 {
259			errorf("go directive expects exactly one argument")
260			return
261		} else if !GoVersionRE.MatchString(args[0]) {
262			errorf("invalid go version '%s': must match format 1.23", args[0])
263			return
264		}
265
266		f.Go = &Go{Syntax: line}
267		f.Go.Version = args[0]
268
269	case "module":
270		if f.Module != nil {
271			errorf("repeated module statement")
272			return
273		}
274		f.Module = &Module{Syntax: line}
275		if len(args) != 1 {
276			errorf("usage: module module/path")
277			return
278		}
279		s, err := parseString(&args[0])
280		if err != nil {
281			errorf("invalid quoted string: %v", err)
282			return
283		}
284		f.Module.Mod = module.Version{Path: s}
285
286	case "require", "exclude":
287		if len(args) != 2 {
288			errorf("usage: %s module/path v1.2.3", verb)
289			return
290		}
291		s, err := parseString(&args[0])
292		if err != nil {
293			errorf("invalid quoted string: %v", err)
294			return
295		}
296		v, err := parseVersion(verb, s, &args[1], fix)
297		if err != nil {
298			wrapError(err)
299			return
300		}
301		pathMajor, err := modulePathMajor(s)
302		if err != nil {
303			wrapError(err)
304			return
305		}
306		if err := module.CheckPathMajor(v, pathMajor); err != nil {
307			wrapModPathError(s, err)
308			return
309		}
310		if verb == "require" {
311			f.Require = append(f.Require, &Require{
312				Mod:      module.Version{Path: s, Version: v},
313				Syntax:   line,
314				Indirect: isIndirect(line),
315			})
316		} else {
317			f.Exclude = append(f.Exclude, &Exclude{
318				Mod:    module.Version{Path: s, Version: v},
319				Syntax: line,
320			})
321		}
322
323	case "replace":
324		arrow := 2
325		if len(args) >= 2 && args[1] == "=>" {
326			arrow = 1
327		}
328		if len(args) < arrow+2 || len(args) > arrow+3 || args[arrow] != "=>" {
329			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)
330			return
331		}
332		s, err := parseString(&args[0])
333		if err != nil {
334			errorf("invalid quoted string: %v", err)
335			return
336		}
337		pathMajor, err := modulePathMajor(s)
338		if err != nil {
339			wrapModPathError(s, err)
340			return
341		}
342		var v string
343		if arrow == 2 {
344			v, err = parseVersion(verb, s, &args[1], fix)
345			if err != nil {
346				wrapError(err)
347				return
348			}
349			if err := module.CheckPathMajor(v, pathMajor); err != nil {
350				wrapModPathError(s, err)
351				return
352			}
353		}
354		ns, err := parseString(&args[arrow+1])
355		if err != nil {
356			errorf("invalid quoted string: %v", err)
357			return
358		}
359		nv := ""
360		if len(args) == arrow+2 {
361			if !IsDirectoryPath(ns) {
362				errorf("replacement module without version must be directory path (rooted or starting with ./ or ../)")
363				return
364			}
365			if filepath.Separator == '/' && strings.Contains(ns, `\`) {
366				errorf("replacement directory appears to be Windows path (on a non-windows system)")
367				return
368			}
369		}
370		if len(args) == arrow+3 {
371			nv, err = parseVersion(verb, ns, &args[arrow+2], fix)
372			if err != nil {
373				wrapError(err)
374				return
375			}
376			if IsDirectoryPath(ns) {
377				errorf("replacement module directory path %q cannot have version", ns)
378				return
379			}
380		}
381		f.Replace = append(f.Replace, &Replace{
382			Old:    module.Version{Path: s, Version: v},
383			New:    module.Version{Path: ns, Version: nv},
384			Syntax: line,
385		})
386
387	case "retract":
388		rationale := parseRetractRationale(block, line)
389		vi, err := parseVersionInterval(verb, "", &args, dontFixRetract)
390		if err != nil {
391			if strict {
392				wrapError(err)
393				return
394			} else {
395				// Only report errors parsing intervals in the main module. We may
396				// support additional syntax in the future, such as open and half-open
397				// intervals. Those can't be supported now, because they break the
398				// go.mod parser, even in lax mode.
399				return
400			}
401		}
402		if len(args) > 0 && strict {
403			// In the future, there may be additional information after the version.
404			errorf("unexpected token after version: %q", args[0])
405			return
406		}
407		retract := &Retract{
408			VersionInterval: vi,
409			Rationale:       rationale,
410			Syntax:          line,
411		}
412		f.Retract = append(f.Retract, retract)
413	}
414}
415
416// fixRetract applies fix to each retract directive in f, appending any errors
417// to errs.
418//
419// Most versions are fixed as we parse the file, but for retract directives,
420// the relevant module path is the one specified with the module directive,
421// and that might appear at the end of the file (or not at all).
422func (f *File) fixRetract(fix VersionFixer, errs *ErrorList) {
423	if fix == nil {
424		return
425	}
426	path := ""
427	if f.Module != nil {
428		path = f.Module.Mod.Path
429	}
430	var r *Retract
431	wrapError := func(err error) {
432		*errs = append(*errs, Error{
433			Filename: f.Syntax.Name,
434			Pos:      r.Syntax.Start,
435			Err:      err,
436		})
437	}
438
439	for _, r = range f.Retract {
440		if path == "" {
441			wrapError(errors.New("no module directive found, so retract cannot be used"))
442			return // only print the first one of these
443		}
444
445		args := r.Syntax.Token
446		if args[0] == "retract" {
447			args = args[1:]
448		}
449		vi, err := parseVersionInterval("retract", path, &args, fix)
450		if err != nil {
451			wrapError(err)
452		}
453		r.VersionInterval = vi
454	}
455}
456
457// isIndirect reports whether line has a "// indirect" comment,
458// meaning it is in go.mod only for its effect on indirect dependencies,
459// so that it can be dropped entirely once the effective version of the
460// indirect dependency reaches the given minimum version.
461func isIndirect(line *Line) bool {
462	if len(line.Suffix) == 0 {
463		return false
464	}
465	f := strings.Fields(strings.TrimPrefix(line.Suffix[0].Token, string(slashSlash)))
466	return (len(f) == 1 && f[0] == "indirect" || len(f) > 1 && f[0] == "indirect;")
467}
468
469// setIndirect sets line to have (or not have) a "// indirect" comment.
470func setIndirect(line *Line, indirect bool) {
471	if isIndirect(line) == indirect {
472		return
473	}
474	if indirect {
475		// Adding comment.
476		if len(line.Suffix) == 0 {
477			// New comment.
478			line.Suffix = []Comment{{Token: "// indirect", Suffix: true}}
479			return
480		}
481
482		com := &line.Suffix[0]
483		text := strings.TrimSpace(strings.TrimPrefix(com.Token, string(slashSlash)))
484		if text == "" {
485			// Empty comment.
486			com.Token = "// indirect"
487			return
488		}
489
490		// Insert at beginning of existing comment.
491		com.Token = "// indirect; " + text
492		return
493	}
494
495	// Removing comment.
496	f := strings.Fields(line.Suffix[0].Token)
497	if len(f) == 2 {
498		// Remove whole comment.
499		line.Suffix = nil
500		return
501	}
502
503	// Remove comment prefix.
504	com := &line.Suffix[0]
505	i := strings.Index(com.Token, "indirect;")
506	com.Token = "//" + com.Token[i+len("indirect;"):]
507}
508
509// IsDirectoryPath reports whether the given path should be interpreted
510// as a directory path. Just like on the go command line, relative paths
511// and rooted paths are directory paths; the rest are module paths.
512func IsDirectoryPath(ns string) bool {
513	// Because go.mod files can move from one system to another,
514	// we check all known path syntaxes, both Unix and Windows.
515	return strings.HasPrefix(ns, "./") || strings.HasPrefix(ns, "../") || strings.HasPrefix(ns, "/") ||
516		strings.HasPrefix(ns, `.\`) || strings.HasPrefix(ns, `..\`) || strings.HasPrefix(ns, `\`) ||
517		len(ns) >= 2 && ('A' <= ns[0] && ns[0] <= 'Z' || 'a' <= ns[0] && ns[0] <= 'z') && ns[1] == ':'
518}
519
520// MustQuote reports whether s must be quoted in order to appear as
521// a single token in a go.mod line.
522func MustQuote(s string) bool {
523	for _, r := range s {
524		switch r {
525		case ' ', '"', '\'', '`':
526			return true
527
528		case '(', ')', '[', ']', '{', '}', ',':
529			if len(s) > 1 {
530				return true
531			}
532
533		default:
534			if !unicode.IsPrint(r) {
535				return true
536			}
537		}
538	}
539	return s == "" || strings.Contains(s, "//") || strings.Contains(s, "/*")
540}
541
542// AutoQuote returns s or, if quoting is required for s to appear in a go.mod,
543// the quotation of s.
544func AutoQuote(s string) string {
545	if MustQuote(s) {
546		return strconv.Quote(s)
547	}
548	return s
549}
550
551func parseVersionInterval(verb string, path string, args *[]string, fix VersionFixer) (VersionInterval, error) {
552	toks := *args
553	if len(toks) == 0 || toks[0] == "(" {
554		return VersionInterval{}, fmt.Errorf("expected '[' or version")
555	}
556	if toks[0] != "[" {
557		v, err := parseVersion(verb, path, &toks[0], fix)
558		if err != nil {
559			return VersionInterval{}, err
560		}
561		*args = toks[1:]
562		return VersionInterval{Low: v, High: v}, nil
563	}
564	toks = toks[1:]
565
566	if len(toks) == 0 {
567		return VersionInterval{}, fmt.Errorf("expected version after '['")
568	}
569	low, err := parseVersion(verb, path, &toks[0], fix)
570	if err != nil {
571		return VersionInterval{}, err
572	}
573	toks = toks[1:]
574
575	if len(toks) == 0 || toks[0] != "," {
576		return VersionInterval{}, fmt.Errorf("expected ',' after version")
577	}
578	toks = toks[1:]
579
580	if len(toks) == 0 {
581		return VersionInterval{}, fmt.Errorf("expected version after ','")
582	}
583	high, err := parseVersion(verb, path, &toks[0], fix)
584	if err != nil {
585		return VersionInterval{}, err
586	}
587	toks = toks[1:]
588
589	if len(toks) == 0 || toks[0] != "]" {
590		return VersionInterval{}, fmt.Errorf("expected ']' after version")
591	}
592	toks = toks[1:]
593
594	*args = toks
595	return VersionInterval{Low: low, High: high}, nil
596}
597
598func parseString(s *string) (string, error) {
599	t := *s
600	if strings.HasPrefix(t, `"`) {
601		var err error
602		if t, err = strconv.Unquote(t); err != nil {
603			return "", err
604		}
605	} else if strings.ContainsAny(t, "\"'`") {
606		// Other quotes are reserved both for possible future expansion
607		// and to avoid confusion. For example if someone types 'x'
608		// we want that to be a syntax error and not a literal x in literal quotation marks.
609		return "", fmt.Errorf("unquoted string cannot contain quote")
610	}
611	*s = AutoQuote(t)
612	return t, nil
613}
614
615// parseRetractRationale extracts the rationale for a retract directive from the
616// surrounding comments. If the line does not have comments and is part of a
617// block that does have comments, the block's comments are used.
618func parseRetractRationale(block *LineBlock, line *Line) string {
619	comments := line.Comment()
620	if block != nil && len(comments.Before) == 0 && len(comments.Suffix) == 0 {
621		comments = block.Comment()
622	}
623	groups := [][]Comment{comments.Before, comments.Suffix}
624	var lines []string
625	for _, g := range groups {
626		for _, c := range g {
627			if !strings.HasPrefix(c.Token, "//") {
628				continue // blank line
629			}
630			lines = append(lines, strings.TrimSpace(strings.TrimPrefix(c.Token, "//")))
631		}
632	}
633	return strings.Join(lines, "\n")
634}
635
636type ErrorList []Error
637
638func (e ErrorList) Error() string {
639	errStrs := make([]string, len(e))
640	for i, err := range e {
641		errStrs[i] = err.Error()
642	}
643	return strings.Join(errStrs, "\n")
644}
645
646type Error struct {
647	Filename string
648	Pos      Position
649	Verb     string
650	ModPath  string
651	Err      error
652}
653
654func (e *Error) Error() string {
655	var pos string
656	if e.Pos.LineRune > 1 {
657		// Don't print LineRune if it's 1 (beginning of line).
658		// It's always 1 except in scanner errors, which are rare.
659		pos = fmt.Sprintf("%s:%d:%d: ", e.Filename, e.Pos.Line, e.Pos.LineRune)
660	} else if e.Pos.Line > 0 {
661		pos = fmt.Sprintf("%s:%d: ", e.Filename, e.Pos.Line)
662	} else if e.Filename != "" {
663		pos = fmt.Sprintf("%s: ", e.Filename)
664	}
665
666	var directive string
667	if e.ModPath != "" {
668		directive = fmt.Sprintf("%s %s: ", e.Verb, e.ModPath)
669	} else if e.Verb != "" {
670		directive = fmt.Sprintf("%s: ", e.Verb)
671	}
672
673	return pos + directive + e.Err.Error()
674}
675
676func (e *Error) Unwrap() error { return e.Err }
677
678func parseVersion(verb string, path string, s *string, fix VersionFixer) (string, error) {
679	t, err := parseString(s)
680	if err != nil {
681		return "", &Error{
682			Verb:    verb,
683			ModPath: path,
684			Err: &module.InvalidVersionError{
685				Version: *s,
686				Err:     err,
687			},
688		}
689	}
690	if fix != nil {
691		fixed, err := fix(path, t)
692		if err != nil {
693			if err, ok := err.(*module.ModuleError); ok {
694				return "", &Error{
695					Verb:    verb,
696					ModPath: path,
697					Err:     err.Err,
698				}
699			}
700			return "", err
701		}
702		t = fixed
703	} else {
704		cv := module.CanonicalVersion(t)
705		if cv == "" {
706			return "", &Error{
707				Verb:    verb,
708				ModPath: path,
709				Err: &module.InvalidVersionError{
710					Version: t,
711					Err:     errors.New("must be of the form v1.2.3"),
712				},
713			}
714		}
715		t = cv
716	}
717	*s = t
718	return *s, nil
719}
720
721func modulePathMajor(path string) (string, error) {
722	_, major, ok := module.SplitPathVersion(path)
723	if !ok {
724		return "", fmt.Errorf("invalid module path")
725	}
726	return major, nil
727}
728
729func (f *File) Format() ([]byte, error) {
730	return Format(f.Syntax), nil
731}
732
733// Cleanup cleans up the file f after any edit operations.
734// To avoid quadratic behavior, modifications like DropRequire
735// clear the entry but do not remove it from the slice.
736// Cleanup cleans out all the cleared entries.
737func (f *File) Cleanup() {
738	w := 0
739	for _, r := range f.Require {
740		if r.Mod.Path != "" {
741			f.Require[w] = r
742			w++
743		}
744	}
745	f.Require = f.Require[:w]
746
747	w = 0
748	for _, x := range f.Exclude {
749		if x.Mod.Path != "" {
750			f.Exclude[w] = x
751			w++
752		}
753	}
754	f.Exclude = f.Exclude[:w]
755
756	w = 0
757	for _, r := range f.Replace {
758		if r.Old.Path != "" {
759			f.Replace[w] = r
760			w++
761		}
762	}
763	f.Replace = f.Replace[:w]
764
765	w = 0
766	for _, r := range f.Retract {
767		if r.Low != "" || r.High != "" {
768			f.Retract[w] = r
769			w++
770		}
771	}
772	f.Retract = f.Retract[:w]
773
774	f.Syntax.Cleanup()
775}
776
777func (f *File) AddGoStmt(version string) error {
778	if !GoVersionRE.MatchString(version) {
779		return fmt.Errorf("invalid language version string %q", version)
780	}
781	if f.Go == nil {
782		var hint Expr
783		if f.Module != nil && f.Module.Syntax != nil {
784			hint = f.Module.Syntax
785		}
786		f.Go = &Go{
787			Version: version,
788			Syntax:  f.Syntax.addLine(hint, "go", version),
789		}
790	} else {
791		f.Go.Version = version
792		f.Syntax.updateLine(f.Go.Syntax, "go", version)
793	}
794	return nil
795}
796
797func (f *File) AddRequire(path, vers string) error {
798	need := true
799	for _, r := range f.Require {
800		if r.Mod.Path == path {
801			if need {
802				r.Mod.Version = vers
803				f.Syntax.updateLine(r.Syntax, "require", AutoQuote(path), vers)
804				need = false
805			} else {
806				f.Syntax.removeLine(r.Syntax)
807				*r = Require{}
808			}
809		}
810	}
811
812	if need {
813		f.AddNewRequire(path, vers, false)
814	}
815	return nil
816}
817
818func (f *File) AddNewRequire(path, vers string, indirect bool) {
819	line := f.Syntax.addLine(nil, "require", AutoQuote(path), vers)
820	setIndirect(line, indirect)
821	f.Require = append(f.Require, &Require{module.Version{Path: path, Version: vers}, indirect, line})
822}
823
824func (f *File) SetRequire(req []*Require) {
825	need := make(map[string]string)
826	indirect := make(map[string]bool)
827	for _, r := range req {
828		need[r.Mod.Path] = r.Mod.Version
829		indirect[r.Mod.Path] = r.Indirect
830	}
831
832	for _, r := range f.Require {
833		if v, ok := need[r.Mod.Path]; ok {
834			r.Mod.Version = v
835			r.Indirect = indirect[r.Mod.Path]
836		} else {
837			*r = Require{}
838		}
839	}
840
841	var newStmts []Expr
842	for _, stmt := range f.Syntax.Stmt {
843		switch stmt := stmt.(type) {
844		case *LineBlock:
845			if len(stmt.Token) > 0 && stmt.Token[0] == "require" {
846				var newLines []*Line
847				for _, line := range stmt.Line {
848					if p, err := parseString(&line.Token[0]); err == nil && need[p] != "" {
849						if len(line.Comments.Before) == 1 && len(line.Comments.Before[0].Token) == 0 {
850							line.Comments.Before = line.Comments.Before[:0]
851						}
852						line.Token[1] = need[p]
853						delete(need, p)
854						setIndirect(line, indirect[p])
855						newLines = append(newLines, line)
856					}
857				}
858				if len(newLines) == 0 {
859					continue // drop stmt
860				}
861				stmt.Line = newLines
862			}
863
864		case *Line:
865			if len(stmt.Token) > 0 && stmt.Token[0] == "require" {
866				if p, err := parseString(&stmt.Token[1]); err == nil && need[p] != "" {
867					stmt.Token[2] = need[p]
868					delete(need, p)
869					setIndirect(stmt, indirect[p])
870				} else {
871					continue // drop stmt
872				}
873			}
874		}
875		newStmts = append(newStmts, stmt)
876	}
877	f.Syntax.Stmt = newStmts
878
879	for path, vers := range need {
880		f.AddNewRequire(path, vers, indirect[path])
881	}
882	f.SortBlocks()
883}
884
885func (f *File) DropRequire(path string) error {
886	for _, r := range f.Require {
887		if r.Mod.Path == path {
888			f.Syntax.removeLine(r.Syntax)
889			*r = Require{}
890		}
891	}
892	return nil
893}
894
895// AddExclude adds a exclude statement to the mod file. Errors if the provided
896// version is not a canonical version string
897func (f *File) AddExclude(path, vers string) error {
898	if err := checkCanonicalVersion(path, vers); err != nil {
899		return err
900	}
901
902	var hint *Line
903	for _, x := range f.Exclude {
904		if x.Mod.Path == path && x.Mod.Version == vers {
905			return nil
906		}
907		if x.Mod.Path == path {
908			hint = x.Syntax
909		}
910	}
911
912	f.Exclude = append(f.Exclude, &Exclude{Mod: module.Version{Path: path, Version: vers}, Syntax: f.Syntax.addLine(hint, "exclude", AutoQuote(path), vers)})
913	return nil
914}
915
916func (f *File) DropExclude(path, vers string) error {
917	for _, x := range f.Exclude {
918		if x.Mod.Path == path && x.Mod.Version == vers {
919			f.Syntax.removeLine(x.Syntax)
920			*x = Exclude{}
921		}
922	}
923	return nil
924}
925
926func (f *File) AddReplace(oldPath, oldVers, newPath, newVers string) error {
927	need := true
928	old := module.Version{Path: oldPath, Version: oldVers}
929	new := module.Version{Path: newPath, Version: newVers}
930	tokens := []string{"replace", AutoQuote(oldPath)}
931	if oldVers != "" {
932		tokens = append(tokens, oldVers)
933	}
934	tokens = append(tokens, "=>", AutoQuote(newPath))
935	if newVers != "" {
936		tokens = append(tokens, newVers)
937	}
938
939	var hint *Line
940	for _, r := range f.Replace {
941		if r.Old.Path == oldPath && (oldVers == "" || r.Old.Version == oldVers) {
942			if need {
943				// Found replacement for old; update to use new.
944				r.New = new
945				f.Syntax.updateLine(r.Syntax, tokens...)
946				need = false
947				continue
948			}
949			// Already added; delete other replacements for same.
950			f.Syntax.removeLine(r.Syntax)
951			*r = Replace{}
952		}
953		if r.Old.Path == oldPath {
954			hint = r.Syntax
955		}
956	}
957	if need {
958		f.Replace = append(f.Replace, &Replace{Old: old, New: new, Syntax: f.Syntax.addLine(hint, tokens...)})
959	}
960	return nil
961}
962
963func (f *File) DropReplace(oldPath, oldVers string) error {
964	for _, r := range f.Replace {
965		if r.Old.Path == oldPath && r.Old.Version == oldVers {
966			f.Syntax.removeLine(r.Syntax)
967			*r = Replace{}
968		}
969	}
970	return nil
971}
972
973// AddRetract adds a retract statement to the mod file. Errors if the provided
974// version interval does not consist of canonical version strings
975func (f *File) AddRetract(vi VersionInterval, rationale string) error {
976	var path string
977	if f.Module != nil {
978		path = f.Module.Mod.Path
979	}
980	if err := checkCanonicalVersion(path, vi.High); err != nil {
981		return err
982	}
983	if err := checkCanonicalVersion(path, vi.Low); err != nil {
984		return err
985	}
986
987	r := &Retract{
988		VersionInterval: vi,
989	}
990	if vi.Low == vi.High {
991		r.Syntax = f.Syntax.addLine(nil, "retract", AutoQuote(vi.Low))
992	} else {
993		r.Syntax = f.Syntax.addLine(nil, "retract", "[", AutoQuote(vi.Low), ",", AutoQuote(vi.High), "]")
994	}
995	if rationale != "" {
996		for _, line := range strings.Split(rationale, "\n") {
997			com := Comment{Token: "// " + line}
998			r.Syntax.Comment().Before = append(r.Syntax.Comment().Before, com)
999		}
1000	}
1001	return nil
1002}
1003
1004func (f *File) DropRetract(vi VersionInterval) error {
1005	for _, r := range f.Retract {
1006		if r.VersionInterval == vi {
1007			f.Syntax.removeLine(r.Syntax)
1008			*r = Retract{}
1009		}
1010	}
1011	return nil
1012}
1013
1014func (f *File) SortBlocks() {
1015	f.removeDups() // otherwise sorting is unsafe
1016
1017	for _, stmt := range f.Syntax.Stmt {
1018		block, ok := stmt.(*LineBlock)
1019		if !ok {
1020			continue
1021		}
1022		less := lineLess
1023		if block.Token[0] == "retract" {
1024			less = lineRetractLess
1025		}
1026		sort.SliceStable(block.Line, func(i, j int) bool {
1027			return less(block.Line[i], block.Line[j])
1028		})
1029	}
1030}
1031
1032// removeDups removes duplicate exclude and replace directives.
1033//
1034// Earlier exclude directives take priority.
1035//
1036// Later replace directives take priority.
1037//
1038// require directives are not de-duplicated. That's left up to higher-level
1039// logic (MVS).
1040//
1041// retract directives are not de-duplicated since comments are
1042// meaningful, and versions may be retracted multiple times.
1043func (f *File) removeDups() {
1044	kill := make(map[*Line]bool)
1045
1046	// Remove duplicate excludes.
1047	haveExclude := make(map[module.Version]bool)
1048	for _, x := range f.Exclude {
1049		if haveExclude[x.Mod] {
1050			kill[x.Syntax] = true
1051			continue
1052		}
1053		haveExclude[x.Mod] = true
1054	}
1055	var excl []*Exclude
1056	for _, x := range f.Exclude {
1057		if !kill[x.Syntax] {
1058			excl = append(excl, x)
1059		}
1060	}
1061	f.Exclude = excl
1062
1063	// Remove duplicate replacements.
1064	// Later replacements take priority over earlier ones.
1065	haveReplace := make(map[module.Version]bool)
1066	for i := len(f.Replace) - 1; i >= 0; i-- {
1067		x := f.Replace[i]
1068		if haveReplace[x.Old] {
1069			kill[x.Syntax] = true
1070			continue
1071		}
1072		haveReplace[x.Old] = true
1073	}
1074	var repl []*Replace
1075	for _, x := range f.Replace {
1076		if !kill[x.Syntax] {
1077			repl = append(repl, x)
1078		}
1079	}
1080	f.Replace = repl
1081
1082	// Duplicate require and retract directives are not removed.
1083
1084	// Drop killed statements from the syntax tree.
1085	var stmts []Expr
1086	for _, stmt := range f.Syntax.Stmt {
1087		switch stmt := stmt.(type) {
1088		case *Line:
1089			if kill[stmt] {
1090				continue
1091			}
1092		case *LineBlock:
1093			var lines []*Line
1094			for _, line := range stmt.Line {
1095				if !kill[line] {
1096					lines = append(lines, line)
1097				}
1098			}
1099			stmt.Line = lines
1100			if len(lines) == 0 {
1101				continue
1102			}
1103		}
1104		stmts = append(stmts, stmt)
1105	}
1106	f.Syntax.Stmt = stmts
1107}
1108
1109// lineLess returns whether li should be sorted before lj. It sorts
1110// lexicographically without assigning any special meaning to tokens.
1111func lineLess(li, lj *Line) bool {
1112	for k := 0; k < len(li.Token) && k < len(lj.Token); k++ {
1113		if li.Token[k] != lj.Token[k] {
1114			return li.Token[k] < lj.Token[k]
1115		}
1116	}
1117	return len(li.Token) < len(lj.Token)
1118}
1119
1120// lineRetractLess returns whether li should be sorted before lj for lines in
1121// a "retract" block. It treats each line as a version interval. Single versions
1122// are compared as if they were intervals with the same low and high version.
1123// Intervals are sorted in descending order, first by low version, then by
1124// high version, using semver.Compare.
1125func lineRetractLess(li, lj *Line) bool {
1126	interval := func(l *Line) VersionInterval {
1127		if len(l.Token) == 1 {
1128			return VersionInterval{Low: l.Token[0], High: l.Token[0]}
1129		} else if len(l.Token) == 5 && l.Token[0] == "[" && l.Token[2] == "," && l.Token[4] == "]" {
1130			return VersionInterval{Low: l.Token[1], High: l.Token[3]}
1131		} else {
1132			// Line in unknown format. Treat as an invalid version.
1133			return VersionInterval{}
1134		}
1135	}
1136	vii := interval(li)
1137	vij := interval(lj)
1138	if cmp := semver.Compare(vii.Low, vij.Low); cmp != 0 {
1139		return cmp > 0
1140	}
1141	return semver.Compare(vii.High, vij.High) > 0
1142}
1143
1144// checkCanonicalVersion returns a non-nil error if vers is not a canonical
1145// version string or does not match the major version of path.
1146//
1147// If path is non-empty, the error text suggests a format with a major version
1148// corresponding to the path.
1149func checkCanonicalVersion(path, vers string) error {
1150	_, pathMajor, pathMajorOk := module.SplitPathVersion(path)
1151
1152	if vers == "" || vers != module.CanonicalVersion(vers) {
1153		if pathMajor == "" {
1154			return &module.InvalidVersionError{
1155				Version: vers,
1156				Err:     fmt.Errorf("must be of the form v1.2.3"),
1157			}
1158		}
1159		return &module.InvalidVersionError{
1160			Version: vers,
1161			Err:     fmt.Errorf("must be of the form %s.2.3", module.PathMajorPrefix(pathMajor)),
1162		}
1163	}
1164
1165	if pathMajorOk {
1166		if err := module.CheckPathMajor(vers, pathMajor); err != nil {
1167			if pathMajor == "" {
1168				// In this context, the user probably wrote "v2.3.4" when they meant
1169				// "v2.3.4+incompatible". Suggest that instead of "v0 or v1".
1170				return &module.InvalidVersionError{
1171					Version: vers,
1172					Err:     fmt.Errorf("should be %s+incompatible (or module %s/%v)", vers, path, semver.Major(vers)),
1173				}
1174			}
1175			return err
1176		}
1177	}
1178
1179	return nil
1180}
1181