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// Parse parses the data, reported in errors as being from file,
129// into a File struct. It applies fix, if non-nil, to canonicalize all module versions found.
130func Parse(file string, data []byte, fix VersionFixer) (*File, error) {
131	return parseToFile(file, data, fix, true)
132}
133
134// ParseLax is like Parse but ignores unknown statements.
135// It is used when parsing go.mod files other than the main module,
136// under the theory that most statement types we add in the future will
137// only apply in the main module, like exclude and replace,
138// and so we get better gradual deployments if old go commands
139// simply ignore those statements when found in go.mod files
140// in dependencies.
141func ParseLax(file string, data []byte, fix VersionFixer) (*File, error) {
142	return parseToFile(file, data, fix, false)
143}
144
145func parseToFile(file string, data []byte, fix VersionFixer, strict bool) (*File, error) {
146	fs, err := parse(file, data)
147	if err != nil {
148		return nil, err
149	}
150	f := &File{
151		Syntax: fs,
152	}
153
154	var errs ErrorList
155	for _, x := range fs.Stmt {
156		switch x := x.(type) {
157		case *Line:
158			f.add(&errs, nil, x, x.Token[0], x.Token[1:], fix, strict)
159
160		case *LineBlock:
161			if len(x.Token) > 1 {
162				if strict {
163					errs = append(errs, Error{
164						Filename: file,
165						Pos:      x.Start,
166						Err:      fmt.Errorf("unknown block type: %s", strings.Join(x.Token, " ")),
167					})
168				}
169				continue
170			}
171			switch x.Token[0] {
172			default:
173				if strict {
174					errs = append(errs, Error{
175						Filename: file,
176						Pos:      x.Start,
177						Err:      fmt.Errorf("unknown block type: %s", strings.Join(x.Token, " ")),
178					})
179				}
180				continue
181			case "module", "require", "exclude", "replace", "retract":
182				for _, l := range x.Line {
183					f.add(&errs, x, l, x.Token[0], l.Token, fix, strict)
184				}
185			}
186		}
187	}
188
189	if len(errs) > 0 {
190		return nil, errs
191	}
192	return f, nil
193}
194
195var GoVersionRE = lazyregexp.New(`^([1-9][0-9]*)\.(0|[1-9][0-9]*)$`)
196
197func (f *File) add(errs *ErrorList, block *LineBlock, line *Line, verb string, args []string, fix VersionFixer, strict bool) {
198	// If strict is false, this module is a dependency.
199	// We ignore all unknown directives as well as main-module-only
200	// directives like replace and exclude. It will work better for
201	// forward compatibility if we can depend on modules that have unknown
202	// statements (presumed relevant only when acting as the main module)
203	// and simply ignore those statements.
204	if !strict {
205		switch verb {
206		case "go", "module", "retract", "require":
207			// want these even for dependency go.mods
208		default:
209			return
210		}
211	}
212
213	wrapModPathError := func(modPath string, err error) {
214		*errs = append(*errs, Error{
215			Filename: f.Syntax.Name,
216			Pos:      line.Start,
217			ModPath:  modPath,
218			Verb:     verb,
219			Err:      err,
220		})
221	}
222	wrapError := func(err error) {
223		*errs = append(*errs, Error{
224			Filename: f.Syntax.Name,
225			Pos:      line.Start,
226			Err:      err,
227		})
228	}
229	errorf := func(format string, args ...interface{}) {
230		wrapError(fmt.Errorf(format, args...))
231	}
232
233	switch verb {
234	default:
235		errorf("unknown directive: %s", verb)
236
237	case "go":
238		if f.Go != nil {
239			errorf("repeated go statement")
240			return
241		}
242		if len(args) != 1 {
243			errorf("go directive expects exactly one argument")
244			return
245		} else if !GoVersionRE.MatchString(args[0]) {
246			errorf("invalid go version '%s': must match format 1.23", args[0])
247			return
248		}
249
250		f.Go = &Go{Syntax: line}
251		f.Go.Version = args[0]
252
253	case "module":
254		if f.Module != nil {
255			errorf("repeated module statement")
256			return
257		}
258		f.Module = &Module{Syntax: line}
259		if len(args) != 1 {
260			errorf("usage: module module/path")
261			return
262		}
263		s, err := parseString(&args[0])
264		if err != nil {
265			errorf("invalid quoted string: %v", err)
266			return
267		}
268		f.Module.Mod = module.Version{Path: s}
269
270	case "require", "exclude":
271		if len(args) != 2 {
272			errorf("usage: %s module/path v1.2.3", verb)
273			return
274		}
275		s, err := parseString(&args[0])
276		if err != nil {
277			errorf("invalid quoted string: %v", err)
278			return
279		}
280		v, err := parseVersion(verb, s, &args[1], fix)
281		if err != nil {
282			wrapError(err)
283			return
284		}
285		pathMajor, err := modulePathMajor(s)
286		if err != nil {
287			wrapError(err)
288			return
289		}
290		if err := module.CheckPathMajor(v, pathMajor); err != nil {
291			wrapModPathError(s, err)
292			return
293		}
294		if verb == "require" {
295			f.Require = append(f.Require, &Require{
296				Mod:      module.Version{Path: s, Version: v},
297				Syntax:   line,
298				Indirect: isIndirect(line),
299			})
300		} else {
301			f.Exclude = append(f.Exclude, &Exclude{
302				Mod:    module.Version{Path: s, Version: v},
303				Syntax: line,
304			})
305		}
306
307	case "replace":
308		arrow := 2
309		if len(args) >= 2 && args[1] == "=>" {
310			arrow = 1
311		}
312		if len(args) < arrow+2 || len(args) > arrow+3 || args[arrow] != "=>" {
313			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)
314			return
315		}
316		s, err := parseString(&args[0])
317		if err != nil {
318			errorf("invalid quoted string: %v", err)
319			return
320		}
321		pathMajor, err := modulePathMajor(s)
322		if err != nil {
323			wrapModPathError(s, err)
324			return
325		}
326		var v string
327		if arrow == 2 {
328			v, err = parseVersion(verb, s, &args[1], fix)
329			if err != nil {
330				wrapError(err)
331				return
332			}
333			if err := module.CheckPathMajor(v, pathMajor); err != nil {
334				wrapModPathError(s, err)
335				return
336			}
337		}
338		ns, err := parseString(&args[arrow+1])
339		if err != nil {
340			errorf("invalid quoted string: %v", err)
341			return
342		}
343		nv := ""
344		if len(args) == arrow+2 {
345			if !IsDirectoryPath(ns) {
346				errorf("replacement module without version must be directory path (rooted or starting with ./ or ../)")
347				return
348			}
349			if filepath.Separator == '/' && strings.Contains(ns, `\`) {
350				errorf("replacement directory appears to be Windows path (on a non-windows system)")
351				return
352			}
353		}
354		if len(args) == arrow+3 {
355			nv, err = parseVersion(verb, ns, &args[arrow+2], fix)
356			if err != nil {
357				wrapError(err)
358				return
359			}
360			if IsDirectoryPath(ns) {
361				errorf("replacement module directory path %q cannot have version", ns)
362				return
363			}
364		}
365		f.Replace = append(f.Replace, &Replace{
366			Old:    module.Version{Path: s, Version: v},
367			New:    module.Version{Path: ns, Version: nv},
368			Syntax: line,
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)
397	}
398}
399
400// isIndirect reports whether line has a "// indirect" comment,
401// meaning it is in go.mod only for its effect on indirect dependencies,
402// so that it can be dropped entirely once the effective version of the
403// indirect dependency reaches the given minimum version.
404func isIndirect(line *Line) bool {
405	if len(line.Suffix) == 0 {
406		return false
407	}
408	f := strings.Fields(strings.TrimPrefix(line.Suffix[0].Token, string(slashSlash)))
409	return (len(f) == 1 && f[0] == "indirect" || len(f) > 1 && f[0] == "indirect;")
410}
411
412// setIndirect sets line to have (or not have) a "// indirect" comment.
413func setIndirect(line *Line, indirect bool) {
414	if isIndirect(line) == indirect {
415		return
416	}
417	if indirect {
418		// Adding comment.
419		if len(line.Suffix) == 0 {
420			// New comment.
421			line.Suffix = []Comment{{Token: "// indirect", Suffix: true}}
422			return
423		}
424
425		com := &line.Suffix[0]
426		text := strings.TrimSpace(strings.TrimPrefix(com.Token, string(slashSlash)))
427		if text == "" {
428			// Empty comment.
429			com.Token = "// indirect"
430			return
431		}
432
433		// Insert at beginning of existing comment.
434		com.Token = "// indirect; " + text
435		return
436	}
437
438	// Removing comment.
439	f := strings.Fields(line.Suffix[0].Token)
440	if len(f) == 2 {
441		// Remove whole comment.
442		line.Suffix = nil
443		return
444	}
445
446	// Remove comment prefix.
447	com := &line.Suffix[0]
448	i := strings.Index(com.Token, "indirect;")
449	com.Token = "//" + com.Token[i+len("indirect;"):]
450}
451
452// IsDirectoryPath reports whether the given path should be interpreted
453// as a directory path. Just like on the go command line, relative paths
454// and rooted paths are directory paths; the rest are module paths.
455func IsDirectoryPath(ns string) bool {
456	// Because go.mod files can move from one system to another,
457	// we check all known path syntaxes, both Unix and Windows.
458	return strings.HasPrefix(ns, "./") || strings.HasPrefix(ns, "../") || strings.HasPrefix(ns, "/") ||
459		strings.HasPrefix(ns, `.\`) || strings.HasPrefix(ns, `..\`) || strings.HasPrefix(ns, `\`) ||
460		len(ns) >= 2 && ('A' <= ns[0] && ns[0] <= 'Z' || 'a' <= ns[0] && ns[0] <= 'z') && ns[1] == ':'
461}
462
463// MustQuote reports whether s must be quoted in order to appear as
464// a single token in a go.mod line.
465func MustQuote(s string) bool {
466	for _, r := range s {
467		switch r {
468		case ' ', '"', '\'', '`':
469			return true
470
471		case '(', ')', '[', ']', '{', '}', ',':
472			if len(s) > 1 {
473				return true
474			}
475
476		default:
477			if !unicode.IsPrint(r) {
478				return true
479			}
480		}
481	}
482	return s == "" || strings.Contains(s, "//") || strings.Contains(s, "/*")
483}
484
485// AutoQuote returns s or, if quoting is required for s to appear in a go.mod,
486// the quotation of s.
487func AutoQuote(s string) string {
488	if MustQuote(s) {
489		return strconv.Quote(s)
490	}
491	return s
492}
493
494func 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
541func parseString(s *string) (string, error) {
542	t := *s
543	if strings.HasPrefix(t, `"`) {
544		var err error
545		if t, err = strconv.Unquote(t); err != nil {
546			return "", err
547		}
548	} else if strings.ContainsAny(t, "\"'`") {
549		// Other quotes are reserved both for possible future expansion
550		// and to avoid confusion. For example if someone types 'x'
551		// we want that to be a syntax error and not a literal x in literal quotation marks.
552		return "", fmt.Errorf("unquoted string cannot contain quote")
553	}
554	*s = AutoQuote(t)
555	return t, nil
556}
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.
561func 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
579type ErrorList []Error
580
581func (e ErrorList) Error() string {
582	errStrs := make([]string, len(e))
583	for i, err := range e {
584		errStrs[i] = err.Error()
585	}
586	return strings.Join(errStrs, "\n")
587}
588
589type Error struct {
590	Filename string
591	Pos      Position
592	Verb     string
593	ModPath  string
594	Err      error
595}
596
597func (e *Error) Error() string {
598	var pos string
599	if e.Pos.LineRune > 1 {
600		// Don't print LineRune if it's 1 (beginning of line).
601		// It's always 1 except in scanner errors, which are rare.
602		pos = fmt.Sprintf("%s:%d:%d: ", e.Filename, e.Pos.Line, e.Pos.LineRune)
603	} else if e.Pos.Line > 0 {
604		pos = fmt.Sprintf("%s:%d: ", e.Filename, e.Pos.Line)
605	} else if e.Filename != "" {
606		pos = fmt.Sprintf("%s: ", e.Filename)
607	}
608
609	var directive string
610	if e.ModPath != "" {
611		directive = fmt.Sprintf("%s %s: ", e.Verb, e.ModPath)
612	} else if e.Verb != "" {
613		directive = fmt.Sprintf("%s: ", e.Verb)
614	}
615
616	return pos + directive + e.Err.Error()
617}
618
619func (e *Error) Unwrap() error { return e.Err }
620
621func parseVersion(verb string, path string, s *string, fix VersionFixer) (string, error) {
622	t, err := parseString(s)
623	if err != nil {
624		return "", &Error{
625			Verb:    verb,
626			ModPath: path,
627			Err: &module.InvalidVersionError{
628				Version: *s,
629				Err:     err,
630			},
631		}
632	}
633	if fix != nil {
634		var err error
635		t, err = fix(path, t)
636		if err != nil {
637			if err, ok := err.(*module.ModuleError); ok {
638				return "", &Error{
639					Verb:    verb,
640					ModPath: path,
641					Err:     err.Err,
642				}
643			}
644			return "", err
645		}
646	}
647	if v := module.CanonicalVersion(t); v != "" {
648		*s = v
649		return *s, nil
650	}
651	return "", &Error{
652		Verb:    verb,
653		ModPath: path,
654		Err: &module.InvalidVersionError{
655			Version: t,
656			Err:     errors.New("must be of the form v1.2.3"),
657		},
658	}
659}
660
661func modulePathMajor(path string) (string, error) {
662	_, major, ok := module.SplitPathVersion(path)
663	if !ok {
664		return "", fmt.Errorf("invalid module path")
665	}
666	return major, nil
667}
668
669func (f *File) Format() ([]byte, error) {
670	return Format(f.Syntax), nil
671}
672
673// Cleanup cleans up the file f after any edit operations.
674// To avoid quadratic behavior, modifications like DropRequire
675// clear the entry but do not remove it from the slice.
676// Cleanup cleans out all the cleared entries.
677func (f *File) Cleanup() {
678	w := 0
679	for _, r := range f.Require {
680		if r.Mod.Path != "" {
681			f.Require[w] = r
682			w++
683		}
684	}
685	f.Require = f.Require[:w]
686
687	w = 0
688	for _, x := range f.Exclude {
689		if x.Mod.Path != "" {
690			f.Exclude[w] = x
691			w++
692		}
693	}
694	f.Exclude = f.Exclude[:w]
695
696	w = 0
697	for _, r := range f.Replace {
698		if r.Old.Path != "" {
699			f.Replace[w] = r
700			w++
701		}
702	}
703	f.Replace = f.Replace[:w]
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
714	f.Syntax.Cleanup()
715}
716
717func (f *File) AddGoStmt(version string) error {
718	if !GoVersionRE.MatchString(version) {
719		return fmt.Errorf("invalid language version string %q", version)
720	}
721	if f.Go == nil {
722		var hint Expr
723		if f.Module != nil && f.Module.Syntax != nil {
724			hint = f.Module.Syntax
725		}
726		f.Go = &Go{
727			Version: version,
728			Syntax:  f.Syntax.addLine(hint, "go", version),
729		}
730	} else {
731		f.Go.Version = version
732		f.Syntax.updateLine(f.Go.Syntax, "go", version)
733	}
734	return nil
735}
736
737func (f *File) AddRequire(path, vers string) error {
738	need := true
739	for _, r := range f.Require {
740		if r.Mod.Path == path {
741			if need {
742				r.Mod.Version = vers
743				f.Syntax.updateLine(r.Syntax, "require", AutoQuote(path), vers)
744				need = false
745			} else {
746				f.Syntax.removeLine(r.Syntax)
747				*r = Require{}
748			}
749		}
750	}
751
752	if need {
753		f.AddNewRequire(path, vers, false)
754	}
755	return nil
756}
757
758func (f *File) AddNewRequire(path, vers string, indirect bool) {
759	line := f.Syntax.addLine(nil, "require", AutoQuote(path), vers)
760	setIndirect(line, indirect)
761	f.Require = append(f.Require, &Require{module.Version{Path: path, Version: vers}, indirect, line})
762}
763
764func (f *File) SetRequire(req []*Require) {
765	need := make(map[string]string)
766	indirect := make(map[string]bool)
767	for _, r := range req {
768		need[r.Mod.Path] = r.Mod.Version
769		indirect[r.Mod.Path] = r.Indirect
770	}
771
772	for _, r := range f.Require {
773		if v, ok := need[r.Mod.Path]; ok {
774			r.Mod.Version = v
775			r.Indirect = indirect[r.Mod.Path]
776		} else {
777			*r = Require{}
778		}
779	}
780
781	var newStmts []Expr
782	for _, stmt := range f.Syntax.Stmt {
783		switch stmt := stmt.(type) {
784		case *LineBlock:
785			if len(stmt.Token) > 0 && stmt.Token[0] == "require" {
786				var newLines []*Line
787				for _, line := range stmt.Line {
788					if p, err := parseString(&line.Token[0]); err == nil && need[p] != "" {
789						if len(line.Comments.Before) == 1 && len(line.Comments.Before[0].Token) == 0 {
790							line.Comments.Before = line.Comments.Before[:0]
791						}
792						line.Token[1] = need[p]
793						delete(need, p)
794						setIndirect(line, indirect[p])
795						newLines = append(newLines, line)
796					}
797				}
798				if len(newLines) == 0 {
799					continue // drop stmt
800				}
801				stmt.Line = newLines
802			}
803
804		case *Line:
805			if len(stmt.Token) > 0 && stmt.Token[0] == "require" {
806				if p, err := parseString(&stmt.Token[1]); err == nil && need[p] != "" {
807					stmt.Token[2] = need[p]
808					delete(need, p)
809					setIndirect(stmt, indirect[p])
810				} else {
811					continue // drop stmt
812				}
813			}
814		}
815		newStmts = append(newStmts, stmt)
816	}
817	f.Syntax.Stmt = newStmts
818
819	for path, vers := range need {
820		f.AddNewRequire(path, vers, indirect[path])
821	}
822	f.SortBlocks()
823}
824
825func (f *File) DropRequire(path string) error {
826	for _, r := range f.Require {
827		if r.Mod.Path == path {
828			f.Syntax.removeLine(r.Syntax)
829			*r = Require{}
830		}
831	}
832	return nil
833}
834
835func (f *File) AddExclude(path, vers string) error {
836	var hint *Line
837	for _, x := range f.Exclude {
838		if x.Mod.Path == path && x.Mod.Version == vers {
839			return nil
840		}
841		if x.Mod.Path == path {
842			hint = x.Syntax
843		}
844	}
845
846	f.Exclude = append(f.Exclude, &Exclude{Mod: module.Version{Path: path, Version: vers}, Syntax: f.Syntax.addLine(hint, "exclude", AutoQuote(path), vers)})
847	return nil
848}
849
850func (f *File) DropExclude(path, vers string) error {
851	for _, x := range f.Exclude {
852		if x.Mod.Path == path && x.Mod.Version == vers {
853			f.Syntax.removeLine(x.Syntax)
854			*x = Exclude{}
855		}
856	}
857	return nil
858}
859
860func (f *File) AddReplace(oldPath, oldVers, newPath, newVers string) error {
861	need := true
862	old := module.Version{Path: oldPath, Version: oldVers}
863	new := module.Version{Path: newPath, Version: newVers}
864	tokens := []string{"replace", AutoQuote(oldPath)}
865	if oldVers != "" {
866		tokens = append(tokens, oldVers)
867	}
868	tokens = append(tokens, "=>", AutoQuote(newPath))
869	if newVers != "" {
870		tokens = append(tokens, newVers)
871	}
872
873	var hint *Line
874	for _, r := range f.Replace {
875		if r.Old.Path == oldPath && (oldVers == "" || r.Old.Version == oldVers) {
876			if need {
877				// Found replacement for old; update to use new.
878				r.New = new
879				f.Syntax.updateLine(r.Syntax, tokens...)
880				need = false
881				continue
882			}
883			// Already added; delete other replacements for same.
884			f.Syntax.removeLine(r.Syntax)
885			*r = Replace{}
886		}
887		if r.Old.Path == oldPath {
888			hint = r.Syntax
889		}
890	}
891	if need {
892		f.Replace = append(f.Replace, &Replace{Old: old, New: new, Syntax: f.Syntax.addLine(hint, tokens...)})
893	}
894	return nil
895}
896
897func (f *File) DropReplace(oldPath, oldVers string) error {
898	for _, r := range f.Replace {
899		if r.Old.Path == oldPath && r.Old.Version == oldVers {
900			f.Syntax.removeLine(r.Syntax)
901			*r = Replace{}
902		}
903	}
904	return nil
905}
906
907func (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
925func (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
935func (f *File) SortBlocks() {
936	f.removeDups() // otherwise sorting is unsafe
937
938	for _, stmt := range f.Syntax.Stmt {
939		block, ok := stmt.(*LineBlock)
940		if !ok {
941			continue
942		}
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])
949		})
950	}
951}
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.
964func (f *File) removeDups() {
965	kill := make(map[*Line]bool)
966
967	// Remove duplicate excludes.
968	haveExclude := make(map[module.Version]bool)
969	for _, x := range f.Exclude {
970		if haveExclude[x.Mod] {
971			kill[x.Syntax] = true
972			continue
973		}
974		haveExclude[x.Mod] = true
975	}
976	var excl []*Exclude
977	for _, x := range f.Exclude {
978		if !kill[x.Syntax] {
979			excl = append(excl, x)
980		}
981	}
982	f.Exclude = excl
983
984	// Remove duplicate replacements.
985	// Later replacements take priority over earlier ones.
986	haveReplace := make(map[module.Version]bool)
987	for i := len(f.Replace) - 1; i >= 0; i-- {
988		x := f.Replace[i]
989		if haveReplace[x.Old] {
990			kill[x.Syntax] = true
991			continue
992		}
993		haveReplace[x.Old] = true
994	}
995	var repl []*Replace
996	for _, x := range f.Replace {
997		if !kill[x.Syntax] {
998			repl = append(repl, x)
999		}
1000	}
1001	f.Replace = repl
1002
1003	// Duplicate require and retract directives are not removed.
1004
1005	// Drop killed statements from the syntax tree.
1006	var stmts []Expr
1007	for _, stmt := range f.Syntax.Stmt {
1008		switch stmt := stmt.(type) {
1009		case *Line:
1010			if kill[stmt] {
1011				continue
1012			}
1013		case *LineBlock:
1014			var lines []*Line
1015			for _, line := range stmt.Line {
1016				if !kill[line] {
1017					lines = append(lines, line)
1018				}
1019			}
1020			stmt.Line = lines
1021			if len(lines) == 0 {
1022				continue
1023			}
1024		}
1025		stmts = append(stmts, stmt)
1026	}
1027	f.Syntax.Stmt = stmts
1028}
1029
1030// lineLess returns whether li should be sorted before lj. It sorts
1031// lexicographically without assigning any special meaning to tokens.
1032func 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.
1046func 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}
1064