336 lines
7.7 KiB
Go
336 lines
7.7 KiB
Go
// Copyright 2021 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 (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// A WorkFile is the parsed, interpreted form of a go.work file.
|
|
type WorkFile struct {
|
|
Go *Go
|
|
Toolchain *Toolchain
|
|
Godebug []*Godebug
|
|
Use []*Use
|
|
Replace []*Replace
|
|
|
|
Syntax *FileSyntax
|
|
}
|
|
|
|
// A Use is a single directory statement.
|
|
type Use struct {
|
|
Path string // Use path of module.
|
|
ModulePath string // Module path in the comment.
|
|
Syntax *Line
|
|
}
|
|
|
|
// ParseWork parses and returns a go.work 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 ParseWork(file string, data []byte, fix VersionFixer) (*WorkFile, error) {
|
|
fs, err := parse(file, data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := &WorkFile{
|
|
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)
|
|
|
|
case *LineBlock:
|
|
if len(x.Token) > 1 {
|
|
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:
|
|
errs = append(errs, Error{
|
|
Filename: file,
|
|
Pos: x.Start,
|
|
Err: fmt.Errorf("unknown block type: %s", strings.Join(x.Token, " ")),
|
|
})
|
|
continue
|
|
case "godebug", "use", "replace":
|
|
for _, l := range x.Line {
|
|
f.add(&errs, l, x.Token[0], l.Token, fix)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(errs) > 0 {
|
|
return nil, errs
|
|
}
|
|
return f, nil
|
|
}
|
|
|
|
// Cleanup cleans up the file f after any edit operations.
|
|
// To avoid quadratic behavior, modifications like [WorkFile.DropRequire]
|
|
// clear the entry but do not remove it from the slice.
|
|
// Cleanup cleans out all the cleared entries.
|
|
func (f *WorkFile) Cleanup() {
|
|
w := 0
|
|
for _, r := range f.Use {
|
|
if r.Path != "" {
|
|
f.Use[w] = r
|
|
w++
|
|
}
|
|
}
|
|
f.Use = f.Use[: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 *WorkFile) AddGoStmt(version string) error {
|
|
if !GoVersionRE.MatchString(version) {
|
|
return fmt.Errorf("invalid language version %q", version)
|
|
}
|
|
if f.Go == nil {
|
|
stmt := &Line{Token: []string{"go", version}}
|
|
f.Go = &Go{
|
|
Version: version,
|
|
Syntax: stmt,
|
|
}
|
|
// Find the first non-comment-only block and add
|
|
// the go statement before it. That will keep file comments at the top.
|
|
i := 0
|
|
for i = 0; i < len(f.Syntax.Stmt); i++ {
|
|
if _, ok := f.Syntax.Stmt[i].(*CommentBlock); !ok {
|
|
break
|
|
}
|
|
}
|
|
f.Syntax.Stmt = append(append(f.Syntax.Stmt[:i:i], stmt), f.Syntax.Stmt[i:]...)
|
|
} else {
|
|
f.Go.Version = version
|
|
f.Syntax.updateLine(f.Go.Syntax, "go", version)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (f *WorkFile) AddToolchainStmt(name string) error {
|
|
if !ToolchainRE.MatchString(name) {
|
|
return fmt.Errorf("invalid toolchain name %q", name)
|
|
}
|
|
if f.Toolchain == nil {
|
|
stmt := &Line{Token: []string{"toolchain", name}}
|
|
f.Toolchain = &Toolchain{
|
|
Name: name,
|
|
Syntax: stmt,
|
|
}
|
|
// Find the go line and add the toolchain line after it.
|
|
// Or else find the first non-comment-only block and add
|
|
// the toolchain line before it. That will keep file comments at the top.
|
|
i := 0
|
|
for i = 0; i < len(f.Syntax.Stmt); i++ {
|
|
if line, ok := f.Syntax.Stmt[i].(*Line); ok && len(line.Token) > 0 && line.Token[0] == "go" {
|
|
i++
|
|
goto Found
|
|
}
|
|
}
|
|
for i = 0; i < len(f.Syntax.Stmt); i++ {
|
|
if _, ok := f.Syntax.Stmt[i].(*CommentBlock); !ok {
|
|
break
|
|
}
|
|
}
|
|
Found:
|
|
f.Syntax.Stmt = append(append(f.Syntax.Stmt[:i:i], stmt), f.Syntax.Stmt[i:]...)
|
|
} else {
|
|
f.Toolchain.Name = name
|
|
f.Syntax.updateLine(f.Toolchain.Syntax, "toolchain", name)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// DropGoStmt deletes the go statement from the file.
|
|
func (f *WorkFile) DropGoStmt() {
|
|
if f.Go != nil {
|
|
f.Go.Syntax.markRemoved()
|
|
f.Go = nil
|
|
}
|
|
}
|
|
|
|
// DropToolchainStmt deletes the toolchain statement from the file.
|
|
func (f *WorkFile) DropToolchainStmt() {
|
|
if f.Toolchain != nil {
|
|
f.Toolchain.Syntax.markRemoved()
|
|
f.Toolchain = nil
|
|
}
|
|
}
|
|
|
|
// AddGodebug sets the first godebug line for key to value,
|
|
// preserving any existing comments for that line and removing all
|
|
// other godebug lines for key.
|
|
//
|
|
// If no line currently exists for key, AddGodebug adds a new line
|
|
// at the end of the last godebug block.
|
|
func (f *WorkFile) AddGodebug(key, value string) error {
|
|
need := true
|
|
for _, g := range f.Godebug {
|
|
if g.Key == key {
|
|
if need {
|
|
g.Value = value
|
|
f.Syntax.updateLine(g.Syntax, "godebug", key+"="+value)
|
|
need = false
|
|
} else {
|
|
g.Syntax.markRemoved()
|
|
*g = Godebug{}
|
|
}
|
|
}
|
|
}
|
|
|
|
if need {
|
|
f.addNewGodebug(key, value)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// addNewGodebug adds a new godebug key=value line at the end
|
|
// of the last godebug block, regardless of any existing godebug lines for key.
|
|
func (f *WorkFile) addNewGodebug(key, value string) {
|
|
line := f.Syntax.addLine(nil, "godebug", key+"="+value)
|
|
g := &Godebug{
|
|
Key: key,
|
|
Value: value,
|
|
Syntax: line,
|
|
}
|
|
f.Godebug = append(f.Godebug, g)
|
|
}
|
|
|
|
func (f *WorkFile) DropGodebug(key string) error {
|
|
for _, g := range f.Godebug {
|
|
if g.Key == key {
|
|
g.Syntax.markRemoved()
|
|
*g = Godebug{}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (f *WorkFile) AddUse(diskPath, modulePath string) error {
|
|
need := true
|
|
for _, d := range f.Use {
|
|
if d.Path == diskPath {
|
|
if need {
|
|
d.ModulePath = modulePath
|
|
f.Syntax.updateLine(d.Syntax, "use", AutoQuote(diskPath))
|
|
need = false
|
|
} else {
|
|
d.Syntax.markRemoved()
|
|
*d = Use{}
|
|
}
|
|
}
|
|
}
|
|
|
|
if need {
|
|
f.AddNewUse(diskPath, modulePath)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (f *WorkFile) AddNewUse(diskPath, modulePath string) {
|
|
line := f.Syntax.addLine(nil, "use", AutoQuote(diskPath))
|
|
f.Use = append(f.Use, &Use{Path: diskPath, ModulePath: modulePath, Syntax: line})
|
|
}
|
|
|
|
func (f *WorkFile) SetUse(dirs []*Use) {
|
|
need := make(map[string]string)
|
|
for _, d := range dirs {
|
|
need[d.Path] = d.ModulePath
|
|
}
|
|
|
|
for _, d := range f.Use {
|
|
if modulePath, ok := need[d.Path]; ok {
|
|
d.ModulePath = modulePath
|
|
} else {
|
|
d.Syntax.markRemoved()
|
|
*d = Use{}
|
|
}
|
|
}
|
|
|
|
// TODO(#45713): Add module path to comment.
|
|
|
|
for diskPath, modulePath := range need {
|
|
f.AddNewUse(diskPath, modulePath)
|
|
}
|
|
f.SortBlocks()
|
|
}
|
|
|
|
func (f *WorkFile) DropUse(path string) error {
|
|
for _, d := range f.Use {
|
|
if d.Path == path {
|
|
d.Syntax.markRemoved()
|
|
*d = Use{}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (f *WorkFile) AddReplace(oldPath, oldVers, newPath, newVers string) error {
|
|
return addReplace(f.Syntax, &f.Replace, oldPath, oldVers, newPath, newVers)
|
|
}
|
|
|
|
func (f *WorkFile) 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
|
|
}
|
|
|
|
func (f *WorkFile) SortBlocks() {
|
|
f.removeDups() // otherwise sorting is unsafe
|
|
|
|
for _, stmt := range f.Syntax.Stmt {
|
|
block, ok := stmt.(*LineBlock)
|
|
if !ok {
|
|
continue
|
|
}
|
|
sort.SliceStable(block.Line, func(i, j int) bool {
|
|
return lineLess(block.Line[i], block.Line[j])
|
|
})
|
|
}
|
|
}
|
|
|
|
// removeDups removes duplicate replace directives.
|
|
//
|
|
// 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 *WorkFile) removeDups() {
|
|
removeDups(f.Syntax, nil, &f.Replace, nil)
|
|
}
|