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.
18 "github.com/rogpeppe/go-internal/module"
19 "github.com/rogpeppe/go-internal/semver"
22 // A File is the parsed, interpreted form of a go.mod file.
33 // A Module is the module statement.
39 // A Go is the go statement.
41 Version string // "1.23"
45 // A Require is a single require statement.
48 Indirect bool // has "// indirect" comment
52 // An Exclude is a single exclude statement.
58 // A Replace is a single replace statement.
65 func (f *File) AddModuleStmt(path string) error {
67 f.Syntax = new(FileSyntax)
71 Mod: module.Version{Path: path},
72 Syntax: f.Syntax.addLine(nil, "module", AutoQuote(path)),
75 f.Module.Mod.Path = path
76 f.Syntax.updateLine(f.Module.Syntax, "module", AutoQuote(path))
81 func (f *File) AddComment(text string) {
83 f.Syntax = new(FileSyntax)
85 f.Syntax.Stmt = append(f.Syntax.Stmt, &CommentBlock{
96 type VersionFixer func(path, version string) (string, error)
98 // Parse parses the data, reported in errors as being from file,
99 // into a File struct. It applies fix, if non-nil, to canonicalize all module versions found.
100 func Parse(file string, data []byte, fix VersionFixer) (*File, error) {
101 return parseToFile(file, data, fix, true)
104 // ParseLax is like Parse but ignores unknown statements.
105 // It is used when parsing go.mod files other than the main module,
106 // under the theory that most statement types we add in the future will
107 // only apply in the main module, like exclude and replace,
108 // and so we get better gradual deployments if old go commands
109 // simply ignore those statements when found in go.mod files
111 func ParseLax(file string, data []byte, fix VersionFixer) (*File, error) {
112 return parseToFile(file, data, fix, false)
115 func parseToFile(file string, data []byte, fix VersionFixer, strict bool) (*File, error) {
116 fs, err := parse(file, data)
124 var errs bytes.Buffer
125 for _, x := range fs.Stmt {
126 switch x := x.(type) {
128 f.add(&errs, x, x.Token[0], x.Token[1:], fix, strict)
131 if len(x.Token) > 1 {
133 fmt.Fprintf(&errs, "%s:%d: unknown block type: %s\n", file, x.Start.Line, strings.Join(x.Token, " "))
140 fmt.Fprintf(&errs, "%s:%d: unknown block type: %s\n", file, x.Start.Line, strings.Join(x.Token, " "))
143 case "module", "require", "exclude", "replace":
144 for _, l := range x.Line {
145 f.add(&errs, l, x.Token[0], l.Token, fix, strict)
152 return nil, errors.New(strings.TrimRight(errs.String(), "\n"))
157 var goVersionRE = regexp.MustCompile(`([1-9][0-9]*)\.(0|[1-9][0-9]*)`)
159 func (f *File) add(errs *bytes.Buffer, line *Line, verb string, args []string, fix VersionFixer, strict bool) {
160 // If strict is false, this module is a dependency.
161 // We ignore all unknown directives as well as main-module-only
162 // directives like replace and exclude. It will work better for
163 // forward compatibility if we can depend on modules that have unknown
164 // statements (presumed relevant only when acting as the main module)
165 // and simply ignore those statements.
168 case "module", "require", "go":
169 // want these even for dependency go.mods
177 fmt.Fprintf(errs, "%s:%d: unknown directive: %s\n", f.Syntax.Name, line.Start.Line, verb)
181 fmt.Fprintf(errs, "%s:%d: repeated go statement\n", f.Syntax.Name, line.Start.Line)
184 if len(args) != 1 || !goVersionRE.MatchString(args[0]) {
185 fmt.Fprintf(errs, "%s:%d: usage: go 1.23\n", f.Syntax.Name, line.Start.Line)
188 f.Go = &Go{Syntax: line}
189 f.Go.Version = args[0]
192 fmt.Fprintf(errs, "%s:%d: repeated module statement\n", f.Syntax.Name, line.Start.Line)
195 f.Module = &Module{Syntax: line}
198 fmt.Fprintf(errs, "%s:%d: usage: module module/path [version]\n", f.Syntax.Name, line.Start.Line)
201 s, err := parseString(&args[0])
203 fmt.Fprintf(errs, "%s:%d: invalid quoted string: %v\n", f.Syntax.Name, line.Start.Line, err)
206 f.Module.Mod = module.Version{Path: s}
207 case "require", "exclude":
209 fmt.Fprintf(errs, "%s:%d: usage: %s module/path v1.2.3\n", f.Syntax.Name, line.Start.Line, verb)
212 s, err := parseString(&args[0])
214 fmt.Fprintf(errs, "%s:%d: invalid quoted string: %v\n", f.Syntax.Name, line.Start.Line, err)
218 v, err := parseVersion(s, &args[1], fix)
220 fmt.Fprintf(errs, "%s:%d: invalid module version %q: %v\n", f.Syntax.Name, line.Start.Line, old, err)
223 pathMajor, err := modulePathMajor(s)
225 fmt.Fprintf(errs, "%s:%d: %v\n", f.Syntax.Name, line.Start.Line, err)
228 if !module.MatchPathMajor(v, pathMajor) {
230 pathMajor = "v0 or v1"
232 fmt.Fprintf(errs, "%s:%d: invalid module: %s should be %s, not %s (%s)\n", f.Syntax.Name, line.Start.Line, s, pathMajor, semver.Major(v), v)
235 if verb == "require" {
236 f.Require = append(f.Require, &Require{
237 Mod: module.Version{Path: s, Version: v},
239 Indirect: isIndirect(line),
242 f.Exclude = append(f.Exclude, &Exclude{
243 Mod: module.Version{Path: s, Version: v},
249 if len(args) >= 2 && args[1] == "=>" {
252 if len(args) < arrow+2 || len(args) > arrow+3 || args[arrow] != "=>" {
253 fmt.Fprintf(errs, "%s:%d: usage: %s module/path [v1.2.3] => other/module v1.4\n\t or %s module/path [v1.2.3] => ../local/directory\n", f.Syntax.Name, line.Start.Line, verb, verb)
256 s, err := parseString(&args[0])
258 fmt.Fprintf(errs, "%s:%d: invalid quoted string: %v\n", f.Syntax.Name, line.Start.Line, err)
261 pathMajor, err := modulePathMajor(s)
263 fmt.Fprintf(errs, "%s:%d: %v\n", f.Syntax.Name, line.Start.Line, err)
269 v, err = parseVersion(s, &args[1], fix)
271 fmt.Fprintf(errs, "%s:%d: invalid module version %v: %v\n", f.Syntax.Name, line.Start.Line, old, err)
274 if !module.MatchPathMajor(v, pathMajor) {
276 pathMajor = "v0 or v1"
278 fmt.Fprintf(errs, "%s:%d: invalid module: %s should be %s, not %s (%s)\n", f.Syntax.Name, line.Start.Line, s, pathMajor, semver.Major(v), v)
282 ns, err := parseString(&args[arrow+1])
284 fmt.Fprintf(errs, "%s:%d: invalid quoted string: %v\n", f.Syntax.Name, line.Start.Line, err)
288 if len(args) == arrow+2 {
289 if !IsDirectoryPath(ns) {
290 fmt.Fprintf(errs, "%s:%d: replacement module without version must be directory path (rooted or starting with ./ or ../)\n", f.Syntax.Name, line.Start.Line)
293 if filepath.Separator == '/' && strings.Contains(ns, `\`) {
294 fmt.Fprintf(errs, "%s:%d: replacement directory appears to be Windows path (on a non-windows system)\n", f.Syntax.Name, line.Start.Line)
298 if len(args) == arrow+3 {
300 nv, err = parseVersion(ns, &args[arrow+2], fix)
302 fmt.Fprintf(errs, "%s:%d: invalid module version %v: %v\n", f.Syntax.Name, line.Start.Line, old, err)
305 if IsDirectoryPath(ns) {
306 fmt.Fprintf(errs, "%s:%d: replacement module directory path %q cannot have version\n", f.Syntax.Name, line.Start.Line, ns)
310 f.Replace = append(f.Replace, &Replace{
311 Old: module.Version{Path: s, Version: v},
312 New: module.Version{Path: ns, Version: nv},
318 // isIndirect reports whether line has a "// indirect" comment,
319 // meaning it is in go.mod only for its effect on indirect dependencies,
320 // so that it can be dropped entirely once the effective version of the
321 // indirect dependency reaches the given minimum version.
322 func isIndirect(line *Line) bool {
323 if len(line.Suffix) == 0 {
326 f := strings.Fields(line.Suffix[0].Token)
327 return (len(f) == 2 && f[1] == "indirect" || len(f) > 2 && f[1] == "indirect;") && f[0] == "//"
330 // setIndirect sets line to have (or not have) a "// indirect" comment.
331 func setIndirect(line *Line, indirect bool) {
332 if isIndirect(line) == indirect {
337 if len(line.Suffix) == 0 {
339 line.Suffix = []Comment{{Token: "// indirect", Suffix: true}}
342 // Insert at beginning of existing comment.
343 com := &line.Suffix[0]
345 if len(com.Token) > 2 && com.Token[2] == ' ' || com.Token[2] == '\t' {
348 com.Token = "// indirect;" + space + com.Token[2:]
353 f := strings.Fields(line.Suffix[0].Token)
355 // Remove whole comment.
360 // Remove comment prefix.
361 com := &line.Suffix[0]
362 i := strings.Index(com.Token, "indirect;")
363 com.Token = "//" + com.Token[i+len("indirect;"):]
366 // IsDirectoryPath reports whether the given path should be interpreted
367 // as a directory path. Just like on the go command line, relative paths
368 // and rooted paths are directory paths; the rest are module paths.
369 func IsDirectoryPath(ns string) bool {
370 // Because go.mod files can move from one system to another,
371 // we check all known path syntaxes, both Unix and Windows.
372 return strings.HasPrefix(ns, "./") || strings.HasPrefix(ns, "../") || strings.HasPrefix(ns, "/") ||
373 strings.HasPrefix(ns, `.\`) || strings.HasPrefix(ns, `..\`) || strings.HasPrefix(ns, `\`) ||
374 len(ns) >= 2 && ('A' <= ns[0] && ns[0] <= 'Z' || 'a' <= ns[0] && ns[0] <= 'z') && ns[1] == ':'
377 // MustQuote reports whether s must be quoted in order to appear as
378 // a single token in a go.mod line.
379 func MustQuote(s string) bool {
380 for _, r := range s {
381 if !unicode.IsPrint(r) || r == ' ' || r == '"' || r == '\'' || r == '`' {
385 return s == "" || strings.Contains(s, "//") || strings.Contains(s, "/*")
388 // AutoQuote returns s or, if quoting is required for s to appear in a go.mod,
389 // the quotation of s.
390 func AutoQuote(s string) string {
392 return strconv.Quote(s)
397 func parseString(s *string) (string, error) {
399 if strings.HasPrefix(t, `"`) {
401 if t, err = strconv.Unquote(t); err != nil {
404 } else if strings.ContainsAny(t, "\"'`") {
405 // Other quotes are reserved both for possible future expansion
406 // and to avoid confusion. For example if someone types 'x'
407 // we want that to be a syntax error and not a literal x in literal quotation marks.
408 return "", fmt.Errorf("unquoted string cannot contain quote")
414 func parseVersion(path string, s *string, fix VersionFixer) (string, error) {
415 t, err := parseString(s)
421 t, err = fix(path, t)
426 if v := module.CanonicalVersion(t); v != "" {
430 return "", fmt.Errorf("version must be of the form v1.2.3")
433 func modulePathMajor(path string) (string, error) {
434 _, major, ok := module.SplitPathVersion(path)
436 return "", fmt.Errorf("invalid module path")
441 func (f *File) Format() ([]byte, error) {
442 return Format(f.Syntax), nil
445 // Cleanup cleans up the file f after any edit operations.
446 // To avoid quadratic behavior, modifications like DropRequire
447 // clear the entry but do not remove it from the slice.
448 // Cleanup cleans out all the cleared entries.
449 func (f *File) Cleanup() {
451 for _, r := range f.Require {
452 if r.Mod.Path != "" {
457 f.Require = f.Require[:w]
460 for _, x := range f.Exclude {
461 if x.Mod.Path != "" {
466 f.Exclude = f.Exclude[:w]
469 for _, r := range f.Replace {
470 if r.Old.Path != "" {
475 f.Replace = f.Replace[:w]
480 func (f *File) AddRequire(path, vers string) error {
482 for _, r := range f.Require {
483 if r.Mod.Path == path {
486 f.Syntax.updateLine(r.Syntax, "require", AutoQuote(path), vers)
489 f.Syntax.removeLine(r.Syntax)
496 f.AddNewRequire(path, vers, false)
501 func (f *File) AddNewRequire(path, vers string, indirect bool) {
502 line := f.Syntax.addLine(nil, "require", AutoQuote(path), vers)
503 setIndirect(line, indirect)
504 f.Require = append(f.Require, &Require{module.Version{Path: path, Version: vers}, indirect, line})
507 func (f *File) SetRequire(req []*Require) {
508 need := make(map[string]string)
509 indirect := make(map[string]bool)
510 for _, r := range req {
511 need[r.Mod.Path] = r.Mod.Version
512 indirect[r.Mod.Path] = r.Indirect
515 for _, r := range f.Require {
516 if v, ok := need[r.Mod.Path]; ok {
518 r.Indirect = indirect[r.Mod.Path]
523 for _, stmt := range f.Syntax.Stmt {
524 switch stmt := stmt.(type) {
526 if len(stmt.Token) > 0 && stmt.Token[0] == "require" {
528 for _, line := range stmt.Line {
529 if p, err := parseString(&line.Token[0]); err == nil && need[p] != "" {
530 line.Token[1] = need[p]
532 setIndirect(line, indirect[p])
533 newLines = append(newLines, line)
536 if len(newLines) == 0 {
537 continue // drop stmt
543 if len(stmt.Token) > 0 && stmt.Token[0] == "require" {
544 if p, err := parseString(&stmt.Token[1]); err == nil && need[p] != "" {
545 stmt.Token[2] = need[p]
547 setIndirect(stmt, indirect[p])
549 continue // drop stmt
553 newStmts = append(newStmts, stmt)
555 f.Syntax.Stmt = newStmts
557 for path, vers := range need {
558 f.AddNewRequire(path, vers, indirect[path])
563 func (f *File) DropRequire(path string) error {
564 for _, r := range f.Require {
565 if r.Mod.Path == path {
566 f.Syntax.removeLine(r.Syntax)
573 func (f *File) AddExclude(path, vers string) error {
575 for _, x := range f.Exclude {
576 if x.Mod.Path == path && x.Mod.Version == vers {
579 if x.Mod.Path == path {
584 f.Exclude = append(f.Exclude, &Exclude{Mod: module.Version{Path: path, Version: vers}, Syntax: f.Syntax.addLine(hint, "exclude", AutoQuote(path), vers)})
588 func (f *File) DropExclude(path, vers string) error {
589 for _, x := range f.Exclude {
590 if x.Mod.Path == path && x.Mod.Version == vers {
591 f.Syntax.removeLine(x.Syntax)
598 func (f *File) AddReplace(oldPath, oldVers, newPath, newVers string) error {
600 old := module.Version{Path: oldPath, Version: oldVers}
601 new := module.Version{Path: newPath, Version: newVers}
602 tokens := []string{"replace", AutoQuote(oldPath)}
604 tokens = append(tokens, oldVers)
606 tokens = append(tokens, "=>", AutoQuote(newPath))
608 tokens = append(tokens, newVers)
612 for _, r := range f.Replace {
613 if r.Old.Path == oldPath && (oldVers == "" || r.Old.Version == oldVers) {
615 // Found replacement for old; update to use new.
617 f.Syntax.updateLine(r.Syntax, tokens...)
621 // Already added; delete other replacements for same.
622 f.Syntax.removeLine(r.Syntax)
625 if r.Old.Path == oldPath {
630 f.Replace = append(f.Replace, &Replace{Old: old, New: new, Syntax: f.Syntax.addLine(hint, tokens...)})
635 func (f *File) DropReplace(oldPath, oldVers string) error {
636 for _, r := range f.Replace {
637 if r.Old.Path == oldPath && r.Old.Version == oldVers {
638 f.Syntax.removeLine(r.Syntax)
645 func (f *File) SortBlocks() {
646 f.removeDups() // otherwise sorting is unsafe
648 for _, stmt := range f.Syntax.Stmt {
649 block, ok := stmt.(*LineBlock)
653 sort.Slice(block.Line, func(i, j int) bool {
656 for k := 0; k < len(li.Token) && k < len(lj.Token); k++ {
657 if li.Token[k] != lj.Token[k] {
658 return li.Token[k] < lj.Token[k]
661 return len(li.Token) < len(lj.Token)
666 func (f *File) removeDups() {
667 have := make(map[module.Version]bool)
668 kill := make(map[*Line]bool)
669 for _, x := range f.Exclude {
671 kill[x.Syntax] = true
677 for _, x := range f.Exclude {
679 excl = append(excl, x)
684 have = make(map[module.Version]bool)
685 // Later replacements take priority over earlier ones.
686 for i := len(f.Replace) - 1; i >= 0; i-- {
689 kill[x.Syntax] = true
695 for _, x := range f.Replace {
697 repl = append(repl, x)
703 for _, stmt := range f.Syntax.Stmt {
704 switch stmt := stmt.(type) {
711 for _, line := range stmt.Line {
713 lines = append(lines, line)
721 stmts = append(stmts, stmt)
723 f.Syntax.Stmt = stmts