1// Copyright 2021 The Go Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style 3// license that can be found in the LICENSE file. 4 5package modfile 6 7import ( 8 "fmt" 9 "sort" 10 "strings" 11) 12 13// A WorkFile is the parsed, interpreted form of a go.work file. 14type WorkFile struct { 15 Go *Go 16 Toolchain *Toolchain 17 Godebug []*Godebug 18 Use []*Use 19 Replace []*Replace 20 21 Syntax *FileSyntax 22} 23 24// A Use is a single directory statement. 25type Use struct { 26 Path string // Use path of module. 27 ModulePath string // Module path in the comment. 28 Syntax *Line 29} 30 31// ParseWork parses and returns a go.work file. 32// 33// file is the name of the file, used in positions and errors. 34// 35// data is the content of the file. 36// 37// fix is an optional function that canonicalizes module versions. 38// If fix is nil, all module versions must be canonical ([module.CanonicalVersion] 39// must return the same string). 40func ParseWork(file string, data []byte, fix VersionFixer) (*WorkFile, error) { 41 fs, err := parse(file, data) 42 if err != nil { 43 return nil, err 44 } 45 f := &WorkFile{ 46 Syntax: fs, 47 } 48 var errs ErrorList 49 50 for _, x := range fs.Stmt { 51 switch x := x.(type) { 52 case *Line: 53 f.add(&errs, x, x.Token[0], x.Token[1:], fix) 54 55 case *LineBlock: 56 if len(x.Token) > 1 { 57 errs = append(errs, Error{ 58 Filename: file, 59 Pos: x.Start, 60 Err: fmt.Errorf("unknown block type: %s", strings.Join(x.Token, " ")), 61 }) 62 continue 63 } 64 switch x.Token[0] { 65 default: 66 errs = append(errs, Error{ 67 Filename: file, 68 Pos: x.Start, 69 Err: fmt.Errorf("unknown block type: %s", strings.Join(x.Token, " ")), 70 }) 71 continue 72 case "godebug", "use", "replace": 73 for _, l := range x.Line { 74 f.add(&errs, l, x.Token[0], l.Token, fix) 75 } 76 } 77 } 78 } 79 80 if len(errs) > 0 { 81 return nil, errs 82 } 83 return f, nil 84} 85 86// Cleanup cleans up the file f after any edit operations. 87// To avoid quadratic behavior, modifications like [WorkFile.DropRequire] 88// clear the entry but do not remove it from the slice. 89// Cleanup cleans out all the cleared entries. 90func (f *WorkFile) Cleanup() { 91 w := 0 92 for _, r := range f.Use { 93 if r.Path != "" { 94 f.Use[w] = r 95 w++ 96 } 97 } 98 f.Use = f.Use[:w] 99 100 w = 0 101 for _, r := range f.Replace { 102 if r.Old.Path != "" { 103 f.Replace[w] = r 104 w++ 105 } 106 } 107 f.Replace = f.Replace[:w] 108 109 f.Syntax.Cleanup() 110} 111 112func (f *WorkFile) AddGoStmt(version string) error { 113 if !GoVersionRE.MatchString(version) { 114 return fmt.Errorf("invalid language version %q", version) 115 } 116 if f.Go == nil { 117 stmt := &Line{Token: []string{"go", version}} 118 f.Go = &Go{ 119 Version: version, 120 Syntax: stmt, 121 } 122 // Find the first non-comment-only block and add 123 // the go statement before it. That will keep file comments at the top. 124 i := 0 125 for i = 0; i < len(f.Syntax.Stmt); i++ { 126 if _, ok := f.Syntax.Stmt[i].(*CommentBlock); !ok { 127 break 128 } 129 } 130 f.Syntax.Stmt = append(append(f.Syntax.Stmt[:i:i], stmt), f.Syntax.Stmt[i:]...) 131 } else { 132 f.Go.Version = version 133 f.Syntax.updateLine(f.Go.Syntax, "go", version) 134 } 135 return nil 136} 137 138func (f *WorkFile) AddToolchainStmt(name string) error { 139 if !ToolchainRE.MatchString(name) { 140 return fmt.Errorf("invalid toolchain name %q", name) 141 } 142 if f.Toolchain == nil { 143 stmt := &Line{Token: []string{"toolchain", name}} 144 f.Toolchain = &Toolchain{ 145 Name: name, 146 Syntax: stmt, 147 } 148 // Find the go line and add the toolchain line after it. 149 // Or else find the first non-comment-only block and add 150 // the toolchain line before it. That will keep file comments at the top. 151 i := 0 152 for i = 0; i < len(f.Syntax.Stmt); i++ { 153 if line, ok := f.Syntax.Stmt[i].(*Line); ok && len(line.Token) > 0 && line.Token[0] == "go" { 154 i++ 155 goto Found 156 } 157 } 158 for i = 0; i < len(f.Syntax.Stmt); i++ { 159 if _, ok := f.Syntax.Stmt[i].(*CommentBlock); !ok { 160 break 161 } 162 } 163 Found: 164 f.Syntax.Stmt = append(append(f.Syntax.Stmt[:i:i], stmt), f.Syntax.Stmt[i:]...) 165 } else { 166 f.Toolchain.Name = name 167 f.Syntax.updateLine(f.Toolchain.Syntax, "toolchain", name) 168 } 169 return nil 170} 171 172// DropGoStmt deletes the go statement from the file. 173func (f *WorkFile) DropGoStmt() { 174 if f.Go != nil { 175 f.Go.Syntax.markRemoved() 176 f.Go = nil 177 } 178} 179 180// DropToolchainStmt deletes the toolchain statement from the file. 181func (f *WorkFile) DropToolchainStmt() { 182 if f.Toolchain != nil { 183 f.Toolchain.Syntax.markRemoved() 184 f.Toolchain = nil 185 } 186} 187 188// AddGodebug sets the first godebug line for key to value, 189// preserving any existing comments for that line and removing all 190// other godebug lines for key. 191// 192// If no line currently exists for key, AddGodebug adds a new line 193// at the end of the last godebug block. 194func (f *WorkFile) AddGodebug(key, value string) error { 195 need := true 196 for _, g := range f.Godebug { 197 if g.Key == key { 198 if need { 199 g.Value = value 200 f.Syntax.updateLine(g.Syntax, "godebug", key+"="+value) 201 need = false 202 } else { 203 g.Syntax.markRemoved() 204 *g = Godebug{} 205 } 206 } 207 } 208 209 if need { 210 f.addNewGodebug(key, value) 211 } 212 return nil 213} 214 215// addNewGodebug adds a new godebug key=value line at the end 216// of the last godebug block, regardless of any existing godebug lines for key. 217func (f *WorkFile) addNewGodebug(key, value string) { 218 line := f.Syntax.addLine(nil, "godebug", key+"="+value) 219 g := &Godebug{ 220 Key: key, 221 Value: value, 222 Syntax: line, 223 } 224 f.Godebug = append(f.Godebug, g) 225} 226 227func (f *WorkFile) DropGodebug(key string) error { 228 for _, g := range f.Godebug { 229 if g.Key == key { 230 g.Syntax.markRemoved() 231 *g = Godebug{} 232 } 233 } 234 return nil 235} 236 237func (f *WorkFile) AddUse(diskPath, modulePath string) error { 238 need := true 239 for _, d := range f.Use { 240 if d.Path == diskPath { 241 if need { 242 d.ModulePath = modulePath 243 f.Syntax.updateLine(d.Syntax, "use", AutoQuote(diskPath)) 244 need = false 245 } else { 246 d.Syntax.markRemoved() 247 *d = Use{} 248 } 249 } 250 } 251 252 if need { 253 f.AddNewUse(diskPath, modulePath) 254 } 255 return nil 256} 257 258func (f *WorkFile) AddNewUse(diskPath, modulePath string) { 259 line := f.Syntax.addLine(nil, "use", AutoQuote(diskPath)) 260 f.Use = append(f.Use, &Use{Path: diskPath, ModulePath: modulePath, Syntax: line}) 261} 262 263func (f *WorkFile) SetUse(dirs []*Use) { 264 need := make(map[string]string) 265 for _, d := range dirs { 266 need[d.Path] = d.ModulePath 267 } 268 269 for _, d := range f.Use { 270 if modulePath, ok := need[d.Path]; ok { 271 d.ModulePath = modulePath 272 } else { 273 d.Syntax.markRemoved() 274 *d = Use{} 275 } 276 } 277 278 // TODO(#45713): Add module path to comment. 279 280 for diskPath, modulePath := range need { 281 f.AddNewUse(diskPath, modulePath) 282 } 283 f.SortBlocks() 284} 285 286func (f *WorkFile) DropUse(path string) error { 287 for _, d := range f.Use { 288 if d.Path == path { 289 d.Syntax.markRemoved() 290 *d = Use{} 291 } 292 } 293 return nil 294} 295 296func (f *WorkFile) AddReplace(oldPath, oldVers, newPath, newVers string) error { 297 return addReplace(f.Syntax, &f.Replace, oldPath, oldVers, newPath, newVers) 298} 299 300func (f *WorkFile) DropReplace(oldPath, oldVers string) error { 301 for _, r := range f.Replace { 302 if r.Old.Path == oldPath && r.Old.Version == oldVers { 303 r.Syntax.markRemoved() 304 *r = Replace{} 305 } 306 } 307 return nil 308} 309 310func (f *WorkFile) SortBlocks() { 311 f.removeDups() // otherwise sorting is unsafe 312 313 for _, stmt := range f.Syntax.Stmt { 314 block, ok := stmt.(*LineBlock) 315 if !ok { 316 continue 317 } 318 sort.SliceStable(block.Line, func(i, j int) bool { 319 return lineLess(block.Line[i], block.Line[j]) 320 }) 321 } 322} 323 324// removeDups removes duplicate replace directives. 325// 326// Later replace directives take priority. 327// 328// require directives are not de-duplicated. That's left up to higher-level 329// logic (MVS). 330// 331// retract directives are not de-duplicated since comments are 332// meaningful, and versions may be retracted multiple times. 333func (f *WorkFile) removeDups() { 334 removeDups(f.Syntax, nil, &f.Replace) 335} 336