// Copyright 2018 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package modfile implements a parser and formatter for go.mod files. // // The go.mod syntax is described in // https://pkg.go.dev/cmd/go/#hdr-The_go_mod_file. // // The [Parse] and [ParseLax] functions both parse a go.mod file and return an // abstract syntax tree. ParseLax ignores unknown statements and may be used to // parse go.mod files that may have been developed with newer versions of Go. // // The [File] struct returned by Parse and ParseLax represent an abstract // go.mod file. File has several methods like [File.AddNewRequire] and // [File.DropReplace] that can be used to programmatically edit a file. // // The [Format] function formats a File back to a byte slice which can be // written to a file. package modfile import ( "errors" "fmt" "path/filepath" "sort" "strconv" "strings" "unicode" "golang.org/x/mod/internal/lazyregexp" "golang.org/x/mod/module" "golang.org/x/mod/semver" ) // A File is the parsed, interpreted form of a go.mod file. type File struct { Module *Module Go *Go Toolchain *Toolchain Require []*Require Exclude []*Exclude Replace []*Replace Retract []*Retract Syntax *FileSyntax } // A Module is the module statement. type Module struct { Mod module.Version Deprecated string Syntax *Line } // A Go is the go statement. type Go struct { Version string // "1.23" Syntax *Line } // A Toolchain is the toolchain statement. type Toolchain struct { Name string // "go1.21rc1" Syntax *Line } // An Exclude is a single exclude statement. type Exclude struct { Mod module.Version Syntax *Line } // A Replace is a single replace statement. type Replace struct { Old module.Version New module.Version Syntax *Line } // A Retract is a single retract statement. type Retract struct { VersionInterval Rationale string Syntax *Line } // A VersionInterval represents a range of versions with upper and lower bounds. // Intervals are closed: both bounds are included. When Low is equal to High, // the interval may refer to a single version ('v1.2.3') or an interval // ('[v1.2.3, v1.2.3]'); both have the same representation. type VersionInterval struct { Low, High string } // A Require is a single require statement. type Require struct { Mod module.Version Indirect bool // has "// indirect" comment Syntax *Line } func (r *Require) markRemoved() { r.Syntax.markRemoved() *r = Require{} } func (r *Require) setVersion(v string) { r.Mod.Version = v if line := r.Syntax; len(line.Token) > 0 { if line.InBlock { // If the line is preceded by an empty line, remove it; see // https://golang.org/issue/33779. if len(line.Comments.Before) == 1 && len(line.Comments.Before[0].Token) == 0 { line.Comments.Before = line.Comments.Before[:0] } if len(line.Token) >= 2 { // example.com v1.2.3 line.Token[1] = v } } else { if len(line.Token) >= 3 { // require example.com v1.2.3 line.Token[2] = v } } } } // setIndirect sets line to have (or not have) a "// indirect" comment. func (r *Require) setIndirect(indirect bool) { r.Indirect = indirect line := r.Syntax if isIndirect(line) == indirect { return } if indirect { // Adding comment. if len(line.Suffix) == 0 { // New comment. line.Suffix = []Comment{{Token: "// indirect", Suffix: true}} return } com := &line.Suffix[0] text := strings.TrimSpace(strings.TrimPrefix(com.Token, string(slashSlash))) if text == "" { // Empty comment. com.Token = "// indirect" return } // Insert at beginning of existing comment. com.Token = "// indirect; " + text return } // Removing comment. f := strings.TrimSpace(strings.TrimPrefix(line.Suffix[0].Token, string(slashSlash))) if f == "indirect" { // Remove whole comment. line.Suffix = nil return } // Remove comment prefix. com := &line.Suffix[0] i := strings.Index(com.Token, "indirect;") com.Token = "//" + com.Token[i+len("indirect;"):] } // isIndirect reports whether line has a "// indirect" comment, // meaning it is in go.mod only for its effect on indirect dependencies, // so that it can be dropped entirely once the effective version of the // indirect dependency reaches the given minimum version. func isIndirect(line *Line) bool { if len(line.Suffix) == 0 { return false } f := strings.Fields(strings.TrimPrefix(line.Suffix[0].Token, string(slashSlash))) return (len(f) == 1 && f[0] == "indirect" || len(f) > 1 && f[0] == "indirect;") } func (f *File) AddModuleStmt(path string) error { if f.Syntax == nil { f.Syntax = new(FileSyntax) } if f.Module == nil { f.Module = &Module{ Mod: module.Version{Path: path}, Syntax: f.Syntax.addLine(nil, "module", AutoQuote(path)), } } else { f.Module.Mod.Path = path f.Syntax.updateLine(f.Module.Syntax, "module", AutoQuote(path)) } return nil } func (f *File) AddComment(text string) { if f.Syntax == nil { f.Syntax = new(FileSyntax) } f.Syntax.Stmt = append(f.Syntax.Stmt, &CommentBlock{ Comments: Comments{ Before: []Comment{ { Token: text, }, }, }, }) } type VersionFixer func(path, version string) (string, error) // errDontFix is returned by a VersionFixer to indicate the version should be // left alone, even if it's not canonical. var dontFixRetract VersionFixer = func(_, vers string) (string, error) { return vers, nil } // Parse parses and returns a go.mod file. // // file is the name of the file, used in positions and errors. // // data is the content of the file. // // fix is an optional function that canonicalizes module versions. // If fix is nil, all module versions must be canonical ([module.CanonicalVersion] // must return the same string). func Parse(file string, data []byte, fix VersionFixer) (*File, error) { return parseToFile(file, data, fix, true) } // ParseLax is like Parse but ignores unknown statements. // It is used when parsing go.mod files other than the main module, // under the theory that most statement types we add in the future will // only apply in the main module, like exclude and replace, // and so we get better gradual deployments if old go commands // simply ignore those statements when found in go.mod files // in dependencies. func ParseLax(file string, data []byte, fix VersionFixer) (*File, error) { return parseToFile(file, data, fix, false) } func parseToFile(file string, data []byte, fix VersionFixer, strict bool) (parsed *File, err error) { fs, err := parse(file, data) if err != nil { return nil, err } f := &File{ Syntax: fs, } var errs ErrorList // fix versions in retract directives after the file is parsed. // We need the module path to fix versions, and it might be at the end. defer func() { oldLen := len(errs) f.fixRetract(fix, &errs) if len(errs) > oldLen { parsed, err = nil, errs } }() for _, x := range fs.Stmt { switch x := x.(type) { case *Line: f.add(&errs, nil, x, x.Token[0], x.Token[1:], fix, strict) case *LineBlock: if len(x.Token) > 1 { if strict { errs = append(errs, Error{ Filename: file, Pos: x.Start, Err: fmt.Errorf("unknown block type: %s", strings.Join(x.Token, " ")), }) } continue } switch x.Token[0] { default: if strict { errs = append(errs, Error{ Filename: file, Pos: x.Start, Err: fmt.Errorf("unknown block type: %s", strings.Join(x.Token, " ")), }) } continue case "module", "require", "exclude", "replace", "retract": for _, l := range x.Line { f.add(&errs, x, l, x.Token[0], l.Token, fix, strict) } } } } if len(errs) > 0 { return nil, errs } return f, nil } var GoVersionRE = lazyregexp.New(`^([1-9][0-9]*)\.(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))?([a-z]+[0-9]+)?$`) var laxGoVersionRE = lazyregexp.New(`^v?(([1-9][0-9]*)\.(0|[1-9][0-9]*))([^0-9].*)$`) // Toolchains must be named beginning with `go1`, // like "go1.20.3" or "go1.20.3-gccgo". As a special case, "default" is also permitted. var ToolchainRE = lazyregexp.New(`^default$|^go1($|\.)`) func (f *File) add(errs *ErrorList, block *LineBlock, line *Line, verb string, args []string, fix VersionFixer, strict bool) { // If strict is false, this module is a dependency. // We ignore all unknown directives as well as main-module-only // directives like replace and exclude. It will work better for // forward compatibility if we can depend on modules that have unknown // statements (presumed relevant only when acting as the main module) // and simply ignore those statements. if !strict { switch verb { case "go", "module", "retract", "require": // want these even for dependency go.mods default: return } } wrapModPathError := func(modPath string, err error) { *errs = append(*errs, Error{ Filename: f.Syntax.Name, Pos: line.Start, ModPath: modPath, Verb: verb, Err: err, }) } wrapError := func(err error) { *errs = append(*errs, Error{ Filename: f.Syntax.Name, Pos: line.Start, Err: err, }) } errorf := func(format string, args ...interface{}) { wrapError(fmt.Errorf(format, args...)) } switch verb { default: errorf("unknown directive: %s", verb) case "go": if f.Go != nil { errorf("repeated go statement") return } if len(args) != 1 { errorf("go directive expects exactly one argument") return } else if !GoVersionRE.MatchString(args[0]) { fixed := false if !strict { if m := laxGoVersionRE.FindStringSubmatch(args[0]); m != nil { args[0] = m[1] fixed = true } } if !fixed { errorf("invalid go version '%s': must match format 1.23", args[0]) return } } f.Go = &Go{Syntax: line} f.Go.Version = args[0] case "toolchain": if f.Toolchain != nil { errorf("repeated toolchain statement") return } if len(args) != 1 { errorf("toolchain directive expects exactly one argument") return } else if strict && !ToolchainRE.MatchString(args[0]) { errorf("invalid toolchain version '%s': must match format go1.23 or local", args[0]) return } f.Toolchain = &Toolchain{Syntax: line} f.Toolchain.Name = args[0] case "module": if f.Module != nil { errorf("repeated module statement") return } deprecated := parseDeprecation(block, line) f.Module = &Module{ Syntax: line, Deprecated: deprecated, } if len(args) != 1 { errorf("usage: module module/path") return } s, err := parseString(&args[0]) if err != nil { errorf("invalid quoted string: %v", err) return } f.Module.Mod = module.Version{Path: s} case "require", "exclude": if len(args) != 2 { errorf("usage: %s module/path v1.2.3", verb) return } s, err := parseString(&args[0]) if err != nil { errorf("invalid quoted string: %v", err) return } v, err := parseVersion(verb, s, &args[1], fix) if err != nil { wrapError(err) return } pathMajor, err := modulePathMajor(s) if err != nil { wrapError(err) return } if err := module.CheckPathMajor(v, pathMajor); err != nil { wrapModPathError(s, err) return } if verb == "require" { f.Require = append(f.Require, &Require{ Mod: module.Version{Path: s, Version: v}, Syntax: line, Indirect: isIndirect(line), }) } else { f.Exclude = append(f.Exclude, &Exclude{ Mod: module.Version{Path: s, Version: v}, Syntax: line, }) } case "replace": replace, wrappederr := parseReplace(f.Syntax.Name, line, verb, args, fix) if wrappederr != nil { *errs = append(*errs, *wrappederr) return } f.Replace = append(f.Replace, replace) case "retract": rationale := parseDirectiveComment(block, line) vi, err := parseVersionInterval(verb, "", &args, dontFixRetract) if err != nil { if strict { wrapError(err) return } else { // Only report errors parsing intervals in the main module. We may // support additional syntax in the future, such as open and half-open // intervals. Those can't be supported now, because they break the // go.mod parser, even in lax mode. return } } if len(args) > 0 && strict { // In the future, there may be additional information after the version. errorf("unexpected token after version: %q", args[0]) return } retract := &Retract{ VersionInterval: vi, Rationale: rationale, Syntax: line, } f.Retract = append(f.Retract, retract) } } func parseReplace(filename string, line *Line, verb string, args []string, fix VersionFixer) (*Replace, *Error) { wrapModPathError := func(modPath string, err error) *Error { return &Error{ Filename: filename, Pos: line.Start, ModPath: modPath, Verb: verb, Err: err, } } wrapError := func(err error) *Error { return &Error{ Filename: filename, Pos: line.Start, Err: err, } } errorf := func(format string, args ...interface{}) *Error { return wrapError(fmt.Errorf(format, args...)) } arrow := 2 if len(args) >= 2 && args[1] == "=>" { arrow = 1 } if len(args) < arrow+2 || len(args) > arrow+3 || args[arrow] != "=>" { return nil, errorf("usage: %s module/path [v1.2.3] => other/module v1.4\n\t or %s module/path [v1.2.3] => ../local/directory", verb, verb) } s, err := parseString(&args[0]) if err != nil { return nil, errorf("invalid quoted string: %v", err) } pathMajor, err := modulePathMajor(s) if err != nil { return nil, wrapModPathError(s, err) } var v string if arrow == 2 { v, err = parseVersion(verb, s, &args[1], fix) if err != nil { return nil, wrapError(err) } if err := module.CheckPathMajor(v, pathMajor); err != nil { return nil, wrapModPathError(s, err) } } ns, err := parseString(&args[arrow+1]) if err != nil { return nil, errorf("invalid quoted string: %v", err) } nv := "" if len(args) == arrow+2 { if !IsDirectoryPath(ns) { if strings.Contains(ns, "@") { return nil, errorf("replacement module must match format 'path version', not 'path@version'") } return nil, errorf("replacement module without version must be directory path (rooted or starting with ./ or ../)") } if filepath.Separator == '/' && strings.Contains(ns, `\`) { return nil, errorf("replacement directory appears to be Windows path (on a non-windows system)") } } if len(args) == arrow+3 { nv, err = parseVersion(verb, ns, &args[arrow+2], fix) if err != nil { return nil, wrapError(err) } if IsDirectoryPath(ns) { return nil, errorf("replacement module directory path %q cannot have version", ns) } } return &Replace{ Old: module.Version{Path: s, Version: v}, New: module.Version{Path: ns, Version: nv}, Syntax: line, }, nil } // fixRetract applies fix to each retract directive in f, appending any errors // to errs. // // Most versions are fixed as we parse the file, but for retract directives, // the relevant module path is the one specified with the module directive, // and that might appear at the end of the file (or not at all). func (f *File) fixRetract(fix VersionFixer, errs *ErrorList) { if fix == nil { return } path := "" if f.Module != nil { path = f.Module.Mod.Path } var r *Retract wrapError := func(err error) { *errs = append(*errs, Error{ Filename: f.Syntax.Name, Pos: r.Syntax.Start, Err: err, }) } for _, r = range f.Retract { if path == "" { wrapError(errors.New("no module directive found, so retract cannot be used")) return // only print the first one of these } args := r.Syntax.Token if args[0] == "retract" { args = args[1:] } vi, err := parseVersionInterval("retract", path, &args, fix) if err != nil { wrapError(err) } r.VersionInterval = vi } } func (f *WorkFile) add(errs *ErrorList, line *Line, verb string, args []string, fix VersionFixer) { wrapError := func(err error) { *errs = append(*errs, Error{ Filename: f.Syntax.Name, Pos: line.Start, Err: err, }) } errorf := func(format string, args ...interface{}) { wrapError(fmt.Errorf(format, args...)) } switch verb { default: errorf("unknown directive: %s", verb) case "go": if f.Go != nil { errorf("repeated go statement") return } if len(args) != 1 { errorf("go directive expects exactly one argument") return } else if !GoVersionRE.MatchString(args[0]) { errorf("invalid go version '%s': must match format 1.23", args[0]) return } f.Go = &Go{Syntax: line} f.Go.Version = args[0] case "toolchain": if f.Toolchain != nil { errorf("repeated toolchain statement") return } if len(args) != 1 { errorf("toolchain directive expects exactly one argument") return } else if !ToolchainRE.MatchString(args[0]) { errorf("invalid toolchain version '%s': must match format go1.23 or local", args[0]) return } f.Toolchain = &Toolchain{Syntax: line} f.Toolchain.Name = args[0] case "use": if len(args) != 1 { errorf("usage: %s local/dir", verb) return } s, err := parseString(&args[0]) if err != nil { errorf("invalid quoted string: %v", err) return } f.Use = append(f.Use, &Use{ Path: s, Syntax: line, }) case "replace": replace, wrappederr := parseReplace(f.Syntax.Name, line, verb, args, fix) if wrappederr != nil { *errs = append(*errs, *wrappederr) return } f.Replace = append(f.Replace, replace) } } // IsDirectoryPath reports whether the given path should be interpreted // as a directory path. Just like on the go command line, relative paths // and rooted paths are directory paths; the rest are module paths. func IsDirectoryPath(ns string) bool { // Because go.mod files can move from one system to another, // we check all known path syntaxes, both Unix and Windows. return strings.HasPrefix(ns, "./") || strings.HasPrefix(ns, "../") || strings.HasPrefix(ns, "/") || strings.HasPrefix(ns, `.\`) || strings.HasPrefix(ns, `..\`) || strings.HasPrefix(ns, `\`) || len(ns) >= 2 && ('A' <= ns[0] && ns[0] <= 'Z' || 'a' <= ns[0] && ns[0] <= 'z') && ns[1] == ':' } // MustQuote reports whether s must be quoted in order to appear as // a single token in a go.mod line. func MustQuote(s string) bool { for _, r := range s { switch r { case ' ', '"', '\'', '`': return true case '(', ')', '[', ']', '{', '}', ',': if len(s) > 1 { return true } default: if !unicode.IsPrint(r) { return true } } } return s == "" || strings.Contains(s, "//") || strings.Contains(s, "/*") } // AutoQuote returns s or, if quoting is required for s to appear in a go.mod, // the quotation of s. func AutoQuote(s string) string { if MustQuote(s) { return strconv.Quote(s) } return s } func parseVersionInterval(verb string, path string, args *[]string, fix VersionFixer) (VersionInterval, error) { toks := *args if len(toks) == 0 || toks[0] == "(" { return VersionInterval{}, fmt.Errorf("expected '[' or version") } if toks[0] != "[" { v, err := parseVersion(verb, path, &toks[0], fix) if err != nil { return VersionInterval{}, err } *args = toks[1:] return VersionInterval{Low: v, High: v}, nil } toks = toks[1:] if len(toks) == 0 { return VersionInterval{}, fmt.Errorf("expected version after '['") } low, err := parseVersion(verb, path, &toks[0], fix) if err != nil { return VersionInterval{}, err } toks = toks[1:] if len(toks) == 0 || toks[0] != "," { return VersionInterval{}, fmt.Errorf("expected ',' after version") } toks = toks[1:] if len(toks) == 0 { return VersionInterval{}, fmt.Errorf("expected version after ','") } high, err := parseVersion(verb, path, &toks[0], fix) if err != nil { return VersionInterval{}, err } toks = toks[1:] if len(toks) == 0 || toks[0] != "]" { return VersionInterval{}, fmt.Errorf("expected ']' after version") } toks = toks[1:] *args = toks return VersionInterval{Low: low, High: high}, nil } func parseString(s *string) (string, error) { t := *s if strings.HasPrefix(t, `"`) { var err error if t, err = strconv.Unquote(t); err != nil { return "", err } } else if strings.ContainsAny(t, "\"'`") { // Other quotes are reserved both for possible future expansion // and to avoid confusion. For example if someone types 'x' // we want that to be a syntax error and not a literal x in literal quotation marks. return "", fmt.Errorf("unquoted string cannot contain quote") } *s = AutoQuote(t) return t, nil } var deprecatedRE = lazyregexp.New(`(?s)(?:^|\n\n)Deprecated: *(.*?)(?:$|\n\n)`) // parseDeprecation extracts the text of comments on a "module" directive and // extracts a deprecation message from that. // // A deprecation message is contained in a paragraph within a block of comments // that starts with "Deprecated:" (case sensitive). The message runs until the // end of the paragraph and does not include the "Deprecated:" prefix. If the // comment block has multiple paragraphs that start with "Deprecated:", // parseDeprecation returns the message from the first. func parseDeprecation(block *LineBlock, line *Line) string { text := parseDirectiveComment(block, line) m := deprecatedRE.FindStringSubmatch(text) if m == nil { return "" } return m[1] } // parseDirectiveComment extracts the text of comments on a directive. // If the directive's line does not have comments and is part of a block that // does have comments, the block's comments are used. func parseDirectiveComment(block *LineBlock, line *Line) string { comments := line.Comment() if block != nil && len(comments.Before) == 0 && len(comments.Suffix) == 0 { comments = block.Comment() } groups := [][]Comment{comments.Before, comments.Suffix} var lines []string for _, g := range groups { for _, c := range g { if !strings.HasPrefix(c.Token, "//") { continue // blank line } lines = append(lines, strings.TrimSpace(strings.TrimPrefix(c.Token, "//"))) } } return strings.Join(lines, "\n") } type ErrorList []Error func (e ErrorList) Error() string { errStrs := make([]string, len(e)) for i, err := range e { errStrs[i] = err.Error() } return strings.Join(errStrs, "\n") } type Error struct { Filename string Pos Position Verb string ModPath string Err error } func (e *Error) Error() string { var pos string if e.Pos.LineRune > 1 { // Don't print LineRune if it's 1 (beginning of line). // It's always 1 except in scanner errors, which are rare. pos = fmt.Sprintf("%s:%d:%d: ", e.Filename, e.Pos.Line, e.Pos.LineRune) } else if e.Pos.Line > 0 { pos = fmt.Sprintf("%s:%d: ", e.Filename, e.Pos.Line) } else if e.Filename != "" { pos = fmt.Sprintf("%s: ", e.Filename) } var directive string if e.ModPath != "" { directive = fmt.Sprintf("%s %s: ", e.Verb, e.ModPath) } else if e.Verb != "" { directive = fmt.Sprintf("%s: ", e.Verb) } return pos + directive + e.Err.Error() } func (e *Error) Unwrap() error { return e.Err } func parseVersion(verb string, path string, s *string, fix VersionFixer) (string, error) { t, err := parseString(s) if err != nil { return "", &Error{ Verb: verb, ModPath: path, Err: &module.InvalidVersionError{ Version: *s, Err: err, }, } } if fix != nil { fixed, err := fix(path, t) if err != nil { if err, ok := err.(*module.ModuleError); ok { return "", &Error{ Verb: verb, ModPath: path, Err: err.Err, } } return "", err } t = fixed } else { cv := module.CanonicalVersion(t) if cv == "" { return "", &Error{ Verb: verb, ModPath: path, Err: &module.InvalidVersionError{ Version: t, Err: errors.New("must be of the form v1.2.3"), }, } } t = cv } *s = t return *s, nil } func modulePathMajor(path string) (string, error) { _, major, ok := module.SplitPathVersion(path) if !ok { return "", fmt.Errorf("invalid module path") } return major, nil } func (f *File) Format() ([]byte, error) { return Format(f.Syntax), nil } // Cleanup cleans up the file f after any edit operations. // To avoid quadratic behavior, modifications like [File.DropRequire] // clear the entry but do not remove it from the slice. // Cleanup cleans out all the cleared entries. func (f *File) Cleanup() { w := 0 for _, r := range f.Require { if r.Mod.Path != "" { f.Require[w] = r w++ } } f.Require = f.Require[:w] w = 0 for _, x := range f.Exclude { if x.Mod.Path != "" { f.Exclude[w] = x w++ } } f.Exclude = f.Exclude[:w] w = 0 for _, r := range f.Replace { if r.Old.Path != "" { f.Replace[w] = r w++ } } f.Replace = f.Replace[:w] w = 0 for _, r := range f.Retract { if r.Low != "" || r.High != "" { f.Retract[w] = r w++ } } f.Retract = f.Retract[:w] f.Syntax.Cleanup() } func (f *File) AddGoStmt(version string) error { if !GoVersionRE.MatchString(version) { return fmt.Errorf("invalid language version %q", version) } if f.Go == nil { var hint Expr if f.Module != nil && f.Module.Syntax != nil { hint = f.Module.Syntax } f.Go = &Go{ Version: version, Syntax: f.Syntax.addLine(hint, "go", version), } } else { f.Go.Version = version f.Syntax.updateLine(f.Go.Syntax, "go", version) } return nil } // DropGoStmt deletes the go statement from the file. func (f *File) DropGoStmt() { if f.Go != nil { f.Go.Syntax.markRemoved() f.Go = nil } } // DropToolchainStmt deletes the toolchain statement from the file. func (f *File) DropToolchainStmt() { if f.Toolchain != nil { f.Toolchain.Syntax.markRemoved() f.Toolchain = nil } } func (f *File) AddToolchainStmt(name string) error { if !ToolchainRE.MatchString(name) { return fmt.Errorf("invalid toolchain name %q", name) } if f.Toolchain == nil { var hint Expr if f.Go != nil && f.Go.Syntax != nil { hint = f.Go.Syntax } else if f.Module != nil && f.Module.Syntax != nil { hint = f.Module.Syntax } f.Toolchain = &Toolchain{ Name: name, Syntax: f.Syntax.addLine(hint, "toolchain", name), } } else { f.Toolchain.Name = name f.Syntax.updateLine(f.Toolchain.Syntax, "toolchain", name) } return nil } // AddRequire sets the first require line for path to version vers, // preserving any existing comments for that line and removing all // other lines for path. // // If no line currently exists for path, AddRequire adds a new line // at the end of the last require block. func (f *File) AddRequire(path, vers string) error { need := true for _, r := range f.Require { if r.Mod.Path == path { if need { r.Mod.Version = vers f.Syntax.updateLine(r.Syntax, "require", AutoQuote(path), vers) need = false } else { r.Syntax.markRemoved() *r = Require{} } } } if need { f.AddNewRequire(path, vers, false) } return nil } // AddNewRequire adds a new require line for path at version vers at the end of // the last require block, regardless of any existing require lines for path. func (f *File) AddNewRequire(path, vers string, indirect bool) { line := f.Syntax.addLine(nil, "require", AutoQuote(path), vers) r := &Require{ Mod: module.Version{Path: path, Version: vers}, Syntax: line, } r.setIndirect(indirect) f.Require = append(f.Require, r) } // SetRequire updates the requirements of f to contain exactly req, preserving // the existing block structure and line comment contents (except for 'indirect' // markings) for the first requirement on each named module path. // // The Syntax field is ignored for the requirements in req. // // Any requirements not already present in the file are added to the block // containing the last require line. // // The requirements in req must specify at most one distinct version for each // module path. // // If any existing requirements may be removed, the caller should call // [File.Cleanup] after all edits are complete. func (f *File) SetRequire(req []*Require) { type elem struct { version string indirect bool } need := make(map[string]elem) for _, r := range req { if prev, dup := need[r.Mod.Path]; dup && prev.version != r.Mod.Version { panic(fmt.Errorf("SetRequire called with conflicting versions for path %s (%s and %s)", r.Mod.Path, prev.version, r.Mod.Version)) } need[r.Mod.Path] = elem{r.Mod.Version, r.Indirect} } // Update or delete the existing Require entries to preserve // only the first for each module path in req. for _, r := range f.Require { e, ok := need[r.Mod.Path] if ok { r.setVersion(e.version) r.setIndirect(e.indirect) } else { r.markRemoved() } delete(need, r.Mod.Path) } // Add new entries in the last block of the file for any paths that weren't // already present. // // This step is nondeterministic, but the final result will be deterministic // because we will sort the block. for path, e := range need { f.AddNewRequire(path, e.version, e.indirect) } f.SortBlocks() } // SetRequireSeparateIndirect updates the requirements of f to contain the given // requirements. Comment contents (except for 'indirect' markings) are retained // from the first existing requirement for each module path. Like SetRequire, // SetRequireSeparateIndirect adds requirements for new paths in req, // updates the version and "// indirect" comment on existing requirements, // and deletes requirements on paths not in req. Existing duplicate requirements // are deleted. // // As its name suggests, SetRequireSeparateIndirect puts direct and indirect // requirements into two separate blocks, one containing only direct // requirements, and the other containing only indirect requirements. // SetRequireSeparateIndirect may move requirements between these two blocks // when their indirect markings change. However, SetRequireSeparateIndirect // won't move requirements from other blocks, especially blocks with comments. // // If the file initially has one uncommented block of requirements, // SetRequireSeparateIndirect will split it into a direct-only and indirect-only // block. This aids in the transition to separate blocks. func (f *File) SetRequireSeparateIndirect(req []*Require) { // hasComments returns whether a line or block has comments // other than "indirect". hasComments := func(c Comments) bool { return len(c.Before) > 0 || len(c.After) > 0 || len(c.Suffix) > 1 || (len(c.Suffix) == 1 && strings.TrimSpace(strings.TrimPrefix(c.Suffix[0].Token, string(slashSlash))) != "indirect") } // moveReq adds r to block. If r was in another block, moveReq deletes // it from that block and transfers its comments. moveReq := func(r *Require, block *LineBlock) { var line *Line if r.Syntax == nil { line = &Line{Token: []string{AutoQuote(r.Mod.Path), r.Mod.Version}} r.Syntax = line if r.Indirect { r.setIndirect(true) } } else { line = new(Line) *line = *r.Syntax if !line.InBlock && len(line.Token) > 0 && line.Token[0] == "require" { line.Token = line.Token[1:] } r.Syntax.Token = nil // Cleanup will delete the old line. r.Syntax = line } line.InBlock = true block.Line = append(block.Line, line) } // Examine existing require lines and blocks. var ( // We may insert new requirements into the last uncommented // direct-only and indirect-only blocks. We may also move requirements // to the opposite block if their indirect markings change. lastDirectIndex = -1 lastIndirectIndex = -1 // If there are no direct-only or indirect-only blocks, a new block may // be inserted after the last require line or block. lastRequireIndex = -1 // If there's only one require line or block, and it's uncommented, // we'll move its requirements to the direct-only or indirect-only blocks. requireLineOrBlockCount = 0 // Track the block each requirement belongs to (if any) so we can // move them later. lineToBlock = make(map[*Line]*LineBlock) ) for i, stmt := range f.Syntax.Stmt { switch stmt := stmt.(type) { case *Line: if len(stmt.Token) == 0 || stmt.Token[0] != "require" { continue } lastRequireIndex = i requireLineOrBlockCount++ if !hasComments(stmt.Comments) { if isIndirect(stmt) { lastIndirectIndex = i } else { lastDirectIndex = i } } case *LineBlock: if len(stmt.Token) == 0 || stmt.Token[0] != "require" { continue } lastRequireIndex = i requireLineOrBlockCount++ allDirect := len(stmt.Line) > 0 && !hasComments(stmt.Comments) allIndirect := len(stmt.Line) > 0 && !hasComments(stmt.Comments) for _, line := range stmt.Line { lineToBlock[line] = stmt if hasComments(line.Comments) { allDirect = false allIndirect = false } else if isIndirect(line) { allDirect = false } else { allIndirect = false } } if allDirect { lastDirectIndex = i } if allIndirect { lastIndirectIndex = i } } } oneFlatUncommentedBlock := requireLineOrBlockCount == 1 && !hasComments(*f.Syntax.Stmt[lastRequireIndex].Comment()) // Create direct and indirect blocks if needed. Convert lines into blocks // if needed. If we end up with an empty block or a one-line block, // Cleanup will delete it or convert it to a line later. insertBlock := func(i int) *LineBlock { block := &LineBlock{Token: []string{"require"}} f.Syntax.Stmt = append(f.Syntax.Stmt, nil) copy(f.Syntax.Stmt[i+1:], f.Syntax.Stmt[i:]) f.Syntax.Stmt[i] = block return block } ensureBlock := func(i int) *LineBlock { switch stmt := f.Syntax.Stmt[i].(type) { case *LineBlock: return stmt case *Line: block := &LineBlock{ Token: []string{"require"}, Line: []*Line{stmt}, } stmt.Token = stmt.Token[1:] // remove "require" stmt.InBlock = true f.Syntax.Stmt[i] = block return block default: panic(fmt.Sprintf("unexpected statement: %v", stmt)) } } var lastDirectBlock *LineBlock if lastDirectIndex < 0 { if lastIndirectIndex >= 0 { lastDirectIndex = lastIndirectIndex lastIndirectIndex++ } else if lastRequireIndex >= 0 { lastDirectIndex = lastRequireIndex + 1 } else { lastDirectIndex = len(f.Syntax.Stmt) } lastDirectBlock = insertBlock(lastDirectIndex) } else { lastDirectBlock = ensureBlock(lastDirectIndex) } var lastIndirectBlock *LineBlock if lastIndirectIndex < 0 { lastIndirectIndex = lastDirectIndex + 1 lastIndirectBlock = insertBlock(lastIndirectIndex) } else { lastIndirectBlock = ensureBlock(lastIndirectIndex) } // Delete requirements we don't want anymore. // Update versions and indirect comments on requirements we want to keep. // If a requirement is in last{Direct,Indirect}Block with the wrong // indirect marking after this, or if the requirement is in an single // uncommented mixed block (oneFlatUncommentedBlock), move it to the // correct block. // // Some blocks may be empty after this. Cleanup will remove them. need := make(map[string]*Require) for _, r := range req { need[r.Mod.Path] = r } have := make(map[string]*Require) for _, r := range f.Require { path := r.Mod.Path if need[path] == nil || have[path] != nil { // Requirement not needed, or duplicate requirement. Delete. r.markRemoved() continue } have[r.Mod.Path] = r r.setVersion(need[path].Mod.Version) r.setIndirect(need[path].Indirect) if need[path].Indirect && (oneFlatUncommentedBlock || lineToBlock[r.Syntax] == lastDirectBlock) { moveReq(r, lastIndirectBlock) } else if !need[path].Indirect && (oneFlatUncommentedBlock || lineToBlock[r.Syntax] == lastIndirectBlock) { moveReq(r, lastDirectBlock) } } // Add new requirements. for path, r := range need { if have[path] == nil { if r.Indirect { moveReq(r, lastIndirectBlock) } else { moveReq(r, lastDirectBlock) } f.Require = append(f.Require, r) } } f.SortBlocks() } func (f *File) DropRequire(path string) error { for _, r := range f.Require { if r.Mod.Path == path { r.Syntax.markRemoved() *r = Require{} } } return nil } // AddExclude adds a exclude statement to the mod file. Errors if the provided // version is not a canonical version string func (f *File) AddExclude(path, vers string) error { if err := checkCanonicalVersion(path, vers); err != nil { return err } var hint *Line for _, x := range f.Exclude { if x.Mod.Path == path && x.Mod.Version == vers { return nil } if x.Mod.Path == path { hint = x.Syntax } } f.Exclude = append(f.Exclude, &Exclude{Mod: module.Version{Path: path, Version: vers}, Syntax: f.Syntax.addLine(hint, "exclude", AutoQuote(path), vers)}) return nil } func (f *File) DropExclude(path, vers string) error { for _, x := range f.Exclude { if x.Mod.Path == path && x.Mod.Version == vers { x.Syntax.markRemoved() *x = Exclude{} } } return nil } func (f *File) AddReplace(oldPath, oldVers, newPath, newVers string) error { return addReplace(f.Syntax, &f.Replace, oldPath, oldVers, newPath, newVers) } func addReplace(syntax *FileSyntax, replace *[]*Replace, oldPath, oldVers, newPath, newVers string) error { need := true old := module.Version{Path: oldPath, Version: oldVers} new := module.Version{Path: newPath, Version: newVers} tokens := []string{"replace", AutoQuote(oldPath)} if oldVers != "" { tokens = append(tokens, oldVers) } tokens = append(tokens, "=>", AutoQuote(newPath)) if newVers != "" { tokens = append(tokens, newVers) } var hint *Line for _, r := range *replace { if r.Old.Path == oldPath && (oldVers == "" || r.Old.Version == oldVers) { if need { // Found replacement for old; update to use new. r.New = new syntax.updateLine(r.Syntax, tokens...) need = false continue } // Already added; delete other replacements for same. r.Syntax.markRemoved() *r = Replace{} } if r.Old.Path == oldPath { hint = r.Syntax } } if need { *replace = append(*replace, &Replace{Old: old, New: new, Syntax: syntax.addLine(hint, tokens...)}) } return nil } func (f *File) DropReplace(oldPath, oldVers string) error { for _, r := range f.Replace { if r.Old.Path == oldPath && r.Old.Version == oldVers { r.Syntax.markRemoved() *r = Replace{} } } return nil } // AddRetract adds a retract statement to the mod file. Errors if the provided // version interval does not consist of canonical version strings func (f *File) AddRetract(vi VersionInterval, rationale string) error { var path string if f.Module != nil { path = f.Module.Mod.Path } if err := checkCanonicalVersion(path, vi.High); err != nil { return err } if err := checkCanonicalVersion(path, vi.Low); err != nil { return err } r := &Retract{ VersionInterval: vi, } if vi.Low == vi.High { r.Syntax = f.Syntax.addLine(nil, "retract", AutoQuote(vi.Low)) } else { r.Syntax = f.Syntax.addLine(nil, "retract", "[", AutoQuote(vi.Low), ",", AutoQuote(vi.High), "]") } if rationale != "" { for _, line := range strings.Split(rationale, "\n") { com := Comment{Token: "// " + line} r.Syntax.Comment().Before = append(r.Syntax.Comment().Before, com) } } return nil } func (f *File) DropRetract(vi VersionInterval) error { for _, r := range f.Retract { if r.VersionInterval == vi { r.Syntax.markRemoved() *r = Retract{} } } return nil } func (f *File) SortBlocks() { f.removeDups() // otherwise sorting is unsafe // semanticSortForExcludeVersionV is the Go version (plus leading "v") at which // lines in exclude blocks start to use semantic sort instead of lexicographic sort. // See go.dev/issue/60028. const semanticSortForExcludeVersionV = "v1.21" useSemanticSortForExclude := f.Go != nil && semver.Compare("v"+f.Go.Version, semanticSortForExcludeVersionV) >= 0 for _, stmt := range f.Syntax.Stmt { block, ok := stmt.(*LineBlock) if !ok { continue } less := lineLess if block.Token[0] == "exclude" && useSemanticSortForExclude { less = lineExcludeLess } else if block.Token[0] == "retract" { less = lineRetractLess } sort.SliceStable(block.Line, func(i, j int) bool { return less(block.Line[i], block.Line[j]) }) } } // removeDups removes duplicate exclude and replace directives. // // Earlier exclude directives take priority. // // Later replace directives take priority. // // require directives are not de-duplicated. That's left up to higher-level // logic (MVS). // // retract directives are not de-duplicated since comments are // meaningful, and versions may be retracted multiple times. func (f *File) removeDups() { removeDups(f.Syntax, &f.Exclude, &f.Replace) } func removeDups(syntax *FileSyntax, exclude *[]*Exclude, replace *[]*Replace) { kill := make(map[*Line]bool) // Remove duplicate excludes. if exclude != nil { haveExclude := make(map[module.Version]bool) for _, x := range *exclude { if haveExclude[x.Mod] { kill[x.Syntax] = true continue } haveExclude[x.Mod] = true } var excl []*Exclude for _, x := range *exclude { if !kill[x.Syntax] { excl = append(excl, x) } } *exclude = excl } // Remove duplicate replacements. // Later replacements take priority over earlier ones. haveReplace := make(map[module.Version]bool) for i := len(*replace) - 1; i >= 0; i-- { x := (*replace)[i] if haveReplace[x.Old] { kill[x.Syntax] = true continue } haveReplace[x.Old] = true } var repl []*Replace for _, x := range *replace { if !kill[x.Syntax] { repl = append(repl, x) } } *replace = repl // Duplicate require and retract directives are not removed. // Drop killed statements from the syntax tree. var stmts []Expr for _, stmt := range syntax.Stmt { switch stmt := stmt.(type) { case *Line: if kill[stmt] { continue } case *LineBlock: var lines []*Line for _, line := range stmt.Line { if !kill[line] { lines = append(lines, line) } } stmt.Line = lines if len(lines) == 0 { continue } } stmts = append(stmts, stmt) } syntax.Stmt = stmts } // lineLess returns whether li should be sorted before lj. It sorts // lexicographically without assigning any special meaning to tokens. func lineLess(li, lj *Line) bool { for k := 0; k < len(li.Token) && k < len(lj.Token); k++ { if li.Token[k] != lj.Token[k] { return li.Token[k] < lj.Token[k] } } return len(li.Token) < len(lj.Token) } // lineExcludeLess reports whether li should be sorted before lj for lines in // an "exclude" block. func lineExcludeLess(li, lj *Line) bool { if len(li.Token) != 2 || len(lj.Token) != 2 { // Not a known exclude specification. // Fall back to sorting lexicographically. return lineLess(li, lj) } // An exclude specification has two tokens: ModulePath and Version. // Compare module path by string order and version by semver rules. if pi, pj := li.Token[0], lj.Token[0]; pi != pj { return pi < pj } return semver.Compare(li.Token[1], lj.Token[1]) < 0 } // lineRetractLess returns whether li should be sorted before lj for lines in // a "retract" block. It treats each line as a version interval. Single versions // are compared as if they were intervals with the same low and high version. // Intervals are sorted in descending order, first by low version, then by // high version, using semver.Compare. func lineRetractLess(li, lj *Line) bool { interval := func(l *Line) VersionInterval { if len(l.Token) == 1 { return VersionInterval{Low: l.Token[0], High: l.Token[0]} } else if len(l.Token) == 5 && l.Token[0] == "[" && l.Token[2] == "," && l.Token[4] == "]" { return VersionInterval{Low: l.Token[1], High: l.Token[3]} } else { // Line in unknown format. Treat as an invalid version. return VersionInterval{} } } vii := interval(li) vij := interval(lj) if cmp := semver.Compare(vii.Low, vij.Low); cmp != 0 { return cmp > 0 } return semver.Compare(vii.High, vij.High) > 0 } // checkCanonicalVersion returns a non-nil error if vers is not a canonical // version string or does not match the major version of path. // // If path is non-empty, the error text suggests a format with a major version // corresponding to the path. func checkCanonicalVersion(path, vers string) error { _, pathMajor, pathMajorOk := module.SplitPathVersion(path) if vers == "" || vers != module.CanonicalVersion(vers) { if pathMajor == "" { return &module.InvalidVersionError{ Version: vers, Err: fmt.Errorf("must be of the form v1.2.3"), } } return &module.InvalidVersionError{ Version: vers, Err: fmt.Errorf("must be of the form %s.2.3", module.PathMajorPrefix(pathMajor)), } } if pathMajorOk { if err := module.CheckPathMajor(vers, pathMajor); err != nil { if pathMajor == "" { // In this context, the user probably wrote "v2.3.4" when they meant // "v2.3.4+incompatible". Suggest that instead of "v0 or v1". return &module.InvalidVersionError{ Version: vers, Err: fmt.Errorf("should be %s+incompatible (or module %s/%v)", vers, path, semver.Major(vers)), } } return err } } return nil }