blob: 9c50102597facb2e8a3106fa53940a66051a94a2 [file] [log] [blame]
// 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"
"sort"
"strconv"
"strings"
"unicode"
"cmd/go/internal/module"
"cmd/go/internal/semver"
)
type File struct {
Module *Module
Require []*Require
Exclude []*Exclude
Replace []*Replace
Syntax *FileSyntax
}
type Module struct {
Mod module.Version
Major string
}
type Require struct {
Mod module.Version
Syntax *Line
}
type Exclude struct {
Mod module.Version
Syntax *Line
}
type Replace struct {
Old module.Version
New module.Version
Syntax *Line
}
func (f *File) AddModuleStmt(path string) {
f.Module = &Module{
Mod: module.Version{Path: path},
}
if f.Syntax == nil {
f.Syntax = new(FileSyntax)
}
f.Syntax.Stmt = append(f.Syntax.Stmt, &Line{
Token: []string{"module", AutoQuote(path)},
})
}
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)
func Parse(file string, data []byte, fix VersionFixer) (*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)
case *LineBlock:
if len(x.Token) > 1 {
fmt.Fprintf(&errs, "%s:%d: unknown block type: %s\n", file, x.Start.Line, strings.Join(x.Token, " "))
continue
}
switch x.Token[0] {
default:
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)
}
}
}
}
if errs.Len() > 0 {
return nil, errors.New(strings.TrimRight(errs.String(), "\n"))
}
return f, nil
}
func (f *File) add(errs *bytes.Buffer, line *Line, verb string, args []string, fix VersionFixer) {
// TODO: We should pass in a flag saying whether this module is a dependency.
// If so, we should ignore all unknown directives and not attempt to parse
// replace and exclude either. They don't matter, and it will work better for
// forward compatibility if we can depend on modules that have local changes.
// TODO: For the target module (not dependencies), maybe we should
// relax the semver requirement and rewrite the file with updated info
// after resolving any versions. That would let people type commit hashes
// or tags or branch names, and then vgo would fix them.
switch verb {
default:
fmt.Fprintf(errs, "%s:%d: unknown directive: %s\n", f.Syntax.Name, line.Start.Line, verb)
case "module":
if f.Module != nil {
fmt.Fprintf(errs, "%s:%d: repeated module statement\n", f.Syntax.Name, line.Start.Line)
return
}
f.Module = new(Module)
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
}
v1, err := moduleMajorVersion(s)
if err != nil {
fmt.Fprintf(errs, "%s:%d: %v\n", f.Syntax.Name, line.Start.Line, err)
return
}
if v2 := semver.Major(v); v1 != v2 && (v1 != "v1" || v2 != "v0") {
fmt.Fprintf(errs, "%s:%d: invalid module: %s should be %s, not %s (%s)\n", f.Syntax.Name, line.Start.Line, s, v1, v2, v)
return
}
if verb == "require" {
f.Require = append(f.Require, &Require{
Mod: module.Version{Path: s, Version: v},
Syntax: line,
})
} else {
f.Exclude = append(f.Exclude, &Exclude{
Mod: module.Version{Path: s, Version: v},
Syntax: line,
})
}
case "replace":
if len(args) < 4 || len(args) > 5 || args[2] != "=>" {
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", 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
}
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
}
v1, err := moduleMajorVersion(s)
if err != nil {
fmt.Fprintf(errs, "%s:%d: %v\n", f.Syntax.Name, line.Start.Line, err)
return
}
if v2 := semver.Major(v); v1 != v2 && (v1 != "v1" || v2 != "v0") {
fmt.Fprintf(errs, "%s:%d: invalid module: %s should be %s, not %s (%s)\n", f.Syntax.Name, line.Start.Line, s, v1, v2, v)
return
}
ns, err := parseString(&args[3])
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) == 4 {
if !isDirectoryPath(ns) {
fmt.Fprintf(errs, "%s:%d: replacement module without version must be directory path (rooted or starting with ./ or ../)", 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)", f.Syntax.Name, line.Start.Line)
return
}
}
if len(args) == 5 {
old := args[4]
nv, err = parseVersion(ns, &args[4], 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", f.Syntax.Name, line.Start.Line, ns)
return
}
}
// TODO: More sanity checks about directories vs module paths.
f.Replace = append(f.Replace, &Replace{
Old: module.Version{Path: s, Version: v},
New: module.Version{Path: ns, Version: nv},
Syntax: line,
})
}
}
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] == ':'
}
func mustQuote(t string) bool {
for _, r := range t {
if !unicode.IsPrint(r) || r == ' ' || r == '"' || r == '\'' || r == '`' {
return true
}
}
return t == "" || strings.Contains(t, "//") || strings.Contains(t, "/*")
}
// 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 semver.IsValid(t) {
*s = semver.Canonical(t)
return *s, nil
}
return "", fmt.Errorf("version must be of the form v1.2.3")
}
func moduleMajorVersion(p string) (string, error) {
if _, _, major, _, ok := ParseGopkgIn(p); ok {
return major, nil
}
start := strings.LastIndex(p, "/") + 1
v := p[start:]
if !isMajorVersion(v) {
return "v1", nil
}
if v[1] == '0' || v == "v1" {
return "", fmt.Errorf("module path has invalid version number %s", v)
}
return v, nil
}
func isMajorVersion(v string) bool {
if len(v) < 2 || v[0] != 'v' {
return false
}
for i := 1; i < len(v); i++ {
if v[i] < '0' || '9' < v[i] {
return false
}
}
return true
}
func (f *File) Format() ([]byte, error) {
return Format(f.Syntax), nil
}
func (x *File) AddRequire(path, vers string) {
var syntax *Line
for i, stmt := range x.Syntax.Stmt {
switch stmt := stmt.(type) {
case *LineBlock:
if len(stmt.Token) > 0 && stmt.Token[0] == "require" {
syntax = &Line{Token: []string{AutoQuote(path), vers}}
stmt.Line = append(stmt.Line, syntax)
goto End
}
case *Line:
if len(stmt.Token) > 0 && stmt.Token[0] == "require" {
stmt.Token = stmt.Token[1:]
syntax = &Line{Token: []string{AutoQuote(path), vers}}
x.Syntax.Stmt[i] = &LineBlock{
Comments: stmt.Comments,
Token: []string{"require"},
Line: []*Line{
stmt,
syntax,
},
}
goto End
}
}
}
syntax = &Line{Token: []string{"require", AutoQuote(path), vers}}
x.Syntax.Stmt = append(x.Syntax.Stmt, syntax)
End:
x.Require = append(x.Require, &Require{module.Version{path, vers}, syntax})
}
func (f *File) SetRequire(req []module.Version) {
need := make(map[string]string)
for _, m := range req {
need[m.Path] = m.Version
}
for _, r := range f.Require {
if v, ok := need[r.Mod.Path]; ok {
r.Mod.Version = v
}
}
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 := strconv.Unquote(line.Token[0]); err == nil && need[p] != "" {
line.Token[1] = need[p]
delete(need, 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 := strconv.Unquote(stmt.Token[1]); err == nil && need[p] != "" {
stmt.Token[2] = need[p]
delete(need, p)
} else {
continue // drop stmt
}
}
}
newStmts = append(newStmts, stmt)
}
f.Syntax.Stmt = newStmts
for path, vers := range need {
f.AddRequire(path, vers)
}
f.SortBlocks()
}
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
}