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