// 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://golang.org/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 AddNewRequire and 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" ) // A File is the parsed, interpreted form of a go.mod file. type File struct { Module *Module Go *Go Require []*Require Exclude []*Exclude Replace []*Replace Syntax *FileSyntax } // A Module is the module statement. type Module struct { Mod module.Version Syntax *Line } // A Go is the go statement. type Go struct { Version string // "1.23" Syntax *Line } // A Require is a single require statement. type Require struct { Mod module.Version Indirect bool // has "// indirect" comment 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 } 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) // Parse parses the data, reported in errors as being from file, // into a File struct. It applies fix, if non-nil, to canonicalize all module versions found. 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) (*File, error) { fs, err := parse(file, data) if err != nil { return nil, err } f := &File{ Syntax: fs, } var errs ErrorList for _, x := range fs.Stmt { switch x := x.(type) { case *Line: f.add(&errs, 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": for _, l := range x.Line { f.add(&errs, 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]*)$`) func (f *File) add(errs *ErrorList, 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 "module", "require", "go": // 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]) { errorf("invalid go version '%s': must match format 1.23", args[0]) return } f.Go = &Go{Syntax: line} f.Go.Version = args[0] case "module": if f.Module != nil { errorf("repeated module statement") return } f.Module = &Module{Syntax: line} 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": arrow := 2 if len(args) >= 2 && args[1] == "=>" { arrow = 1 } if len(args) < arrow+2 || len(args) > arrow+3 || args[arrow] != "=>" { 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) return } s, err := parseString(&args[0]) if err != nil { errorf("invalid quoted string: %v", err) return } pathMajor, err := modulePathMajor(s) if err != nil { wrapModPathError(s, err) return } var v string if arrow == 2 { v, err = parseVersion(verb, s, &args[1], fix) if err != nil { wrapError(err) return } if err := module.CheckPathMajor(v, pathMajor); err != nil { wrapModPathError(s, err) return } } ns, err := parseString(&args[arrow+1]) if err != nil { errorf("invalid quoted string: %v", err) return } nv := "" if len(args) == arrow+2 { if !IsDirectoryPath(ns) { errorf("replacement module without version must be directory path (rooted or starting with ./ or ../)") return } if filepath.Separator == '/' && strings.Contains(ns, `\`) { errorf("replacement directory appears to be Windows path (on a non-windows system)") return } } if len(args) == arrow+3 { nv, err = parseVersion(verb, ns, &args[arrow+2], fix) if err != nil { wrapError(err) return } if IsDirectoryPath(ns) { errorf("replacement module directory path %q cannot have version", ns) return } } f.Replace = append(f.Replace, &Replace{ Old: module.Version{Path: s, Version: v}, New: module.Version{Path: ns, Version: nv}, Syntax: line, }) } } // 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;") } // setIndirect sets line to have (or not have) a "// indirect" comment. func setIndirect(line *Line, indirect bool) { 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.Fields(line.Suffix[0].Token) if len(f) == 2 { // 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;"):] } // 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 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 } 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) } 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 { var err error t, 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 } } if v := module.CanonicalVersion(t); v != "" { *s = v return *s, nil } return "", &Error{ Verb: verb, ModPath: path, Err: &module.InvalidVersionError{ Version: t, Err: errors.New("must be of the form v1.2.3"), }, } } 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 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] f.Syntax.Cleanup() } func (f *File) AddGoStmt(version string) error { if !GoVersionRE.MatchString(version) { return fmt.Errorf("invalid language version string %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 } 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 { f.Syntax.removeLine(r.Syntax) *r = Require{} } } } if need { f.AddNewRequire(path, vers, false) } return nil } func (f *File) AddNewRequire(path, vers string, indirect bool) { line := f.Syntax.addLine(nil, "require", AutoQuote(path), vers) setIndirect(line, indirect) f.Require = append(f.Require, &Require{module.Version{Path: path, Version: vers}, indirect, line}) } func (f *File) SetRequire(req []*Require) { need := make(map[string]string) indirect := make(map[string]bool) for _, r := range req { need[r.Mod.Path] = r.Mod.Version indirect[r.Mod.Path] = r.Indirect } for _, r := range f.Require { if v, ok := need[r.Mod.Path]; ok { r.Mod.Version = v r.Indirect = indirect[r.Mod.Path] } else { *r = Require{} } } var newStmts []Expr for _, stmt := range f.Syntax.Stmt { switch stmt := stmt.(type) { case *LineBlock: if len(stmt.Token) > 0 && stmt.Token[0] == "require" { var newLines []*Line for _, line := range stmt.Line { if p, err := parseString(&line.Token[0]); err == nil && need[p] != "" { if len(line.Comments.Before) == 1 && len(line.Comments.Before[0].Token) == 0 { line.Comments.Before = line.Comments.Before[:0] } line.Token[1] = need[p] delete(need, p) setIndirect(line, indirect[p]) newLines = append(newLines, line) } } if len(newLines) == 0 { continue // drop stmt } stmt.Line = newLines } case *Line: if len(stmt.Token) > 0 && stmt.Token[0] == "require" { if p, err := parseString(&stmt.Token[1]); err == nil && need[p] != "" { stmt.Token[2] = need[p] delete(need, p) setIndirect(stmt, indirect[p]) } else { continue // drop stmt } } } newStmts = append(newStmts, stmt) } f.Syntax.Stmt = newStmts for path, vers := range need { f.AddNewRequire(path, vers, indirect[path]) } f.SortBlocks() } func (f *File) DropRequire(path string) error { for _, r := range f.Require { if r.Mod.Path == path { f.Syntax.removeLine(r.Syntax) *r = Require{} } } return nil } func (f *File) AddExclude(path, vers string) error { 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 { f.Syntax.removeLine(x.Syntax) *x = Exclude{} } } return nil } func (f *File) AddReplace(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 f.Replace { if r.Old.Path == oldPath && (oldVers == "" || r.Old.Version == oldVers) { if need { // Found replacement for old; update to use new. r.New = new f.Syntax.updateLine(r.Syntax, tokens...) need = false continue } // Already added; delete other replacements for same. f.Syntax.removeLine(r.Syntax) *r = Replace{} } if r.Old.Path == oldPath { hint = r.Syntax } } if need { f.Replace = append(f.Replace, &Replace{Old: old, New: new, Syntax: f.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 { f.Syntax.removeLine(r.Syntax) *r = Replace{} } } return nil } func (f *File) SortBlocks() { f.removeDups() // otherwise sorting is unsafe for _, stmt := range f.Syntax.Stmt { block, ok := stmt.(*LineBlock) if !ok { continue } sort.Slice(block.Line, func(i, j int) bool { li := block.Line[i] lj := block.Line[j] 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) }) } } func (f *File) removeDups() { have := make(map[module.Version]bool) kill := make(map[*Line]bool) for _, x := range f.Exclude { if have[x.Mod] { kill[x.Syntax] = true continue } have[x.Mod] = true } var excl []*Exclude for _, x := range f.Exclude { if !kill[x.Syntax] { excl = append(excl, x) } } f.Exclude = excl have = make(map[module.Version]bool) // Later replacements take priority over earlier ones. for i := len(f.Replace) - 1; i >= 0; i-- { x := f.Replace[i] if have[x.Old] { kill[x.Syntax] = true continue } have[x.Old] = true } var repl []*Replace for _, x := range f.Replace { if !kill[x.Syntax] { repl = append(repl, x) } } f.Replace = repl var stmts []Expr for _, stmt := range f.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) } f.Syntax.Stmt = stmts }