123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285 |
- // 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
- 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 "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 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
- }
- }
- 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)
- }
|