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.
5 // Package modfile implements a parser and formatter for go.mod files.
7 // The go.mod syntax is described in
8 // https://golang.org/cmd/go/#hdr-The_go_mod_file.
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.
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.
18 // The Format function formats a File back to a byte slice which can be
31 "golang.org/x/mod/internal/lazyregexp"
32 "golang.org/x/mod/module"
33 "golang.org/x/mod/semver"
36 // A File is the parsed, interpreted form of a go.mod file.
48 // A Module is the module statement.
54 // A Go is the go statement.
56 Version string // "1.23"
60 // A Require is a single require statement.
63 Indirect bool // has "// indirect" comment
67 // An Exclude is a single exclude statement.
73 // A Replace is a single replace statement.
80 // A Retract is a single retract statement.
87 // A VersionInterval represents a range of versions with upper and lower bounds.
88 // Intervals are closed: both bounds are included. When Low is equal to High,
89 // the interval may refer to a single version ('v1.2.3') or an interval
90 // ('[v1.2.3, v1.2.3]'); both have the same representation.
91 type VersionInterval struct {
95 func (f *File) AddModuleStmt(path string) error {
97 f.Syntax = new(FileSyntax)
101 Mod: module.Version{Path: path},
102 Syntax: f.Syntax.addLine(nil, "module", AutoQuote(path)),
105 f.Module.Mod.Path = path
106 f.Syntax.updateLine(f.Module.Syntax, "module", AutoQuote(path))
111 func (f *File) AddComment(text string) {
113 f.Syntax = new(FileSyntax)
115 f.Syntax.Stmt = append(f.Syntax.Stmt, &CommentBlock{
126 type VersionFixer func(path, version string) (string, error)
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.
130 func Parse(file string, data []byte, fix VersionFixer) (*File, error) {
131 return parseToFile(file, data, fix, true)
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
141 func ParseLax(file string, data []byte, fix VersionFixer) (*File, error) {
142 return parseToFile(file, data, fix, false)
145 func parseToFile(file string, data []byte, fix VersionFixer, strict bool) (*File, error) {
146 fs, err := parse(file, data)
155 for _, x := range fs.Stmt {
156 switch x := x.(type) {
158 f.add(&errs, nil, x, x.Token[0], x.Token[1:], fix, strict)
161 if len(x.Token) > 1 {
163 errs = append(errs, Error{
166 Err: fmt.Errorf("unknown block type: %s", strings.Join(x.Token, " ")),
174 errs = append(errs, Error{
177 Err: fmt.Errorf("unknown block type: %s", strings.Join(x.Token, " ")),
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)
195 var GoVersionRE = lazyregexp.New(`^([1-9][0-9]*)\.(0|[1-9][0-9]*)$`)
197 func (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.
206 case "go", "module", "retract", "require":
207 // want these even for dependency go.mods
213 wrapModPathError := func(modPath string, err error) {
214 *errs = append(*errs, Error{
215 Filename: f.Syntax.Name,
222 wrapError := func(err error) {
223 *errs = append(*errs, Error{
224 Filename: f.Syntax.Name,
229 errorf := func(format string, args ...interface{}) {
230 wrapError(fmt.Errorf(format, args...))
235 errorf("unknown directive: %s", verb)
239 errorf("repeated go statement")
243 errorf("go directive expects exactly one argument")
245 } else if !GoVersionRE.MatchString(args[0]) {
246 errorf("invalid go version '%s': must match format 1.23", args[0])
250 f.Go = &Go{Syntax: line}
251 f.Go.Version = args[0]
255 errorf("repeated module statement")
258 f.Module = &Module{Syntax: line}
260 errorf("usage: module module/path")
263 s, err := parseString(&args[0])
265 errorf("invalid quoted string: %v", err)
268 f.Module.Mod = module.Version{Path: s}
270 case "require", "exclude":
272 errorf("usage: %s module/path v1.2.3", verb)
275 s, err := parseString(&args[0])
277 errorf("invalid quoted string: %v", err)
280 v, err := parseVersion(verb, s, &args[1], fix)
285 pathMajor, err := modulePathMajor(s)
290 if err := module.CheckPathMajor(v, pathMajor); err != nil {
291 wrapModPathError(s, err)
294 if verb == "require" {
295 f.Require = append(f.Require, &Require{
296 Mod: module.Version{Path: s, Version: v},
298 Indirect: isIndirect(line),
301 f.Exclude = append(f.Exclude, &Exclude{
302 Mod: module.Version{Path: s, Version: v},
309 if len(args) >= 2 && args[1] == "=>" {
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)
316 s, err := parseString(&args[0])
318 errorf("invalid quoted string: %v", err)
321 pathMajor, err := modulePathMajor(s)
323 wrapModPathError(s, err)
328 v, err = parseVersion(verb, s, &args[1], fix)
333 if err := module.CheckPathMajor(v, pathMajor); err != nil {
334 wrapModPathError(s, err)
338 ns, err := parseString(&args[arrow+1])
340 errorf("invalid quoted string: %v", err)
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 ../)")
349 if filepath.Separator == '/' && strings.Contains(ns, `\`) {
350 errorf("replacement directory appears to be Windows path (on a non-windows system)")
354 if len(args) == arrow+3 {
355 nv, err = parseVersion(verb, ns, &args[arrow+2], fix)
360 if IsDirectoryPath(ns) {
361 errorf("replacement module directory path %q cannot have version", ns)
365 f.Replace = append(f.Replace, &Replace{
366 Old: module.Version{Path: s, Version: v},
367 New: module.Version{Path: ns, Version: nv},
372 rationale := parseRetractRationale(block, line)
373 vi, err := parseVersionInterval(verb, &args, fix)
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.
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])
393 Rationale: rationale,
396 f.Retract = append(f.Retract, retract)
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.
404 func isIndirect(line *Line) bool {
405 if len(line.Suffix) == 0 {
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;")
412 // setIndirect sets line to have (or not have) a "// indirect" comment.
413 func setIndirect(line *Line, indirect bool) {
414 if isIndirect(line) == indirect {
419 if len(line.Suffix) == 0 {
421 line.Suffix = []Comment{{Token: "// indirect", Suffix: true}}
425 com := &line.Suffix[0]
426 text := strings.TrimSpace(strings.TrimPrefix(com.Token, string(slashSlash)))
429 com.Token = "// indirect"
433 // Insert at beginning of existing comment.
434 com.Token = "// indirect; " + text
439 f := strings.Fields(line.Suffix[0].Token)
441 // Remove whole comment.
446 // Remove comment prefix.
447 com := &line.Suffix[0]
448 i := strings.Index(com.Token, "indirect;")
449 com.Token = "//" + com.Token[i+len("indirect;"):]
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.
455 func 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] == ':'
463 // MustQuote reports whether s must be quoted in order to appear as
464 // a single token in a go.mod line.
465 func MustQuote(s string) bool {
466 for _, r := range s {
468 case ' ', '"', '\'', '`':
471 case '(', ')', '[', ']', '{', '}', ',':
477 if !unicode.IsPrint(r) {
482 return s == "" || strings.Contains(s, "//") || strings.Contains(s, "/*")
485 // AutoQuote returns s or, if quoting is required for s to appear in a go.mod,
486 // the quotation of s.
487 func AutoQuote(s string) string {
489 return strconv.Quote(s)
494 func parseVersionInterval(verb string, args *[]string, fix VersionFixer) (VersionInterval, error) {
496 if len(toks) == 0 || toks[0] == "(" {
497 return VersionInterval{}, fmt.Errorf("expected '[' or version")
500 v, err := parseVersion(verb, "", &toks[0], fix)
502 return VersionInterval{}, err
505 return VersionInterval{Low: v, High: v}, nil
510 return VersionInterval{}, fmt.Errorf("expected version after '['")
512 low, err := parseVersion(verb, "", &toks[0], fix)
514 return VersionInterval{}, err
518 if len(toks) == 0 || toks[0] != "," {
519 return VersionInterval{}, fmt.Errorf("expected ',' after version")
524 return VersionInterval{}, fmt.Errorf("expected version after ','")
526 high, err := parseVersion(verb, "", &toks[0], fix)
528 return VersionInterval{}, err
532 if len(toks) == 0 || toks[0] != "]" {
533 return VersionInterval{}, fmt.Errorf("expected ']' after version")
538 return VersionInterval{Low: low, High: high}, nil
541 func parseString(s *string) (string, error) {
543 if strings.HasPrefix(t, `"`) {
545 if t, err = strconv.Unquote(t); err != nil {
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")
558 // parseRetractRationale extracts the rationale for a retract directive from the
559 // surrounding comments. If the line does not have comments and is part of a
560 // block that does have comments, the block's comments are used.
561 func parseRetractRationale(block *LineBlock, line *Line) string {
562 comments := line.Comment()
563 if block != nil && len(comments.Before) == 0 && len(comments.Suffix) == 0 {
564 comments = block.Comment()
566 groups := [][]Comment{comments.Before, comments.Suffix}
568 for _, g := range groups {
569 for _, c := range g {
570 if !strings.HasPrefix(c.Token, "//") {
571 continue // blank line
573 lines = append(lines, strings.TrimSpace(strings.TrimPrefix(c.Token, "//")))
576 return strings.Join(lines, "\n")
579 type ErrorList []Error
581 func (e ErrorList) Error() string {
582 errStrs := make([]string, len(e))
583 for i, err := range e {
584 errStrs[i] = err.Error()
586 return strings.Join(errStrs, "\n")
597 func (e *Error) Error() 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)
611 directive = fmt.Sprintf("%s %s: ", e.Verb, e.ModPath)
612 } else if e.Verb != "" {
613 directive = fmt.Sprintf("%s: ", e.Verb)
616 return pos + directive + e.Err.Error()
619 func (e *Error) Unwrap() error { return e.Err }
621 func parseVersion(verb string, path string, s *string, fix VersionFixer) (string, error) {
622 t, err := parseString(s)
627 Err: &module.InvalidVersionError{
635 t, err = fix(path, t)
637 if err, ok := err.(*module.ModuleError); ok {
647 if v := module.CanonicalVersion(t); v != "" {
654 Err: &module.InvalidVersionError{
656 Err: errors.New("must be of the form v1.2.3"),
661 func modulePathMajor(path string) (string, error) {
662 _, major, ok := module.SplitPathVersion(path)
664 return "", fmt.Errorf("invalid module path")
669 func (f *File) Format() ([]byte, error) {
670 return Format(f.Syntax), nil
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.
677 func (f *File) Cleanup() {
679 for _, r := range f.Require {
680 if r.Mod.Path != "" {
685 f.Require = f.Require[:w]
688 for _, x := range f.Exclude {
689 if x.Mod.Path != "" {
694 f.Exclude = f.Exclude[:w]
697 for _, r := range f.Replace {
698 if r.Old.Path != "" {
703 f.Replace = f.Replace[:w]
706 for _, r := range f.Retract {
707 if r.Low != "" || r.High != "" {
712 f.Retract = f.Retract[:w]
717 func (f *File) AddGoStmt(version string) error {
718 if !GoVersionRE.MatchString(version) {
719 return fmt.Errorf("invalid language version string %q", version)
723 if f.Module != nil && f.Module.Syntax != nil {
724 hint = f.Module.Syntax
728 Syntax: f.Syntax.addLine(hint, "go", version),
731 f.Go.Version = version
732 f.Syntax.updateLine(f.Go.Syntax, "go", version)
737 func (f *File) AddRequire(path, vers string) error {
739 for _, r := range f.Require {
740 if r.Mod.Path == path {
743 f.Syntax.updateLine(r.Syntax, "require", AutoQuote(path), vers)
746 f.Syntax.removeLine(r.Syntax)
753 f.AddNewRequire(path, vers, false)
758 func (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})
764 func (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
772 for _, r := range f.Require {
773 if v, ok := need[r.Mod.Path]; ok {
775 r.Indirect = indirect[r.Mod.Path]
782 for _, stmt := range f.Syntax.Stmt {
783 switch stmt := stmt.(type) {
785 if len(stmt.Token) > 0 && stmt.Token[0] == "require" {
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]
792 line.Token[1] = need[p]
794 setIndirect(line, indirect[p])
795 newLines = append(newLines, line)
798 if len(newLines) == 0 {
799 continue // drop stmt
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]
809 setIndirect(stmt, indirect[p])
811 continue // drop stmt
815 newStmts = append(newStmts, stmt)
817 f.Syntax.Stmt = newStmts
819 for path, vers := range need {
820 f.AddNewRequire(path, vers, indirect[path])
825 func (f *File) DropRequire(path string) error {
826 for _, r := range f.Require {
827 if r.Mod.Path == path {
828 f.Syntax.removeLine(r.Syntax)
835 // AddExclude adds a exclude statement to the mod file. Errors if the provided
836 // version is not a canonical version string
837 func (f *File) AddExclude(path, vers string) error {
838 if !isCanonicalVersion(vers) {
839 return &module.InvalidVersionError{
841 Err: errors.New("must be of the form v1.2.3"),
846 for _, x := range f.Exclude {
847 if x.Mod.Path == path && x.Mod.Version == vers {
850 if x.Mod.Path == path {
855 f.Exclude = append(f.Exclude, &Exclude{Mod: module.Version{Path: path, Version: vers}, Syntax: f.Syntax.addLine(hint, "exclude", AutoQuote(path), vers)})
859 func (f *File) DropExclude(path, vers string) error {
860 for _, x := range f.Exclude {
861 if x.Mod.Path == path && x.Mod.Version == vers {
862 f.Syntax.removeLine(x.Syntax)
869 func (f *File) AddReplace(oldPath, oldVers, newPath, newVers string) error {
871 old := module.Version{Path: oldPath, Version: oldVers}
872 new := module.Version{Path: newPath, Version: newVers}
873 tokens := []string{"replace", AutoQuote(oldPath)}
875 tokens = append(tokens, oldVers)
877 tokens = append(tokens, "=>", AutoQuote(newPath))
879 tokens = append(tokens, newVers)
883 for _, r := range f.Replace {
884 if r.Old.Path == oldPath && (oldVers == "" || r.Old.Version == oldVers) {
886 // Found replacement for old; update to use new.
888 f.Syntax.updateLine(r.Syntax, tokens...)
892 // Already added; delete other replacements for same.
893 f.Syntax.removeLine(r.Syntax)
896 if r.Old.Path == oldPath {
901 f.Replace = append(f.Replace, &Replace{Old: old, New: new, Syntax: f.Syntax.addLine(hint, tokens...)})
906 func (f *File) DropReplace(oldPath, oldVers string) error {
907 for _, r := range f.Replace {
908 if r.Old.Path == oldPath && r.Old.Version == oldVers {
909 f.Syntax.removeLine(r.Syntax)
916 // AddRetract adds a retract statement to the mod file. Errors if the provided
917 // version interval does not consist of canonical version strings
918 func (f *File) AddRetract(vi VersionInterval, rationale string) error {
919 if !isCanonicalVersion(vi.High) {
920 return &module.InvalidVersionError{
922 Err: errors.New("must be of the form v1.2.3"),
925 if !isCanonicalVersion(vi.Low) {
926 return &module.InvalidVersionError{
928 Err: errors.New("must be of the form v1.2.3"),
935 if vi.Low == vi.High {
936 r.Syntax = f.Syntax.addLine(nil, "retract", AutoQuote(vi.Low))
938 r.Syntax = f.Syntax.addLine(nil, "retract", "[", AutoQuote(vi.Low), ",", AutoQuote(vi.High), "]")
941 for _, line := range strings.Split(rationale, "\n") {
942 com := Comment{Token: "// " + line}
943 r.Syntax.Comment().Before = append(r.Syntax.Comment().Before, com)
949 func (f *File) DropRetract(vi VersionInterval) error {
950 for _, r := range f.Retract {
951 if r.VersionInterval == vi {
952 f.Syntax.removeLine(r.Syntax)
959 func (f *File) SortBlocks() {
960 f.removeDups() // otherwise sorting is unsafe
962 for _, stmt := range f.Syntax.Stmt {
963 block, ok := stmt.(*LineBlock)
968 if block.Token[0] == "retract" {
969 less = lineRetractLess
971 sort.SliceStable(block.Line, func(i, j int) bool {
972 return less(block.Line[i], block.Line[j])
977 // removeDups removes duplicate exclude and replace directives.
979 // Earlier exclude directives take priority.
981 // Later replace directives take priority.
983 // require directives are not de-duplicated. That's left up to higher-level
986 // retract directives are not de-duplicated since comments are
987 // meaningful, and versions may be retracted multiple times.
988 func (f *File) removeDups() {
989 kill := make(map[*Line]bool)
991 // Remove duplicate excludes.
992 haveExclude := make(map[module.Version]bool)
993 for _, x := range f.Exclude {
994 if haveExclude[x.Mod] {
995 kill[x.Syntax] = true
998 haveExclude[x.Mod] = true
1001 for _, x := range f.Exclude {
1002 if !kill[x.Syntax] {
1003 excl = append(excl, x)
1008 // Remove duplicate replacements.
1009 // Later replacements take priority over earlier ones.
1010 haveReplace := make(map[module.Version]bool)
1011 for i := len(f.Replace) - 1; i >= 0; i-- {
1013 if haveReplace[x.Old] {
1014 kill[x.Syntax] = true
1017 haveReplace[x.Old] = true
1020 for _, x := range f.Replace {
1021 if !kill[x.Syntax] {
1022 repl = append(repl, x)
1027 // Duplicate require and retract directives are not removed.
1029 // Drop killed statements from the syntax tree.
1031 for _, stmt := range f.Syntax.Stmt {
1032 switch stmt := stmt.(type) {
1039 for _, line := range stmt.Line {
1041 lines = append(lines, line)
1045 if len(lines) == 0 {
1049 stmts = append(stmts, stmt)
1051 f.Syntax.Stmt = stmts
1054 // lineLess returns whether li should be sorted before lj. It sorts
1055 // lexicographically without assigning any special meaning to tokens.
1056 func lineLess(li, lj *Line) bool {
1057 for k := 0; k < len(li.Token) && k < len(lj.Token); k++ {
1058 if li.Token[k] != lj.Token[k] {
1059 return li.Token[k] < lj.Token[k]
1062 return len(li.Token) < len(lj.Token)
1065 // lineRetractLess returns whether li should be sorted before lj for lines in
1066 // a "retract" block. It treats each line as a version interval. Single versions
1067 // are compared as if they were intervals with the same low and high version.
1068 // Intervals are sorted in descending order, first by low version, then by
1069 // high version, using semver.Compare.
1070 func lineRetractLess(li, lj *Line) bool {
1071 interval := func(l *Line) VersionInterval {
1072 if len(l.Token) == 1 {
1073 return VersionInterval{Low: l.Token[0], High: l.Token[0]}
1074 } else if len(l.Token) == 5 && l.Token[0] == "[" && l.Token[2] == "," && l.Token[4] == "]" {
1075 return VersionInterval{Low: l.Token[1], High: l.Token[3]}
1077 // Line in unknown format. Treat as an invalid version.
1078 return VersionInterval{}
1083 if cmp := semver.Compare(vii.Low, vij.Low); cmp != 0 {
1086 return semver.Compare(vii.High, vij.High) > 0
1089 // isCanonicalVersion tests if the provided version string represents a valid
1090 // canonical version.
1091 func isCanonicalVersion(vers string) bool {
1092 return vers != "" && semver.Canonical(vers) == vers