Files
polaris/vendor/github.com/rogpeppe/go-internal/modfile/rule.go
Bobby Brennan 54a4f92695 Pack static assets into packr box for portability
copy config.yaml to docker image

enable external usage of dashboard package

gofmt

fix comment

use packr for assets

add gobuffalo/packr dependency

add dependencies

fix pointer issues

add output-file option
2019-04-18 18:25:16 +00:00

725 lines
18 KiB
Go

// 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
import (
"bytes"
"errors"
"fmt"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"unicode"
"github.com/rogpeppe/go-internal/module"
"github.com/rogpeppe/go-internal/semver"
)
// 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 bytes.Buffer
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 {
fmt.Fprintf(&errs, "%s:%d: unknown block type: %s\n", file, x.Start.Line, strings.Join(x.Token, " "))
}
continue
}
switch x.Token[0] {
default:
if strict {
fmt.Fprintf(&errs, "%s:%d: unknown block type: %s\n", file, x.Start.Line, 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 errs.Len() > 0 {
return nil, errors.New(strings.TrimRight(errs.String(), "\n"))
}
return f, nil
}
var goVersionRE = regexp.MustCompile(`([1-9][0-9]*)\.(0|[1-9][0-9]*)`)
func (f *File) add(errs *bytes.Buffer, 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
}
}
switch verb {
default:
fmt.Fprintf(errs, "%s:%d: unknown directive: %s\n", f.Syntax.Name, line.Start.Line, verb)
case "go":
if f.Go != nil {
fmt.Fprintf(errs, "%s:%d: repeated go statement\n", f.Syntax.Name, line.Start.Line)
return
}
if len(args) != 1 || !goVersionRE.MatchString(args[0]) {
fmt.Fprintf(errs, "%s:%d: usage: go 1.23\n", f.Syntax.Name, line.Start.Line)
return
}
f.Go = &Go{Syntax: line}
f.Go.Version = args[0]
case "module":
if f.Module != nil {
fmt.Fprintf(errs, "%s:%d: repeated module statement\n", f.Syntax.Name, line.Start.Line)
return
}
f.Module = &Module{Syntax: line}
if len(args) != 1 {
fmt.Fprintf(errs, "%s:%d: usage: module module/path [version]\n", f.Syntax.Name, line.Start.Line)
return
}
s, err := parseString(&args[0])
if err != nil {
fmt.Fprintf(errs, "%s:%d: invalid quoted string: %v\n", f.Syntax.Name, line.Start.Line, err)
return
}
f.Module.Mod = module.Version{Path: s}
case "require", "exclude":
if len(args) != 2 {
fmt.Fprintf(errs, "%s:%d: usage: %s module/path v1.2.3\n", f.Syntax.Name, line.Start.Line, verb)
return
}
s, err := parseString(&args[0])
if err != nil {
fmt.Fprintf(errs, "%s:%d: invalid quoted string: %v\n", f.Syntax.Name, line.Start.Line, err)
return
}
old := args[1]
v, err := parseVersion(s, &args[1], fix)
if err != nil {
fmt.Fprintf(errs, "%s:%d: invalid module version %q: %v\n", f.Syntax.Name, line.Start.Line, old, err)
return
}
pathMajor, err := modulePathMajor(s)
if err != nil {
fmt.Fprintf(errs, "%s:%d: %v\n", f.Syntax.Name, line.Start.Line, err)
return
}
if !module.MatchPathMajor(v, pathMajor) {
if pathMajor == "" {
pathMajor = "v0 or v1"
}
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)
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] != "=>" {
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)
return
}
s, err := parseString(&args[0])
if err != nil {
fmt.Fprintf(errs, "%s:%d: invalid quoted string: %v\n", f.Syntax.Name, line.Start.Line, err)
return
}
pathMajor, err := modulePathMajor(s)
if err != nil {
fmt.Fprintf(errs, "%s:%d: %v\n", f.Syntax.Name, line.Start.Line, err)
return
}
var v string
if arrow == 2 {
old := args[1]
v, err = parseVersion(s, &args[1], fix)
if err != nil {
fmt.Fprintf(errs, "%s:%d: invalid module version %v: %v\n", f.Syntax.Name, line.Start.Line, old, err)
return
}
if !module.MatchPathMajor(v, pathMajor) {
if pathMajor == "" {
pathMajor = "v0 or v1"
}
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)
return
}
}
ns, err := parseString(&args[arrow+1])
if err != nil {
fmt.Fprintf(errs, "%s:%d: invalid quoted string: %v\n", f.Syntax.Name, line.Start.Line, err)
return
}
nv := ""
if len(args) == arrow+2 {
if !IsDirectoryPath(ns) {
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)
return
}
if filepath.Separator == '/' && strings.Contains(ns, `\`) {
fmt.Fprintf(errs, "%s:%d: replacement directory appears to be Windows path (on a non-windows system)\n", f.Syntax.Name, line.Start.Line)
return
}
}
if len(args) == arrow+3 {
old := args[arrow+1]
nv, err = parseVersion(ns, &args[arrow+2], fix)
if err != nil {
fmt.Fprintf(errs, "%s:%d: invalid module version %v: %v\n", f.Syntax.Name, line.Start.Line, old, err)
return
}
if IsDirectoryPath(ns) {
fmt.Fprintf(errs, "%s:%d: replacement module directory path %q cannot have version\n", f.Syntax.Name, line.Start.Line, 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(line.Suffix[0].Token)
return (len(f) == 2 && f[1] == "indirect" || len(f) > 2 && f[1] == "indirect;") && f[0] == "//"
}
// 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
}
// Insert at beginning of existing comment.
com := &line.Suffix[0]
space := " "
if len(com.Token) > 2 && com.Token[2] == ' ' || com.Token[2] == '\t' {
space = ""
}
com.Token = "// indirect;" + space + com.Token[2:]
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 {
if !unicode.IsPrint(r) || r == ' ' || r == '"' || r == '\'' || 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
}
func parseVersion(path string, s *string, fix VersionFixer) (string, error) {
t, err := parseString(s)
if err != nil {
return "", err
}
if fix != nil {
var err error
t, err = fix(path, t)
if err != nil {
return "", err
}
}
if v := module.CanonicalVersion(t); v != "" {
*s = v
return *s, nil
}
return "", fmt.Errorf("version 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) 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]
}
}
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] != "" {
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
}