Ver Fonte

implement module compatibility check

This package imports all "importable" packages, i.e., packages that:

- are not applications ("main")
- are not internal
- and that have non-test go-files

We do this to verify that our code can be consumed as a dependency
in "module mode". When using a dependency that does not have a go.mod
(i.e.; is not a "module"), go implicitly generates a go.mod. Lacking
information from the dependency itself, it assumes "go1.16" language
(see [DefaultGoModVersion]). Starting with Go1.21, go downgrades the
language version used for such dependencies, which means that any
language feature used that is not supported by go1.16 results in a
compile error;

    # github.com/docker/cli/cli/context/store
    /go/pkg/mod/github.com/docker/cli@v25.0.0-beta.2+incompatible/cli/context/store/storeconfig.go:6:24: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    /go/pkg/mod/github.com/docker/cli@v25.0.0-beta.2+incompatible/cli/context/store/store.go:74:12: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)

These errors do NOT occur when using GOPATH mode, nor do they occur
when using "pseudo module mode" (the "-mod=mod -modfile=vendor.mod"
approach used in this repository).

As a workaround for this situation, we must include "//go:build" comments
in any file that uses newer go-language features (such as the "any" type
or the "min()", "max()" builtins).

From the go toolchain docs (https://go.dev/doc/toolchain):

> The go line for each module sets the language version the compiler enforces
> when compiling packages in that module. The language version can be changed
> on a per-file basis by using a build constraint.
>
> For example, a module containing code that uses the Go 1.21 language version
> should have a go.mod file with a go line such as go 1.21 or go 1.21.3.
> If a specific source file should be compiled only when using a newer Go
> toolchain, adding //go:build go1.22 to that source file both ensures that
> only Go 1.22 and newer toolchains will compile the file and also changes
> the language version in that file to Go 1.22.

This file is a generated module that imports all packages provided in
the repository, which replicates an external consumer using our code
as a dependency in go-module mode, and verifies all files in those
packages have the correct "//go:build <go language version>" set.

To test this package:

    make -C ./internal/gocompat/
    GO111MODULE=off go generate .
    go mod tidy
    go test -v
    # github.com/docker/docker/libnetwork/options
    ../../libnetwork/options/options.go:45:25: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    # github.com/docker/docker/libnetwork/internal/setmatrix
    ../../libnetwork/internal/setmatrix/setmatrix.go:13:16: type parameter requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../libnetwork/internal/setmatrix/setmatrix.go:13:18: predeclared comparable requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../libnetwork/internal/setmatrix/setmatrix.go:14:20: type instantiation requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../libnetwork/internal/setmatrix/setmatrix.go:20:10: type instantiation requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../libnetwork/internal/setmatrix/setmatrix.go:31:10: type instantiation requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../libnetwork/internal/setmatrix/setmatrix.go:43:10: type instantiation requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../libnetwork/internal/setmatrix/setmatrix.go:59:10: type instantiation requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../libnetwork/internal/setmatrix/setmatrix.go:80:10: type instantiation requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../libnetwork/internal/setmatrix/setmatrix.go:93:10: type instantiation requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../libnetwork/internal/setmatrix/setmatrix.go:104:10: type instantiation requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../libnetwork/internal/setmatrix/setmatrix.go:104:10: too many errors
    # github.com/docker/docker/libnetwork/config
    ../../libnetwork/config/config.go:35:47: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../libnetwork/config/config.go:47:41: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../libnetwork/config/config.go:63:55: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../libnetwork/config/config.go:95:63: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    # github.com/docker/docker/testutil
    ../../testutil/helpers.go:80:9: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    # github.com/docker/docker/builder/builder-next/adapters/containerimage
    ../../builder/builder-next/adapters/containerimage/pull.go:72:4: type instantiation requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../builder/builder-next/adapters/containerimage/pull.go:200:19: type instantiation requires go1.18 or later (-lang was set to go1.16; check go.mod)
    FAIL	gocompat [build failed]
    make: *** [Makefile:5: verify] Error 1

[DefaultGoModVersion]: https://github.com/golang/go/blob/58c28ba286dd0e98fe4cca80f5d64bbcb824a685/src/cmd/go/internal/gover/version.go#L15-L24
[2]: https://go.dev/doc/toolchain

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
Sebastiaan van Stijn há 1 ano atrás
pai
commit
1be116a15d

+ 4 - 0
internal/gocompat/.gitignore

@@ -0,0 +1,4 @@
+go.mod
+go.sum
+main.go
+main_test.go

+ 12 - 0
internal/gocompat/Makefile

@@ -0,0 +1,12 @@
+.PHONY: verify
+verify: generate
+	GO111MODULE=on go test -v
+
+.PHONY: generate
+generate: clean
+	GO111MODULE=off go generate .
+	GO111MODULE=on go mod tidy
+
+.PHONY: clean
+clean:
+	@rm -f go.mod go.sum main.go main_test.go

+ 6 - 0
internal/gocompat/generate.go

@@ -0,0 +1,6 @@
+package main
+
+//go:generate go run modulegenerator.go
+
+// make sure the modfile package is vendored.
+import _ "golang.org/x/mod/modfile"

+ 193 - 0
internal/gocompat/modulegenerator.go

@@ -0,0 +1,193 @@
+//go:build ignore
+
+package main
+
+import (
+	"bytes"
+	"fmt"
+	"log"
+	"os"
+	"os/exec"
+	"strings"
+	"text/template"
+
+	"golang.org/x/mod/modfile"
+)
+
+func main() {
+	if err := generateApp(); err != nil {
+		log.Fatal(err)
+	}
+	if err := generateModule(); err != nil {
+		log.Fatal(err)
+	}
+}
+
+func generateApp() error {
+	cmd := exec.Command("go", "list", "-find", "-f", `{{- if ne .Name "main"}}{{if .GoFiles}}{{.ImportPath}}{{end}}{{end -}}`, "../../...")
+	out, err := cmd.CombinedOutput()
+	if err != nil {
+		return err
+	}
+
+	var pkgs []string
+	for _, p := range strings.Split(string(out), "\n") {
+		if strings.TrimSpace(p) == "" || strings.Contains(p, "/internal") {
+			continue
+		}
+		pkgs = append(pkgs, p)
+	}
+	tmpl, err := template.New("main").Parse(appTemplate)
+	if err != nil {
+		return err
+	}
+
+	var buf bytes.Buffer
+	err = tmpl.Execute(&buf, appContext{Generator: cmd.String(), Packages: pkgs})
+	if err != nil {
+		return err
+	}
+
+	return os.WriteFile("main_test.go", buf.Bytes(), 0o644)
+}
+
+func generateModule() error {
+	content, err := os.ReadFile("../../go.mod")
+	if err != nil {
+		if !os.IsNotExist(err) {
+			return err
+		}
+		content = []byte("module github.com/docker/docker\n")
+		if err := os.WriteFile("../../go.mod", content, 0o644); err != nil {
+			return err
+		}
+		// Let's be nice, and remove the go.mod if we created it.
+		// FIXME(thaJeztah): we need to clean up the go.mod after running the test, but need to know if we created it (or if it was an existing go.mod)
+		// defer os.Remove("../../go.mod")
+	} else {
+		log.Println("WARN: go.mod exists in the repository root!")
+		log.Println("WARN: Using your go.mod instead of our generated version -- this may misbehave!")
+	}
+	mod, err := modfile.Parse("../../go.mod", content, nil)
+	if err != nil {
+		return err
+	}
+	if mod.Go != nil && mod.Go.Version != "" {
+		return fmt.Errorf("main go.mod must not contain a go version")
+	}
+	content, err = os.ReadFile("../../vendor.mod")
+	if err != nil {
+		return err
+	}
+	mod, err = modfile.Parse("../../vendor.mod", content, nil)
+	if err != nil {
+		return err
+	}
+	if err := mod.AddModuleStmt("gocompat"); err != nil {
+		return err
+	}
+	if err := mod.AddReplace("github.com/docker/docker", "", "../../", ""); err != nil {
+		return err
+	}
+	if err := mod.AddGoStmt("1.21"); err != nil {
+		return err
+	}
+	out, err := mod.Format()
+	if err != nil {
+		return err
+	}
+	tmpl, err := template.New("mod").Parse(modTemplate)
+	if err != nil {
+		return err
+	}
+
+	gen, _ := os.Executable()
+
+	var buf bytes.Buffer
+	err = tmpl.Execute(&buf, appContext{Generator: gen, Dependencies: string(out)})
+	if err != nil {
+		return err
+	}
+
+	return os.WriteFile("go.mod", buf.Bytes(), 0o644)
+}
+
+type appContext struct {
+	Generator    string
+	Packages     []string
+	Dependencies string
+}
+
+const appTemplate = `// Code generated by "{{ .Generator }}". DO NOT EDIT.
+
+package main_test
+
+import (
+	"testing"
+
+	// Import all importable packages, i.e., packages that:
+	//
+	// - are not applications ("main")
+	// - are not internal
+	// - and that have non-test go-files
+{{- range .Packages }}
+ 	_ "{{ . }}"
+{{- end}}
+)
+
+// This file import all "importable" packages, i.e., packages that:
+//
+// - are not applications ("main")
+// - are not internal
+// - and that have non-test go-files
+//
+// We do this to verify that our code can be consumed as a dependency
+// in "module mode". When using a dependency that does not have a go.mod
+// (i.e.; is not a "module"), go implicitly generates a go.mod. Lacking
+// information from the dependency itself, it assumes "go1.16" language
+// (see [DefaultGoModVersion]). Starting with Go1.21, go downgrades the
+// language version used for such dependencies, which means that any
+// language feature used that is not supported by go1.16 results in a
+// compile error;
+//
+//	# github.com/docker/cli/cli/context/store
+//	/go/pkg/mod/github.com/docker/cli@v25.0.0-beta.2+incompatible/cli/context/store/storeconfig.go:6:24: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
+//	/go/pkg/mod/github.com/docker/cli@v25.0.0-beta.2+incompatible/cli/context/store/store.go:74:12: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
+// 
+// These errors do NOT occur when using GOPATH mode, nor do they occur
+// when using "pseudo module mode" (the "-mod=mod -modfile=vendor.mod"
+// approach used in this repository).
+//
+// As a workaround for this situation, we must include "//go:build" comments
+// in any file that uses newer go-language features (such as the "any" type
+// or the "min()", "max()" builtins).
+//
+// From the go toolchain docs (https://go.dev/doc/toolchain):
+//
+// > The go line for each module sets the language version the compiler enforces
+// > when compiling packages in that module. The language version can be changed
+// > on a per-file basis by using a build constraint.
+// > 
+// > For example, a module containing code that uses the Go 1.21 language version
+// > should have a go.mod file with a go line such as go 1.21 or go 1.21.3.
+// > If a specific source file should be compiled only when using a newer Go
+// > toolchain, adding //go:build go1.22 to that source file both ensures that
+// > only Go 1.22 and newer toolchains will compile the file and also changes
+// > the language version in that file to Go 1.22.
+//
+// This file is a generated module that imports all packages provided in
+// the repository, which replicates an external consumer using our code
+// as a dependency in go-module mode, and verifies all files in those
+// packages have the correct "//go:build <go language version>" set.
+//
+// [DefaultGoModVersion]: https://github.com/golang/go/blob/58c28ba286dd0e98fe4cca80f5d64bbcb824a685/src/cmd/go/internal/gover/version.go#L15-L24
+// [2]: https://go.dev/doc/toolchain
+func TestModuleCompatibllity(t *testing.T) {
+	t.Log("all packages have the correct go version specified through //go:build")
+}
+`
+
+const modTemplate = `// Code generated by "{{ .Generator }}". DO NOT EDIT.
+
+{{.Dependencies}}
+`

+ 184 - 0
vendor/golang.org/x/mod/modfile/print.go

@@ -0,0 +1,184 @@
+// 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.
+
+// Module file printer.
+
+package modfile
+
+import (
+	"bytes"
+	"fmt"
+	"strings"
+)
+
+// Format returns a go.mod file as a byte slice, formatted in standard style.
+func Format(f *FileSyntax) []byte {
+	pr := &printer{}
+	pr.file(f)
+
+	// remove trailing blank lines
+	b := pr.Bytes()
+	for len(b) > 0 && b[len(b)-1] == '\n' && (len(b) == 1 || b[len(b)-2] == '\n') {
+		b = b[:len(b)-1]
+	}
+	return b
+}
+
+// A printer collects the state during printing of a file or expression.
+type printer struct {
+	bytes.Buffer           // output buffer
+	comment      []Comment // pending end-of-line comments
+	margin       int       // left margin (indent), a number of tabs
+}
+
+// printf prints to the buffer.
+func (p *printer) printf(format string, args ...interface{}) {
+	fmt.Fprintf(p, format, args...)
+}
+
+// indent returns the position on the current line, in bytes, 0-indexed.
+func (p *printer) indent() int {
+	b := p.Bytes()
+	n := 0
+	for n < len(b) && b[len(b)-1-n] != '\n' {
+		n++
+	}
+	return n
+}
+
+// newline ends the current line, flushing end-of-line comments.
+func (p *printer) newline() {
+	if len(p.comment) > 0 {
+		p.printf(" ")
+		for i, com := range p.comment {
+			if i > 0 {
+				p.trim()
+				p.printf("\n")
+				for i := 0; i < p.margin; i++ {
+					p.printf("\t")
+				}
+			}
+			p.printf("%s", strings.TrimSpace(com.Token))
+		}
+		p.comment = p.comment[:0]
+	}
+
+	p.trim()
+	if b := p.Bytes(); len(b) == 0 || (len(b) >= 2 && b[len(b)-1] == '\n' && b[len(b)-2] == '\n') {
+		// skip the blank line at top of file or after a blank line
+	} else {
+		p.printf("\n")
+	}
+	for i := 0; i < p.margin; i++ {
+		p.printf("\t")
+	}
+}
+
+// trim removes trailing spaces and tabs from the current line.
+func (p *printer) trim() {
+	// Remove trailing spaces and tabs from line we're about to end.
+	b := p.Bytes()
+	n := len(b)
+	for n > 0 && (b[n-1] == '\t' || b[n-1] == ' ') {
+		n--
+	}
+	p.Truncate(n)
+}
+
+// file formats the given file into the print buffer.
+func (p *printer) file(f *FileSyntax) {
+	for _, com := range f.Before {
+		p.printf("%s", strings.TrimSpace(com.Token))
+		p.newline()
+	}
+
+	for i, stmt := range f.Stmt {
+		switch x := stmt.(type) {
+		case *CommentBlock:
+			// comments already handled
+			p.expr(x)
+
+		default:
+			p.expr(x)
+			p.newline()
+		}
+
+		for _, com := range stmt.Comment().After {
+			p.printf("%s", strings.TrimSpace(com.Token))
+			p.newline()
+		}
+
+		if i+1 < len(f.Stmt) {
+			p.newline()
+		}
+	}
+}
+
+func (p *printer) expr(x Expr) {
+	// Emit line-comments preceding this expression.
+	if before := x.Comment().Before; len(before) > 0 {
+		// Want to print a line comment.
+		// Line comments must be at the current margin.
+		p.trim()
+		if p.indent() > 0 {
+			// There's other text on the line. Start a new line.
+			p.printf("\n")
+		}
+		// Re-indent to margin.
+		for i := 0; i < p.margin; i++ {
+			p.printf("\t")
+		}
+		for _, com := range before {
+			p.printf("%s", strings.TrimSpace(com.Token))
+			p.newline()
+		}
+	}
+
+	switch x := x.(type) {
+	default:
+		panic(fmt.Errorf("printer: unexpected type %T", x))
+
+	case *CommentBlock:
+		// done
+
+	case *LParen:
+		p.printf("(")
+	case *RParen:
+		p.printf(")")
+
+	case *Line:
+		p.tokens(x.Token)
+
+	case *LineBlock:
+		p.tokens(x.Token)
+		p.printf(" ")
+		p.expr(&x.LParen)
+		p.margin++
+		for _, l := range x.Line {
+			p.newline()
+			p.expr(l)
+		}
+		p.margin--
+		p.newline()
+		p.expr(&x.RParen)
+	}
+
+	// Queue end-of-line comments for printing when we
+	// reach the end of the line.
+	p.comment = append(p.comment, x.Comment().Suffix...)
+}
+
+func (p *printer) tokens(tokens []string) {
+	sep := ""
+	for _, t := range tokens {
+		if t == "," || t == ")" || t == "]" || t == "}" {
+			sep = ""
+		}
+		p.printf("%s%s", sep, t)
+		sep = " "
+		if t == "(" || t == "[" || t == "{" {
+			sep = ""
+		}
+	}
+}

+ 958 - 0
vendor/golang.org/x/mod/modfile/read.go

@@ -0,0 +1,958 @@
+// 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"
+	"os"
+	"strconv"
+	"strings"
+	"unicode"
+	"unicode/utf8"
+)
+
+// A Position describes an arbitrary source position in a file, including the
+// file, line, column, and byte offset.
+type Position struct {
+	Line     int // line in input (starting at 1)
+	LineRune int // rune in line (starting at 1)
+	Byte     int // byte in input (starting at 0)
+}
+
+// add returns the position at the end of s, assuming it starts at p.
+func (p Position) add(s string) Position {
+	p.Byte += len(s)
+	if n := strings.Count(s, "\n"); n > 0 {
+		p.Line += n
+		s = s[strings.LastIndex(s, "\n")+1:]
+		p.LineRune = 1
+	}
+	p.LineRune += utf8.RuneCountInString(s)
+	return p
+}
+
+// An Expr represents an input element.
+type Expr interface {
+	// Span returns the start and end position of the expression,
+	// excluding leading or trailing comments.
+	Span() (start, end Position)
+
+	// Comment returns the comments attached to the expression.
+	// This method would normally be named 'Comments' but that
+	// would interfere with embedding a type of the same name.
+	Comment() *Comments
+}
+
+// A Comment represents a single // comment.
+type Comment struct {
+	Start  Position
+	Token  string // without trailing newline
+	Suffix bool   // an end of line (not whole line) comment
+}
+
+// Comments collects the comments associated with an expression.
+type Comments struct {
+	Before []Comment // whole-line comments before this expression
+	Suffix []Comment // end-of-line comments after this expression
+
+	// For top-level expressions only, After lists whole-line
+	// comments following the expression.
+	After []Comment
+}
+
+// Comment returns the receiver. This isn't useful by itself, but
+// a Comments struct is embedded into all the expression
+// implementation types, and this gives each of those a Comment
+// method to satisfy the Expr interface.
+func (c *Comments) Comment() *Comments {
+	return c
+}
+
+// A FileSyntax represents an entire go.mod file.
+type FileSyntax struct {
+	Name string // file path
+	Comments
+	Stmt []Expr
+}
+
+func (x *FileSyntax) Span() (start, end Position) {
+	if len(x.Stmt) == 0 {
+		return
+	}
+	start, _ = x.Stmt[0].Span()
+	_, end = x.Stmt[len(x.Stmt)-1].Span()
+	return start, end
+}
+
+// addLine adds a line containing the given tokens to the file.
+//
+// If the first token of the hint matches the first token of the
+// line, the new line is added at the end of the block containing hint,
+// extracting hint into a new block if it is not yet in one.
+//
+// If the hint is non-nil buts its first token does not match,
+// the new line is added after the block containing hint
+// (or hint itself, if not in a block).
+//
+// If no hint is provided, addLine appends the line to the end of
+// the last block with a matching first token,
+// or to the end of the file if no such block exists.
+func (x *FileSyntax) addLine(hint Expr, tokens ...string) *Line {
+	if hint == nil {
+		// If no hint given, add to the last statement of the given type.
+	Loop:
+		for i := len(x.Stmt) - 1; i >= 0; i-- {
+			stmt := x.Stmt[i]
+			switch stmt := stmt.(type) {
+			case *Line:
+				if stmt.Token != nil && stmt.Token[0] == tokens[0] {
+					hint = stmt
+					break Loop
+				}
+			case *LineBlock:
+				if stmt.Token[0] == tokens[0] {
+					hint = stmt
+					break Loop
+				}
+			}
+		}
+	}
+
+	newLineAfter := func(i int) *Line {
+		new := &Line{Token: tokens}
+		if i == len(x.Stmt) {
+			x.Stmt = append(x.Stmt, new)
+		} else {
+			x.Stmt = append(x.Stmt, nil)
+			copy(x.Stmt[i+2:], x.Stmt[i+1:])
+			x.Stmt[i+1] = new
+		}
+		return new
+	}
+
+	if hint != nil {
+		for i, stmt := range x.Stmt {
+			switch stmt := stmt.(type) {
+			case *Line:
+				if stmt == hint {
+					if stmt.Token == nil || stmt.Token[0] != tokens[0] {
+						return newLineAfter(i)
+					}
+
+					// Convert line to line block.
+					stmt.InBlock = true
+					block := &LineBlock{Token: stmt.Token[:1], Line: []*Line{stmt}}
+					stmt.Token = stmt.Token[1:]
+					x.Stmt[i] = block
+					new := &Line{Token: tokens[1:], InBlock: true}
+					block.Line = append(block.Line, new)
+					return new
+				}
+
+			case *LineBlock:
+				if stmt == hint {
+					if stmt.Token[0] != tokens[0] {
+						return newLineAfter(i)
+					}
+
+					new := &Line{Token: tokens[1:], InBlock: true}
+					stmt.Line = append(stmt.Line, new)
+					return new
+				}
+
+				for j, line := range stmt.Line {
+					if line == hint {
+						if stmt.Token[0] != tokens[0] {
+							return newLineAfter(i)
+						}
+
+						// Add new line after hint within the block.
+						stmt.Line = append(stmt.Line, nil)
+						copy(stmt.Line[j+2:], stmt.Line[j+1:])
+						new := &Line{Token: tokens[1:], InBlock: true}
+						stmt.Line[j+1] = new
+						return new
+					}
+				}
+			}
+		}
+	}
+
+	new := &Line{Token: tokens}
+	x.Stmt = append(x.Stmt, new)
+	return new
+}
+
+func (x *FileSyntax) updateLine(line *Line, tokens ...string) {
+	if line.InBlock {
+		tokens = tokens[1:]
+	}
+	line.Token = tokens
+}
+
+// markRemoved modifies line so that it (and its end-of-line comment, if any)
+// will be dropped by (*FileSyntax).Cleanup.
+func (line *Line) markRemoved() {
+	line.Token = nil
+	line.Comments.Suffix = nil
+}
+
+// Cleanup cleans up the file syntax x after any edit operations.
+// To avoid quadratic behavior, (*Line).markRemoved marks the line as dead
+// by setting line.Token = nil but does not remove it from the slice
+// in which it appears. After edits have all been indicated,
+// calling Cleanup cleans out the dead lines.
+func (x *FileSyntax) Cleanup() {
+	w := 0
+	for _, stmt := range x.Stmt {
+		switch stmt := stmt.(type) {
+		case *Line:
+			if stmt.Token == nil {
+				continue
+			}
+		case *LineBlock:
+			ww := 0
+			for _, line := range stmt.Line {
+				if line.Token != nil {
+					stmt.Line[ww] = line
+					ww++
+				}
+			}
+			if ww == 0 {
+				continue
+			}
+			if ww == 1 {
+				// Collapse block into single line.
+				line := &Line{
+					Comments: Comments{
+						Before: commentsAdd(stmt.Before, stmt.Line[0].Before),
+						Suffix: commentsAdd(stmt.Line[0].Suffix, stmt.Suffix),
+						After:  commentsAdd(stmt.Line[0].After, stmt.After),
+					},
+					Token: stringsAdd(stmt.Token, stmt.Line[0].Token),
+				}
+				x.Stmt[w] = line
+				w++
+				continue
+			}
+			stmt.Line = stmt.Line[:ww]
+		}
+		x.Stmt[w] = stmt
+		w++
+	}
+	x.Stmt = x.Stmt[:w]
+}
+
+func commentsAdd(x, y []Comment) []Comment {
+	return append(x[:len(x):len(x)], y...)
+}
+
+func stringsAdd(x, y []string) []string {
+	return append(x[:len(x):len(x)], y...)
+}
+
+// A CommentBlock represents a top-level block of comments separate
+// from any rule.
+type CommentBlock struct {
+	Comments
+	Start Position
+}
+
+func (x *CommentBlock) Span() (start, end Position) {
+	return x.Start, x.Start
+}
+
+// A Line is a single line of tokens.
+type Line struct {
+	Comments
+	Start   Position
+	Token   []string
+	InBlock bool
+	End     Position
+}
+
+func (x *Line) Span() (start, end Position) {
+	return x.Start, x.End
+}
+
+// A LineBlock is a factored block of lines, like
+//
+//	require (
+//		"x"
+//		"y"
+//	)
+type LineBlock struct {
+	Comments
+	Start  Position
+	LParen LParen
+	Token  []string
+	Line   []*Line
+	RParen RParen
+}
+
+func (x *LineBlock) Span() (start, end Position) {
+	return x.Start, x.RParen.Pos.add(")")
+}
+
+// An LParen represents the beginning of a parenthesized line block.
+// It is a place to store suffix comments.
+type LParen struct {
+	Comments
+	Pos Position
+}
+
+func (x *LParen) Span() (start, end Position) {
+	return x.Pos, x.Pos.add(")")
+}
+
+// An RParen represents the end of a parenthesized line block.
+// It is a place to store whole-line (before) comments.
+type RParen struct {
+	Comments
+	Pos Position
+}
+
+func (x *RParen) Span() (start, end Position) {
+	return x.Pos, x.Pos.add(")")
+}
+
+// An input represents a single input file being parsed.
+type input struct {
+	// Lexing state.
+	filename   string    // name of input file, for errors
+	complete   []byte    // entire input
+	remaining  []byte    // remaining input
+	tokenStart []byte    // token being scanned to end of input
+	token      token     // next token to be returned by lex, peek
+	pos        Position  // current input position
+	comments   []Comment // accumulated comments
+
+	// Parser state.
+	file        *FileSyntax // returned top-level syntax tree
+	parseErrors ErrorList   // errors encountered during parsing
+
+	// Comment assignment state.
+	pre  []Expr // all expressions, in preorder traversal
+	post []Expr // all expressions, in postorder traversal
+}
+
+func newInput(filename string, data []byte) *input {
+	return &input{
+		filename:  filename,
+		complete:  data,
+		remaining: data,
+		pos:       Position{Line: 1, LineRune: 1, Byte: 0},
+	}
+}
+
+// parse parses the input file.
+func parse(file string, data []byte) (f *FileSyntax, err error) {
+	// The parser panics for both routine errors like syntax errors
+	// and for programmer bugs like array index errors.
+	// Turn both into error returns. Catching bug panics is
+	// especially important when processing many files.
+	in := newInput(file, data)
+	defer func() {
+		if e := recover(); e != nil && e != &in.parseErrors {
+			in.parseErrors = append(in.parseErrors, Error{
+				Filename: in.filename,
+				Pos:      in.pos,
+				Err:      fmt.Errorf("internal error: %v", e),
+			})
+		}
+		if err == nil && len(in.parseErrors) > 0 {
+			err = in.parseErrors
+		}
+	}()
+
+	// Prime the lexer by reading in the first token. It will be available
+	// in the next peek() or lex() call.
+	in.readToken()
+
+	// Invoke the parser.
+	in.parseFile()
+	if len(in.parseErrors) > 0 {
+		return nil, in.parseErrors
+	}
+	in.file.Name = in.filename
+
+	// Assign comments to nearby syntax.
+	in.assignComments()
+
+	return in.file, nil
+}
+
+// Error is called to report an error.
+// Error does not return: it panics.
+func (in *input) Error(s string) {
+	in.parseErrors = append(in.parseErrors, Error{
+		Filename: in.filename,
+		Pos:      in.pos,
+		Err:      errors.New(s),
+	})
+	panic(&in.parseErrors)
+}
+
+// eof reports whether the input has reached end of file.
+func (in *input) eof() bool {
+	return len(in.remaining) == 0
+}
+
+// peekRune returns the next rune in the input without consuming it.
+func (in *input) peekRune() int {
+	if len(in.remaining) == 0 {
+		return 0
+	}
+	r, _ := utf8.DecodeRune(in.remaining)
+	return int(r)
+}
+
+// peekPrefix reports whether the remaining input begins with the given prefix.
+func (in *input) peekPrefix(prefix string) bool {
+	// This is like bytes.HasPrefix(in.remaining, []byte(prefix))
+	// but without the allocation of the []byte copy of prefix.
+	for i := 0; i < len(prefix); i++ {
+		if i >= len(in.remaining) || in.remaining[i] != prefix[i] {
+			return false
+		}
+	}
+	return true
+}
+
+// readRune consumes and returns the next rune in the input.
+func (in *input) readRune() int {
+	if len(in.remaining) == 0 {
+		in.Error("internal lexer error: readRune at EOF")
+	}
+	r, size := utf8.DecodeRune(in.remaining)
+	in.remaining = in.remaining[size:]
+	if r == '\n' {
+		in.pos.Line++
+		in.pos.LineRune = 1
+	} else {
+		in.pos.LineRune++
+	}
+	in.pos.Byte += size
+	return int(r)
+}
+
+type token struct {
+	kind   tokenKind
+	pos    Position
+	endPos Position
+	text   string
+}
+
+type tokenKind int
+
+const (
+	_EOF tokenKind = -(iota + 1)
+	_EOLCOMMENT
+	_IDENT
+	_STRING
+	_COMMENT
+
+	// newlines and punctuation tokens are allowed as ASCII codes.
+)
+
+func (k tokenKind) isComment() bool {
+	return k == _COMMENT || k == _EOLCOMMENT
+}
+
+// isEOL returns whether a token terminates a line.
+func (k tokenKind) isEOL() bool {
+	return k == _EOF || k == _EOLCOMMENT || k == '\n'
+}
+
+// startToken marks the beginning of the next input token.
+// It must be followed by a call to endToken, once the token's text has
+// been consumed using readRune.
+func (in *input) startToken() {
+	in.tokenStart = in.remaining
+	in.token.text = ""
+	in.token.pos = in.pos
+}
+
+// endToken marks the end of an input token.
+// It records the actual token string in tok.text.
+// A single trailing newline (LF or CRLF) will be removed from comment tokens.
+func (in *input) endToken(kind tokenKind) {
+	in.token.kind = kind
+	text := string(in.tokenStart[:len(in.tokenStart)-len(in.remaining)])
+	if kind.isComment() {
+		if strings.HasSuffix(text, "\r\n") {
+			text = text[:len(text)-2]
+		} else {
+			text = strings.TrimSuffix(text, "\n")
+		}
+	}
+	in.token.text = text
+	in.token.endPos = in.pos
+}
+
+// peek returns the kind of the next token returned by lex.
+func (in *input) peek() tokenKind {
+	return in.token.kind
+}
+
+// lex is called from the parser to obtain the next input token.
+func (in *input) lex() token {
+	tok := in.token
+	in.readToken()
+	return tok
+}
+
+// readToken lexes the next token from the text and stores it in in.token.
+func (in *input) readToken() {
+	// Skip past spaces, stopping at non-space or EOF.
+	for !in.eof() {
+		c := in.peekRune()
+		if c == ' ' || c == '\t' || c == '\r' {
+			in.readRune()
+			continue
+		}
+
+		// Comment runs to end of line.
+		if in.peekPrefix("//") {
+			in.startToken()
+
+			// Is this comment the only thing on its line?
+			// Find the last \n before this // and see if it's all
+			// spaces from there to here.
+			i := bytes.LastIndex(in.complete[:in.pos.Byte], []byte("\n"))
+			suffix := len(bytes.TrimSpace(in.complete[i+1:in.pos.Byte])) > 0
+			in.readRune()
+			in.readRune()
+
+			// Consume comment.
+			for len(in.remaining) > 0 && in.readRune() != '\n' {
+			}
+
+			// If we are at top level (not in a statement), hand the comment to
+			// the parser as a _COMMENT token. The grammar is written
+			// to handle top-level comments itself.
+			if !suffix {
+				in.endToken(_COMMENT)
+				return
+			}
+
+			// Otherwise, save comment for later attachment to syntax tree.
+			in.endToken(_EOLCOMMENT)
+			in.comments = append(in.comments, Comment{in.token.pos, in.token.text, suffix})
+			return
+		}
+
+		if in.peekPrefix("/*") {
+			in.Error("mod files must use // comments (not /* */ comments)")
+		}
+
+		// Found non-space non-comment.
+		break
+	}
+
+	// Found the beginning of the next token.
+	in.startToken()
+
+	// End of file.
+	if in.eof() {
+		in.endToken(_EOF)
+		return
+	}
+
+	// Punctuation tokens.
+	switch c := in.peekRune(); c {
+	case '\n', '(', ')', '[', ']', '{', '}', ',':
+		in.readRune()
+		in.endToken(tokenKind(c))
+		return
+
+	case '"', '`': // quoted string
+		quote := c
+		in.readRune()
+		for {
+			if in.eof() {
+				in.pos = in.token.pos
+				in.Error("unexpected EOF in string")
+			}
+			if in.peekRune() == '\n' {
+				in.Error("unexpected newline in string")
+			}
+			c := in.readRune()
+			if c == quote {
+				break
+			}
+			if c == '\\' && quote != '`' {
+				if in.eof() {
+					in.pos = in.token.pos
+					in.Error("unexpected EOF in string")
+				}
+				in.readRune()
+			}
+		}
+		in.endToken(_STRING)
+		return
+	}
+
+	// Checked all punctuation. Must be identifier token.
+	if c := in.peekRune(); !isIdent(c) {
+		in.Error(fmt.Sprintf("unexpected input character %#q", c))
+	}
+
+	// Scan over identifier.
+	for isIdent(in.peekRune()) {
+		if in.peekPrefix("//") {
+			break
+		}
+		if in.peekPrefix("/*") {
+			in.Error("mod files must use // comments (not /* */ comments)")
+		}
+		in.readRune()
+	}
+	in.endToken(_IDENT)
+}
+
+// isIdent reports whether c is an identifier rune.
+// We treat most printable runes as identifier runes, except for a handful of
+// ASCII punctuation characters.
+func isIdent(c int) bool {
+	switch r := rune(c); r {
+	case ' ', '(', ')', '[', ']', '{', '}', ',':
+		return false
+	default:
+		return !unicode.IsSpace(r) && unicode.IsPrint(r)
+	}
+}
+
+// Comment assignment.
+// We build two lists of all subexpressions, preorder and postorder.
+// The preorder list is ordered by start location, with outer expressions first.
+// The postorder list is ordered by end location, with outer expressions last.
+// We use the preorder list to assign each whole-line comment to the syntax
+// immediately following it, and we use the postorder list to assign each
+// end-of-line comment to the syntax immediately preceding it.
+
+// order walks the expression adding it and its subexpressions to the
+// preorder and postorder lists.
+func (in *input) order(x Expr) {
+	if x != nil {
+		in.pre = append(in.pre, x)
+	}
+	switch x := x.(type) {
+	default:
+		panic(fmt.Errorf("order: unexpected type %T", x))
+	case nil:
+		// nothing
+	case *LParen, *RParen:
+		// nothing
+	case *CommentBlock:
+		// nothing
+	case *Line:
+		// nothing
+	case *FileSyntax:
+		for _, stmt := range x.Stmt {
+			in.order(stmt)
+		}
+	case *LineBlock:
+		in.order(&x.LParen)
+		for _, l := range x.Line {
+			in.order(l)
+		}
+		in.order(&x.RParen)
+	}
+	if x != nil {
+		in.post = append(in.post, x)
+	}
+}
+
+// assignComments attaches comments to nearby syntax.
+func (in *input) assignComments() {
+	const debug = false
+
+	// Generate preorder and postorder lists.
+	in.order(in.file)
+
+	// Split into whole-line comments and suffix comments.
+	var line, suffix []Comment
+	for _, com := range in.comments {
+		if com.Suffix {
+			suffix = append(suffix, com)
+		} else {
+			line = append(line, com)
+		}
+	}
+
+	if debug {
+		for _, c := range line {
+			fmt.Fprintf(os.Stderr, "LINE %q :%d:%d #%d\n", c.Token, c.Start.Line, c.Start.LineRune, c.Start.Byte)
+		}
+	}
+
+	// Assign line comments to syntax immediately following.
+	for _, x := range in.pre {
+		start, _ := x.Span()
+		if debug {
+			fmt.Fprintf(os.Stderr, "pre %T :%d:%d #%d\n", x, start.Line, start.LineRune, start.Byte)
+		}
+		xcom := x.Comment()
+		for len(line) > 0 && start.Byte >= line[0].Start.Byte {
+			if debug {
+				fmt.Fprintf(os.Stderr, "ASSIGN LINE %q #%d\n", line[0].Token, line[0].Start.Byte)
+			}
+			xcom.Before = append(xcom.Before, line[0])
+			line = line[1:]
+		}
+	}
+
+	// Remaining line comments go at end of file.
+	in.file.After = append(in.file.After, line...)
+
+	if debug {
+		for _, c := range suffix {
+			fmt.Fprintf(os.Stderr, "SUFFIX %q :%d:%d #%d\n", c.Token, c.Start.Line, c.Start.LineRune, c.Start.Byte)
+		}
+	}
+
+	// Assign suffix comments to syntax immediately before.
+	for i := len(in.post) - 1; i >= 0; i-- {
+		x := in.post[i]
+
+		start, end := x.Span()
+		if debug {
+			fmt.Fprintf(os.Stderr, "post %T :%d:%d #%d :%d:%d #%d\n", x, start.Line, start.LineRune, start.Byte, end.Line, end.LineRune, end.Byte)
+		}
+
+		// Do not assign suffix comments to end of line block or whole file.
+		// Instead assign them to the last element inside.
+		switch x.(type) {
+		case *FileSyntax:
+			continue
+		}
+
+		// Do not assign suffix comments to something that starts
+		// on an earlier line, so that in
+		//
+		//	x ( y
+		//		z ) // comment
+		//
+		// we assign the comment to z and not to x ( ... ).
+		if start.Line != end.Line {
+			continue
+		}
+		xcom := x.Comment()
+		for len(suffix) > 0 && end.Byte <= suffix[len(suffix)-1].Start.Byte {
+			if debug {
+				fmt.Fprintf(os.Stderr, "ASSIGN SUFFIX %q #%d\n", suffix[len(suffix)-1].Token, suffix[len(suffix)-1].Start.Byte)
+			}
+			xcom.Suffix = append(xcom.Suffix, suffix[len(suffix)-1])
+			suffix = suffix[:len(suffix)-1]
+		}
+	}
+
+	// We assigned suffix comments in reverse.
+	// If multiple suffix comments were appended to the same
+	// expression node, they are now in reverse. Fix that.
+	for _, x := range in.post {
+		reverseComments(x.Comment().Suffix)
+	}
+
+	// Remaining suffix comments go at beginning of file.
+	in.file.Before = append(in.file.Before, suffix...)
+}
+
+// reverseComments reverses the []Comment list.
+func reverseComments(list []Comment) {
+	for i, j := 0, len(list)-1; i < j; i, j = i+1, j-1 {
+		list[i], list[j] = list[j], list[i]
+	}
+}
+
+func (in *input) parseFile() {
+	in.file = new(FileSyntax)
+	var cb *CommentBlock
+	for {
+		switch in.peek() {
+		case '\n':
+			in.lex()
+			if cb != nil {
+				in.file.Stmt = append(in.file.Stmt, cb)
+				cb = nil
+			}
+		case _COMMENT:
+			tok := in.lex()
+			if cb == nil {
+				cb = &CommentBlock{Start: tok.pos}
+			}
+			com := cb.Comment()
+			com.Before = append(com.Before, Comment{Start: tok.pos, Token: tok.text})
+		case _EOF:
+			if cb != nil {
+				in.file.Stmt = append(in.file.Stmt, cb)
+			}
+			return
+		default:
+			in.parseStmt()
+			if cb != nil {
+				in.file.Stmt[len(in.file.Stmt)-1].Comment().Before = cb.Before
+				cb = nil
+			}
+		}
+	}
+}
+
+func (in *input) parseStmt() {
+	tok := in.lex()
+	start := tok.pos
+	end := tok.endPos
+	tokens := []string{tok.text}
+	for {
+		tok := in.lex()
+		switch {
+		case tok.kind.isEOL():
+			in.file.Stmt = append(in.file.Stmt, &Line{
+				Start: start,
+				Token: tokens,
+				End:   end,
+			})
+			return
+
+		case tok.kind == '(':
+			if next := in.peek(); next.isEOL() {
+				// Start of block: no more tokens on this line.
+				in.file.Stmt = append(in.file.Stmt, in.parseLineBlock(start, tokens, tok))
+				return
+			} else if next == ')' {
+				rparen := in.lex()
+				if in.peek().isEOL() {
+					// Empty block.
+					in.lex()
+					in.file.Stmt = append(in.file.Stmt, &LineBlock{
+						Start:  start,
+						Token:  tokens,
+						LParen: LParen{Pos: tok.pos},
+						RParen: RParen{Pos: rparen.pos},
+					})
+					return
+				}
+				// '( )' in the middle of the line, not a block.
+				tokens = append(tokens, tok.text, rparen.text)
+			} else {
+				// '(' in the middle of the line, not a block.
+				tokens = append(tokens, tok.text)
+			}
+
+		default:
+			tokens = append(tokens, tok.text)
+			end = tok.endPos
+		}
+	}
+}
+
+func (in *input) parseLineBlock(start Position, token []string, lparen token) *LineBlock {
+	x := &LineBlock{
+		Start:  start,
+		Token:  token,
+		LParen: LParen{Pos: lparen.pos},
+	}
+	var comments []Comment
+	for {
+		switch in.peek() {
+		case _EOLCOMMENT:
+			// Suffix comment, will be attached later by assignComments.
+			in.lex()
+		case '\n':
+			// Blank line. Add an empty comment to preserve it.
+			in.lex()
+			if len(comments) == 0 && len(x.Line) > 0 || len(comments) > 0 && comments[len(comments)-1].Token != "" {
+				comments = append(comments, Comment{})
+			}
+		case _COMMENT:
+			tok := in.lex()
+			comments = append(comments, Comment{Start: tok.pos, Token: tok.text})
+		case _EOF:
+			in.Error(fmt.Sprintf("syntax error (unterminated block started at %s:%d:%d)", in.filename, x.Start.Line, x.Start.LineRune))
+		case ')':
+			rparen := in.lex()
+			x.RParen.Before = comments
+			x.RParen.Pos = rparen.pos
+			if !in.peek().isEOL() {
+				in.Error("syntax error (expected newline after closing paren)")
+			}
+			in.lex()
+			return x
+		default:
+			l := in.parseLine()
+			x.Line = append(x.Line, l)
+			l.Comment().Before = comments
+			comments = nil
+		}
+	}
+}
+
+func (in *input) parseLine() *Line {
+	tok := in.lex()
+	if tok.kind.isEOL() {
+		in.Error("internal parse error: parseLine at end of line")
+	}
+	start := tok.pos
+	end := tok.endPos
+	tokens := []string{tok.text}
+	for {
+		tok := in.lex()
+		if tok.kind.isEOL() {
+			return &Line{
+				Start:   start,
+				Token:   tokens,
+				End:     end,
+				InBlock: true,
+			}
+		}
+		tokens = append(tokens, tok.text)
+		end = tok.endPos
+	}
+}
+
+var (
+	slashSlash = []byte("//")
+	moduleStr  = []byte("module")
+)
+
+// ModulePath returns the module path from the gomod file text.
+// If it cannot find a module path, it returns an empty string.
+// It is tolerant of unrelated problems in the go.mod file.
+func ModulePath(mod []byte) string {
+	for len(mod) > 0 {
+		line := mod
+		mod = nil
+		if i := bytes.IndexByte(line, '\n'); i >= 0 {
+			line, mod = line[:i], line[i+1:]
+		}
+		if i := bytes.Index(line, slashSlash); i >= 0 {
+			line = line[:i]
+		}
+		line = bytes.TrimSpace(line)
+		if !bytes.HasPrefix(line, moduleStr) {
+			continue
+		}
+		line = line[len(moduleStr):]
+		n := len(line)
+		line = bytes.TrimSpace(line)
+		if len(line) == n || len(line) == 0 {
+			continue
+		}
+
+		if line[0] == '"' || line[0] == '`' {
+			p, err := strconv.Unquote(string(line))
+			if err != nil {
+				return "" // malformed quoted string or multiline module path
+			}
+			return p
+		}
+
+		return string(line)
+	}
+	return "" // missing module path
+}

+ 1663 - 0
vendor/golang.org/x/mod/modfile/rule.go

@@ -0,0 +1,1663 @@
+// 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 implements a parser and formatter for go.mod files.
+//
+// The go.mod syntax is described in
+// https://golang.org/cmd/go/#hdr-The_go_mod_file.
+//
+// The Parse and ParseLax functions both parse a go.mod file and return an
+// abstract syntax tree. ParseLax ignores unknown statements and may be used to
+// parse go.mod files that may have been developed with newer versions of Go.
+//
+// The File struct returned by Parse and ParseLax represent an abstract
+// go.mod file. File has several methods like AddNewRequire and DropReplace
+// that can be used to programmatically edit a file.
+//
+// The Format function formats a File back to a byte slice which can be
+// written to a file.
+package modfile
+
+import (
+	"errors"
+	"fmt"
+	"path/filepath"
+	"sort"
+	"strconv"
+	"strings"
+	"unicode"
+
+	"golang.org/x/mod/internal/lazyregexp"
+	"golang.org/x/mod/module"
+	"golang.org/x/mod/semver"
+)
+
+// A File is the parsed, interpreted form of a go.mod file.
+type File struct {
+	Module    *Module
+	Go        *Go
+	Toolchain *Toolchain
+	Require   []*Require
+	Exclude   []*Exclude
+	Replace   []*Replace
+	Retract   []*Retract
+
+	Syntax *FileSyntax
+}
+
+// A Module is the module statement.
+type Module struct {
+	Mod        module.Version
+	Deprecated string
+	Syntax     *Line
+}
+
+// A Go is the go statement.
+type Go struct {
+	Version string // "1.23"
+	Syntax  *Line
+}
+
+// A Toolchain is the toolchain statement.
+type Toolchain struct {
+	Name   string // "go1.21rc1"
+	Syntax *Line
+}
+
+// An Exclude is a single exclude statement.
+type Exclude struct {
+	Mod    module.Version
+	Syntax *Line
+}
+
+// A Replace is a single replace statement.
+type Replace struct {
+	Old    module.Version
+	New    module.Version
+	Syntax *Line
+}
+
+// A Retract is a single retract statement.
+type Retract struct {
+	VersionInterval
+	Rationale string
+	Syntax    *Line
+}
+
+// A VersionInterval represents a range of versions with upper and lower bounds.
+// Intervals are closed: both bounds are included. When Low is equal to High,
+// the interval may refer to a single version ('v1.2.3') or an interval
+// ('[v1.2.3, v1.2.3]'); both have the same representation.
+type VersionInterval struct {
+	Low, High string
+}
+
+// A Require is a single require statement.
+type Require struct {
+	Mod      module.Version
+	Indirect bool // has "// indirect" comment
+	Syntax   *Line
+}
+
+func (r *Require) markRemoved() {
+	r.Syntax.markRemoved()
+	*r = Require{}
+}
+
+func (r *Require) setVersion(v string) {
+	r.Mod.Version = v
+
+	if line := r.Syntax; len(line.Token) > 0 {
+		if line.InBlock {
+			// If the line is preceded by an empty line, remove it; see
+			// https://golang.org/issue/33779.
+			if len(line.Comments.Before) == 1 && len(line.Comments.Before[0].Token) == 0 {
+				line.Comments.Before = line.Comments.Before[:0]
+			}
+			if len(line.Token) >= 2 { // example.com v1.2.3
+				line.Token[1] = v
+			}
+		} else {
+			if len(line.Token) >= 3 { // require example.com v1.2.3
+				line.Token[2] = v
+			}
+		}
+	}
+}
+
+// setIndirect sets line to have (or not have) a "// indirect" comment.
+func (r *Require) setIndirect(indirect bool) {
+	r.Indirect = indirect
+	line := r.Syntax
+	if isIndirect(line) == indirect {
+		return
+	}
+	if indirect {
+		// Adding comment.
+		if len(line.Suffix) == 0 {
+			// New comment.
+			line.Suffix = []Comment{{Token: "// indirect", Suffix: true}}
+			return
+		}
+
+		com := &line.Suffix[0]
+		text := strings.TrimSpace(strings.TrimPrefix(com.Token, string(slashSlash)))
+		if text == "" {
+			// Empty comment.
+			com.Token = "// indirect"
+			return
+		}
+
+		// Insert at beginning of existing comment.
+		com.Token = "// indirect; " + text
+		return
+	}
+
+	// Removing comment.
+	f := strings.TrimSpace(strings.TrimPrefix(line.Suffix[0].Token, string(slashSlash)))
+	if f == "indirect" {
+		// Remove whole comment.
+		line.Suffix = nil
+		return
+	}
+
+	// Remove comment prefix.
+	com := &line.Suffix[0]
+	i := strings.Index(com.Token, "indirect;")
+	com.Token = "//" + com.Token[i+len("indirect;"):]
+}
+
+// isIndirect reports whether line has a "// indirect" comment,
+// meaning it is in go.mod only for its effect on indirect dependencies,
+// so that it can be dropped entirely once the effective version of the
+// indirect dependency reaches the given minimum version.
+func isIndirect(line *Line) bool {
+	if len(line.Suffix) == 0 {
+		return false
+	}
+	f := strings.Fields(strings.TrimPrefix(line.Suffix[0].Token, string(slashSlash)))
+	return (len(f) == 1 && f[0] == "indirect" || len(f) > 1 && f[0] == "indirect;")
+}
+
+func (f *File) AddModuleStmt(path string) error {
+	if f.Syntax == nil {
+		f.Syntax = new(FileSyntax)
+	}
+	if f.Module == nil {
+		f.Module = &Module{
+			Mod:    module.Version{Path: path},
+			Syntax: f.Syntax.addLine(nil, "module", AutoQuote(path)),
+		}
+	} else {
+		f.Module.Mod.Path = path
+		f.Syntax.updateLine(f.Module.Syntax, "module", AutoQuote(path))
+	}
+	return nil
+}
+
+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)
+
+// errDontFix is returned by a VersionFixer to indicate the version should be
+// left alone, even if it's not canonical.
+var dontFixRetract VersionFixer = func(_, vers string) (string, error) {
+	return vers, nil
+}
+
+// Parse parses and returns a go.mod 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 Parse(file string, data []byte, fix VersionFixer) (*File, error) {
+	return parseToFile(file, data, fix, true)
+}
+
+// ParseLax is like Parse but ignores unknown statements.
+// It is used when parsing go.mod files other than the main module,
+// under the theory that most statement types we add in the future will
+// only apply in the main module, like exclude and replace,
+// and so we get better gradual deployments if old go commands
+// simply ignore those statements when found in go.mod files
+// in dependencies.
+func ParseLax(file string, data []byte, fix VersionFixer) (*File, error) {
+	return parseToFile(file, data, fix, false)
+}
+
+func parseToFile(file string, data []byte, fix VersionFixer, strict bool) (parsed *File, err error) {
+	fs, err := parse(file, data)
+	if err != nil {
+		return nil, err
+	}
+	f := &File{
+		Syntax: fs,
+	}
+	var errs ErrorList
+
+	// fix versions in retract directives after the file is parsed.
+	// We need the module path to fix versions, and it might be at the end.
+	defer func() {
+		oldLen := len(errs)
+		f.fixRetract(fix, &errs)
+		if len(errs) > oldLen {
+			parsed, err = nil, errs
+		}
+	}()
+
+	for _, x := range fs.Stmt {
+		switch x := x.(type) {
+		case *Line:
+			f.add(&errs, nil, x, x.Token[0], x.Token[1:], fix, strict)
+
+		case *LineBlock:
+			if len(x.Token) > 1 {
+				if strict {
+					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:
+				if strict {
+					errs = append(errs, Error{
+						Filename: file,
+						Pos:      x.Start,
+						Err:      fmt.Errorf("unknown block type: %s", strings.Join(x.Token, " ")),
+					})
+				}
+				continue
+			case "module", "require", "exclude", "replace", "retract":
+				for _, l := range x.Line {
+					f.add(&errs, x, l, x.Token[0], l.Token, fix, strict)
+				}
+			}
+		}
+	}
+
+	if len(errs) > 0 {
+		return nil, errs
+	}
+	return f, nil
+}
+
+var GoVersionRE = lazyregexp.New(`^([1-9][0-9]*)\.(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))?([a-z]+[0-9]+)?$`)
+var laxGoVersionRE = lazyregexp.New(`^v?(([1-9][0-9]*)\.(0|[1-9][0-9]*))([^0-9].*)$`)
+
+// Toolchains must be named beginning with `go1`,
+// like "go1.20.3" or "go1.20.3-gccgo". As a special case, "default" is also permitted.
+var ToolchainRE = lazyregexp.New(`^default$|^go1($|\.)`)
+
+func (f *File) add(errs *ErrorList, block *LineBlock, line *Line, verb string, args []string, fix VersionFixer, strict bool) {
+	// If strict is false, this module is a dependency.
+	// We ignore all unknown directives as well as main-module-only
+	// directives like replace and exclude. It will work better for
+	// forward compatibility if we can depend on modules that have unknown
+	// statements (presumed relevant only when acting as the main module)
+	// and simply ignore those statements.
+	if !strict {
+		switch verb {
+		case "go", "module", "retract", "require":
+			// want these even for dependency go.mods
+		default:
+			return
+		}
+	}
+
+	wrapModPathError := func(modPath string, err error) {
+		*errs = append(*errs, Error{
+			Filename: f.Syntax.Name,
+			Pos:      line.Start,
+			ModPath:  modPath,
+			Verb:     verb,
+			Err:      err,
+		})
+	}
+	wrapError := func(err error) {
+		*errs = append(*errs, Error{
+			Filename: f.Syntax.Name,
+			Pos:      line.Start,
+			Err:      err,
+		})
+	}
+	errorf := func(format string, args ...interface{}) {
+		wrapError(fmt.Errorf(format, args...))
+	}
+
+	switch verb {
+	default:
+		errorf("unknown directive: %s", verb)
+
+	case "go":
+		if f.Go != nil {
+			errorf("repeated go statement")
+			return
+		}
+		if len(args) != 1 {
+			errorf("go directive expects exactly one argument")
+			return
+		} else if !GoVersionRE.MatchString(args[0]) {
+			fixed := false
+			if !strict {
+				if m := laxGoVersionRE.FindStringSubmatch(args[0]); m != nil {
+					args[0] = m[1]
+					fixed = true
+				}
+			}
+			if !fixed {
+				errorf("invalid go version '%s': must match format 1.23", args[0])
+				return
+			}
+		}
+
+		f.Go = &Go{Syntax: line}
+		f.Go.Version = args[0]
+
+	case "toolchain":
+		if f.Toolchain != nil {
+			errorf("repeated toolchain statement")
+			return
+		}
+		if len(args) != 1 {
+			errorf("toolchain directive expects exactly one argument")
+			return
+		} else if strict && !ToolchainRE.MatchString(args[0]) {
+			errorf("invalid toolchain version '%s': must match format go1.23 or local", args[0])
+			return
+		}
+		f.Toolchain = &Toolchain{Syntax: line}
+		f.Toolchain.Name = args[0]
+
+	case "module":
+		if f.Module != nil {
+			errorf("repeated module statement")
+			return
+		}
+		deprecated := parseDeprecation(block, line)
+		f.Module = &Module{
+			Syntax:     line,
+			Deprecated: deprecated,
+		}
+		if len(args) != 1 {
+			errorf("usage: module module/path")
+			return
+		}
+		s, err := parseString(&args[0])
+		if err != nil {
+			errorf("invalid quoted string: %v", err)
+			return
+		}
+		f.Module.Mod = module.Version{Path: s}
+
+	case "require", "exclude":
+		if len(args) != 2 {
+			errorf("usage: %s module/path v1.2.3", verb)
+			return
+		}
+		s, err := parseString(&args[0])
+		if err != nil {
+			errorf("invalid quoted string: %v", err)
+			return
+		}
+		v, err := parseVersion(verb, s, &args[1], fix)
+		if err != nil {
+			wrapError(err)
+			return
+		}
+		pathMajor, err := modulePathMajor(s)
+		if err != nil {
+			wrapError(err)
+			return
+		}
+		if err := module.CheckPathMajor(v, pathMajor); err != nil {
+			wrapModPathError(s, err)
+			return
+		}
+		if verb == "require" {
+			f.Require = append(f.Require, &Require{
+				Mod:      module.Version{Path: s, Version: v},
+				Syntax:   line,
+				Indirect: isIndirect(line),
+			})
+		} else {
+			f.Exclude = append(f.Exclude, &Exclude{
+				Mod:    module.Version{Path: s, Version: v},
+				Syntax: line,
+			})
+		}
+
+	case "replace":
+		replace, wrappederr := parseReplace(f.Syntax.Name, line, verb, args, fix)
+		if wrappederr != nil {
+			*errs = append(*errs, *wrappederr)
+			return
+		}
+		f.Replace = append(f.Replace, replace)
+
+	case "retract":
+		rationale := parseDirectiveComment(block, line)
+		vi, err := parseVersionInterval(verb, "", &args, dontFixRetract)
+		if err != nil {
+			if strict {
+				wrapError(err)
+				return
+			} else {
+				// Only report errors parsing intervals in the main module. We may
+				// support additional syntax in the future, such as open and half-open
+				// intervals. Those can't be supported now, because they break the
+				// go.mod parser, even in lax mode.
+				return
+			}
+		}
+		if len(args) > 0 && strict {
+			// In the future, there may be additional information after the version.
+			errorf("unexpected token after version: %q", args[0])
+			return
+		}
+		retract := &Retract{
+			VersionInterval: vi,
+			Rationale:       rationale,
+			Syntax:          line,
+		}
+		f.Retract = append(f.Retract, retract)
+	}
+}
+
+func parseReplace(filename string, line *Line, verb string, args []string, fix VersionFixer) (*Replace, *Error) {
+	wrapModPathError := func(modPath string, err error) *Error {
+		return &Error{
+			Filename: filename,
+			Pos:      line.Start,
+			ModPath:  modPath,
+			Verb:     verb,
+			Err:      err,
+		}
+	}
+	wrapError := func(err error) *Error {
+		return &Error{
+			Filename: filename,
+			Pos:      line.Start,
+			Err:      err,
+		}
+	}
+	errorf := func(format string, args ...interface{}) *Error {
+		return wrapError(fmt.Errorf(format, args...))
+	}
+
+	arrow := 2
+	if len(args) >= 2 && args[1] == "=>" {
+		arrow = 1
+	}
+	if len(args) < arrow+2 || len(args) > arrow+3 || args[arrow] != "=>" {
+		return nil, errorf("usage: %s module/path [v1.2.3] => other/module v1.4\n\t or %s module/path [v1.2.3] => ../local/directory", verb, verb)
+	}
+	s, err := parseString(&args[0])
+	if err != nil {
+		return nil, errorf("invalid quoted string: %v", err)
+	}
+	pathMajor, err := modulePathMajor(s)
+	if err != nil {
+		return nil, wrapModPathError(s, err)
+
+	}
+	var v string
+	if arrow == 2 {
+		v, err = parseVersion(verb, s, &args[1], fix)
+		if err != nil {
+			return nil, wrapError(err)
+		}
+		if err := module.CheckPathMajor(v, pathMajor); err != nil {
+			return nil, wrapModPathError(s, err)
+		}
+	}
+	ns, err := parseString(&args[arrow+1])
+	if err != nil {
+		return nil, errorf("invalid quoted string: %v", err)
+	}
+	nv := ""
+	if len(args) == arrow+2 {
+		if !IsDirectoryPath(ns) {
+			if strings.Contains(ns, "@") {
+				return nil, errorf("replacement module must match format 'path version', not 'path@version'")
+			}
+			return nil, errorf("replacement module without version must be directory path (rooted or starting with ./ or ../)")
+		}
+		if filepath.Separator == '/' && strings.Contains(ns, `\`) {
+			return nil, errorf("replacement directory appears to be Windows path (on a non-windows system)")
+		}
+	}
+	if len(args) == arrow+3 {
+		nv, err = parseVersion(verb, ns, &args[arrow+2], fix)
+		if err != nil {
+			return nil, wrapError(err)
+		}
+		if IsDirectoryPath(ns) {
+			return nil, errorf("replacement module directory path %q cannot have version", ns)
+
+		}
+	}
+	return &Replace{
+		Old:    module.Version{Path: s, Version: v},
+		New:    module.Version{Path: ns, Version: nv},
+		Syntax: line,
+	}, nil
+}
+
+// fixRetract applies fix to each retract directive in f, appending any errors
+// to errs.
+//
+// Most versions are fixed as we parse the file, but for retract directives,
+// the relevant module path is the one specified with the module directive,
+// and that might appear at the end of the file (or not at all).
+func (f *File) fixRetract(fix VersionFixer, errs *ErrorList) {
+	if fix == nil {
+		return
+	}
+	path := ""
+	if f.Module != nil {
+		path = f.Module.Mod.Path
+	}
+	var r *Retract
+	wrapError := func(err error) {
+		*errs = append(*errs, Error{
+			Filename: f.Syntax.Name,
+			Pos:      r.Syntax.Start,
+			Err:      err,
+		})
+	}
+
+	for _, r = range f.Retract {
+		if path == "" {
+			wrapError(errors.New("no module directive found, so retract cannot be used"))
+			return // only print the first one of these
+		}
+
+		args := r.Syntax.Token
+		if args[0] == "retract" {
+			args = args[1:]
+		}
+		vi, err := parseVersionInterval("retract", path, &args, fix)
+		if err != nil {
+			wrapError(err)
+		}
+		r.VersionInterval = vi
+	}
+}
+
+func (f *WorkFile) add(errs *ErrorList, line *Line, verb string, args []string, fix VersionFixer) {
+	wrapError := func(err error) {
+		*errs = append(*errs, Error{
+			Filename: f.Syntax.Name,
+			Pos:      line.Start,
+			Err:      err,
+		})
+	}
+	errorf := func(format string, args ...interface{}) {
+		wrapError(fmt.Errorf(format, args...))
+	}
+
+	switch verb {
+	default:
+		errorf("unknown directive: %s", verb)
+
+	case "go":
+		if f.Go != nil {
+			errorf("repeated go statement")
+			return
+		}
+		if len(args) != 1 {
+			errorf("go directive expects exactly one argument")
+			return
+		} else if !GoVersionRE.MatchString(args[0]) {
+			errorf("invalid go version '%s': must match format 1.23", args[0])
+			return
+		}
+
+		f.Go = &Go{Syntax: line}
+		f.Go.Version = args[0]
+
+	case "toolchain":
+		if f.Toolchain != nil {
+			errorf("repeated toolchain statement")
+			return
+		}
+		if len(args) != 1 {
+			errorf("toolchain directive expects exactly one argument")
+			return
+		} else if !ToolchainRE.MatchString(args[0]) {
+			errorf("invalid toolchain version '%s': must match format go1.23 or local", args[0])
+			return
+		}
+
+		f.Toolchain = &Toolchain{Syntax: line}
+		f.Toolchain.Name = args[0]
+
+	case "use":
+		if len(args) != 1 {
+			errorf("usage: %s local/dir", verb)
+			return
+		}
+		s, err := parseString(&args[0])
+		if err != nil {
+			errorf("invalid quoted string: %v", err)
+			return
+		}
+		f.Use = append(f.Use, &Use{
+			Path:   s,
+			Syntax: line,
+		})
+
+	case "replace":
+		replace, wrappederr := parseReplace(f.Syntax.Name, line, verb, args, fix)
+		if wrappederr != nil {
+			*errs = append(*errs, *wrappederr)
+			return
+		}
+		f.Replace = append(f.Replace, replace)
+	}
+}
+
+// IsDirectoryPath reports whether the given path should be interpreted
+// as a directory path. Just like on the go command line, relative paths
+// and rooted paths are directory paths; the rest are module paths.
+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] == ':'
+}
+
+// MustQuote reports whether s must be quoted in order to appear as
+// a single token in a go.mod line.
+func MustQuote(s string) bool {
+	for _, r := range s {
+		switch r {
+		case ' ', '"', '\'', '`':
+			return true
+
+		case '(', ')', '[', ']', '{', '}', ',':
+			if len(s) > 1 {
+				return true
+			}
+
+		default:
+			if !unicode.IsPrint(r) {
+				return true
+			}
+		}
+	}
+	return s == "" || strings.Contains(s, "//") || strings.Contains(s, "/*")
+}
+
+// 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 parseVersionInterval(verb string, path string, args *[]string, fix VersionFixer) (VersionInterval, error) {
+	toks := *args
+	if len(toks) == 0 || toks[0] == "(" {
+		return VersionInterval{}, fmt.Errorf("expected '[' or version")
+	}
+	if toks[0] != "[" {
+		v, err := parseVersion(verb, path, &toks[0], fix)
+		if err != nil {
+			return VersionInterval{}, err
+		}
+		*args = toks[1:]
+		return VersionInterval{Low: v, High: v}, nil
+	}
+	toks = toks[1:]
+
+	if len(toks) == 0 {
+		return VersionInterval{}, fmt.Errorf("expected version after '['")
+	}
+	low, err := parseVersion(verb, path, &toks[0], fix)
+	if err != nil {
+		return VersionInterval{}, err
+	}
+	toks = toks[1:]
+
+	if len(toks) == 0 || toks[0] != "," {
+		return VersionInterval{}, fmt.Errorf("expected ',' after version")
+	}
+	toks = toks[1:]
+
+	if len(toks) == 0 {
+		return VersionInterval{}, fmt.Errorf("expected version after ','")
+	}
+	high, err := parseVersion(verb, path, &toks[0], fix)
+	if err != nil {
+		return VersionInterval{}, err
+	}
+	toks = toks[1:]
+
+	if len(toks) == 0 || toks[0] != "]" {
+		return VersionInterval{}, fmt.Errorf("expected ']' after version")
+	}
+	toks = toks[1:]
+
+	*args = toks
+	return VersionInterval{Low: low, High: high}, nil
+}
+
+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
+}
+
+var deprecatedRE = lazyregexp.New(`(?s)(?:^|\n\n)Deprecated: *(.*?)(?:$|\n\n)`)
+
+// parseDeprecation extracts the text of comments on a "module" directive and
+// extracts a deprecation message from that.
+//
+// A deprecation message is contained in a paragraph within a block of comments
+// that starts with "Deprecated:" (case sensitive). The message runs until the
+// end of the paragraph and does not include the "Deprecated:" prefix. If the
+// comment block has multiple paragraphs that start with "Deprecated:",
+// parseDeprecation returns the message from the first.
+func parseDeprecation(block *LineBlock, line *Line) string {
+	text := parseDirectiveComment(block, line)
+	m := deprecatedRE.FindStringSubmatch(text)
+	if m == nil {
+		return ""
+	}
+	return m[1]
+}
+
+// parseDirectiveComment extracts the text of comments on a directive.
+// If the directive's line does not have comments and is part of a block that
+// does have comments, the block's comments are used.
+func parseDirectiveComment(block *LineBlock, line *Line) string {
+	comments := line.Comment()
+	if block != nil && len(comments.Before) == 0 && len(comments.Suffix) == 0 {
+		comments = block.Comment()
+	}
+	groups := [][]Comment{comments.Before, comments.Suffix}
+	var lines []string
+	for _, g := range groups {
+		for _, c := range g {
+			if !strings.HasPrefix(c.Token, "//") {
+				continue // blank line
+			}
+			lines = append(lines, strings.TrimSpace(strings.TrimPrefix(c.Token, "//")))
+		}
+	}
+	return strings.Join(lines, "\n")
+}
+
+type ErrorList []Error
+
+func (e ErrorList) Error() string {
+	errStrs := make([]string, len(e))
+	for i, err := range e {
+		errStrs[i] = err.Error()
+	}
+	return strings.Join(errStrs, "\n")
+}
+
+type Error struct {
+	Filename string
+	Pos      Position
+	Verb     string
+	ModPath  string
+	Err      error
+}
+
+func (e *Error) Error() string {
+	var pos string
+	if e.Pos.LineRune > 1 {
+		// Don't print LineRune if it's 1 (beginning of line).
+		// It's always 1 except in scanner errors, which are rare.
+		pos = fmt.Sprintf("%s:%d:%d: ", e.Filename, e.Pos.Line, e.Pos.LineRune)
+	} else if e.Pos.Line > 0 {
+		pos = fmt.Sprintf("%s:%d: ", e.Filename, e.Pos.Line)
+	} else if e.Filename != "" {
+		pos = fmt.Sprintf("%s: ", e.Filename)
+	}
+
+	var directive string
+	if e.ModPath != "" {
+		directive = fmt.Sprintf("%s %s: ", e.Verb, e.ModPath)
+	} else if e.Verb != "" {
+		directive = fmt.Sprintf("%s: ", e.Verb)
+	}
+
+	return pos + directive + e.Err.Error()
+}
+
+func (e *Error) Unwrap() error { return e.Err }
+
+func parseVersion(verb string, path string, s *string, fix VersionFixer) (string, error) {
+	t, err := parseString(s)
+	if err != nil {
+		return "", &Error{
+			Verb:    verb,
+			ModPath: path,
+			Err: &module.InvalidVersionError{
+				Version: *s,
+				Err:     err,
+			},
+		}
+	}
+	if fix != nil {
+		fixed, err := fix(path, t)
+		if err != nil {
+			if err, ok := err.(*module.ModuleError); ok {
+				return "", &Error{
+					Verb:    verb,
+					ModPath: path,
+					Err:     err.Err,
+				}
+			}
+			return "", err
+		}
+		t = fixed
+	} else {
+		cv := module.CanonicalVersion(t)
+		if cv == "" {
+			return "", &Error{
+				Verb:    verb,
+				ModPath: path,
+				Err: &module.InvalidVersionError{
+					Version: t,
+					Err:     errors.New("must be of the form v1.2.3"),
+				},
+			}
+		}
+		t = cv
+	}
+	*s = t
+	return *s, nil
+}
+
+func modulePathMajor(path string) (string, error) {
+	_, major, ok := module.SplitPathVersion(path)
+	if !ok {
+		return "", fmt.Errorf("invalid module path")
+	}
+	return major, nil
+}
+
+func (f *File) Format() ([]byte, error) {
+	return Format(f.Syntax), 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 *File) Cleanup() {
+	w := 0
+	for _, r := range f.Require {
+		if r.Mod.Path != "" {
+			f.Require[w] = r
+			w++
+		}
+	}
+	f.Require = f.Require[:w]
+
+	w = 0
+	for _, x := range f.Exclude {
+		if x.Mod.Path != "" {
+			f.Exclude[w] = x
+			w++
+		}
+	}
+	f.Exclude = f.Exclude[:w]
+
+	w = 0
+	for _, r := range f.Replace {
+		if r.Old.Path != "" {
+			f.Replace[w] = r
+			w++
+		}
+	}
+	f.Replace = f.Replace[:w]
+
+	w = 0
+	for _, r := range f.Retract {
+		if r.Low != "" || r.High != "" {
+			f.Retract[w] = r
+			w++
+		}
+	}
+	f.Retract = f.Retract[:w]
+
+	f.Syntax.Cleanup()
+}
+
+func (f *File) AddGoStmt(version string) error {
+	if !GoVersionRE.MatchString(version) {
+		return fmt.Errorf("invalid language version %q", version)
+	}
+	if f.Go == nil {
+		var hint Expr
+		if f.Module != nil && f.Module.Syntax != nil {
+			hint = f.Module.Syntax
+		}
+		f.Go = &Go{
+			Version: version,
+			Syntax:  f.Syntax.addLine(hint, "go", version),
+		}
+	} else {
+		f.Go.Version = version
+		f.Syntax.updateLine(f.Go.Syntax, "go", version)
+	}
+	return nil
+}
+
+// DropGoStmt deletes the go statement from the file.
+func (f *File) DropGoStmt() {
+	if f.Go != nil {
+		f.Go.Syntax.markRemoved()
+		f.Go = nil
+	}
+}
+
+// DropToolchainStmt deletes the toolchain statement from the file.
+func (f *File) DropToolchainStmt() {
+	if f.Toolchain != nil {
+		f.Toolchain.Syntax.markRemoved()
+		f.Toolchain = nil
+	}
+}
+
+func (f *File) AddToolchainStmt(name string) error {
+	if !ToolchainRE.MatchString(name) {
+		return fmt.Errorf("invalid toolchain name %q", name)
+	}
+	if f.Toolchain == nil {
+		var hint Expr
+		if f.Go != nil && f.Go.Syntax != nil {
+			hint = f.Go.Syntax
+		} else if f.Module != nil && f.Module.Syntax != nil {
+			hint = f.Module.Syntax
+		}
+		f.Toolchain = &Toolchain{
+			Name:   name,
+			Syntax: f.Syntax.addLine(hint, "toolchain", name),
+		}
+	} else {
+		f.Toolchain.Name = name
+		f.Syntax.updateLine(f.Toolchain.Syntax, "toolchain", name)
+	}
+	return nil
+}
+
+// AddRequire sets the first require line for path to version vers,
+// preserving any existing comments for that line and removing all
+// other lines for path.
+//
+// If no line currently exists for path, AddRequire adds a new line
+// at the end of the last require block.
+func (f *File) AddRequire(path, vers string) error {
+	need := true
+	for _, r := range f.Require {
+		if r.Mod.Path == path {
+			if need {
+				r.Mod.Version = vers
+				f.Syntax.updateLine(r.Syntax, "require", AutoQuote(path), vers)
+				need = false
+			} else {
+				r.Syntax.markRemoved()
+				*r = Require{}
+			}
+		}
+	}
+
+	if need {
+		f.AddNewRequire(path, vers, false)
+	}
+	return nil
+}
+
+// AddNewRequire adds a new require line for path at version vers at the end of
+// the last require block, regardless of any existing require lines for path.
+func (f *File) AddNewRequire(path, vers string, indirect bool) {
+	line := f.Syntax.addLine(nil, "require", AutoQuote(path), vers)
+	r := &Require{
+		Mod:    module.Version{Path: path, Version: vers},
+		Syntax: line,
+	}
+	r.setIndirect(indirect)
+	f.Require = append(f.Require, r)
+}
+
+// SetRequire updates the requirements of f to contain exactly req, preserving
+// the existing block structure and line comment contents (except for 'indirect'
+// markings) for the first requirement on each named module path.
+//
+// The Syntax field is ignored for the requirements in req.
+//
+// Any requirements not already present in the file are added to the block
+// containing the last require line.
+//
+// The requirements in req must specify at most one distinct version for each
+// module path.
+//
+// If any existing requirements may be removed, the caller should call Cleanup
+// after all edits are complete.
+func (f *File) SetRequire(req []*Require) {
+	type elem struct {
+		version  string
+		indirect bool
+	}
+	need := make(map[string]elem)
+	for _, r := range req {
+		if prev, dup := need[r.Mod.Path]; dup && prev.version != r.Mod.Version {
+			panic(fmt.Errorf("SetRequire called with conflicting versions for path %s (%s and %s)", r.Mod.Path, prev.version, r.Mod.Version))
+		}
+		need[r.Mod.Path] = elem{r.Mod.Version, r.Indirect}
+	}
+
+	// Update or delete the existing Require entries to preserve
+	// only the first for each module path in req.
+	for _, r := range f.Require {
+		e, ok := need[r.Mod.Path]
+		if ok {
+			r.setVersion(e.version)
+			r.setIndirect(e.indirect)
+		} else {
+			r.markRemoved()
+		}
+		delete(need, r.Mod.Path)
+	}
+
+	// Add new entries in the last block of the file for any paths that weren't
+	// already present.
+	//
+	// This step is nondeterministic, but the final result will be deterministic
+	// because we will sort the block.
+	for path, e := range need {
+		f.AddNewRequire(path, e.version, e.indirect)
+	}
+
+	f.SortBlocks()
+}
+
+// SetRequireSeparateIndirect updates the requirements of f to contain the given
+// requirements. Comment contents (except for 'indirect' markings) are retained
+// from the first existing requirement for each module path. Like SetRequire,
+// SetRequireSeparateIndirect adds requirements for new paths in req,
+// updates the version and "// indirect" comment on existing requirements,
+// and deletes requirements on paths not in req. Existing duplicate requirements
+// are deleted.
+//
+// As its name suggests, SetRequireSeparateIndirect puts direct and indirect
+// requirements into two separate blocks, one containing only direct
+// requirements, and the other containing only indirect requirements.
+// SetRequireSeparateIndirect may move requirements between these two blocks
+// when their indirect markings change. However, SetRequireSeparateIndirect
+// won't move requirements from other blocks, especially blocks with comments.
+//
+// If the file initially has one uncommented block of requirements,
+// SetRequireSeparateIndirect will split it into a direct-only and indirect-only
+// block. This aids in the transition to separate blocks.
+func (f *File) SetRequireSeparateIndirect(req []*Require) {
+	// hasComments returns whether a line or block has comments
+	// other than "indirect".
+	hasComments := func(c Comments) bool {
+		return len(c.Before) > 0 || len(c.After) > 0 || len(c.Suffix) > 1 ||
+			(len(c.Suffix) == 1 &&
+				strings.TrimSpace(strings.TrimPrefix(c.Suffix[0].Token, string(slashSlash))) != "indirect")
+	}
+
+	// moveReq adds r to block. If r was in another block, moveReq deletes
+	// it from that block and transfers its comments.
+	moveReq := func(r *Require, block *LineBlock) {
+		var line *Line
+		if r.Syntax == nil {
+			line = &Line{Token: []string{AutoQuote(r.Mod.Path), r.Mod.Version}}
+			r.Syntax = line
+			if r.Indirect {
+				r.setIndirect(true)
+			}
+		} else {
+			line = new(Line)
+			*line = *r.Syntax
+			if !line.InBlock && len(line.Token) > 0 && line.Token[0] == "require" {
+				line.Token = line.Token[1:]
+			}
+			r.Syntax.Token = nil // Cleanup will delete the old line.
+			r.Syntax = line
+		}
+		line.InBlock = true
+		block.Line = append(block.Line, line)
+	}
+
+	// Examine existing require lines and blocks.
+	var (
+		// We may insert new requirements into the last uncommented
+		// direct-only and indirect-only blocks. We may also move requirements
+		// to the opposite block if their indirect markings change.
+		lastDirectIndex   = -1
+		lastIndirectIndex = -1
+
+		// If there are no direct-only or indirect-only blocks, a new block may
+		// be inserted after the last require line or block.
+		lastRequireIndex = -1
+
+		// If there's only one require line or block, and it's uncommented,
+		// we'll move its requirements to the direct-only or indirect-only blocks.
+		requireLineOrBlockCount = 0
+
+		// Track the block each requirement belongs to (if any) so we can
+		// move them later.
+		lineToBlock = make(map[*Line]*LineBlock)
+	)
+	for i, stmt := range f.Syntax.Stmt {
+		switch stmt := stmt.(type) {
+		case *Line:
+			if len(stmt.Token) == 0 || stmt.Token[0] != "require" {
+				continue
+			}
+			lastRequireIndex = i
+			requireLineOrBlockCount++
+			if !hasComments(stmt.Comments) {
+				if isIndirect(stmt) {
+					lastIndirectIndex = i
+				} else {
+					lastDirectIndex = i
+				}
+			}
+
+		case *LineBlock:
+			if len(stmt.Token) == 0 || stmt.Token[0] != "require" {
+				continue
+			}
+			lastRequireIndex = i
+			requireLineOrBlockCount++
+			allDirect := len(stmt.Line) > 0 && !hasComments(stmt.Comments)
+			allIndirect := len(stmt.Line) > 0 && !hasComments(stmt.Comments)
+			for _, line := range stmt.Line {
+				lineToBlock[line] = stmt
+				if hasComments(line.Comments) {
+					allDirect = false
+					allIndirect = false
+				} else if isIndirect(line) {
+					allDirect = false
+				} else {
+					allIndirect = false
+				}
+			}
+			if allDirect {
+				lastDirectIndex = i
+			}
+			if allIndirect {
+				lastIndirectIndex = i
+			}
+		}
+	}
+
+	oneFlatUncommentedBlock := requireLineOrBlockCount == 1 &&
+		!hasComments(*f.Syntax.Stmt[lastRequireIndex].Comment())
+
+	// Create direct and indirect blocks if needed. Convert lines into blocks
+	// if needed. If we end up with an empty block or a one-line block,
+	// Cleanup will delete it or convert it to a line later.
+	insertBlock := func(i int) *LineBlock {
+		block := &LineBlock{Token: []string{"require"}}
+		f.Syntax.Stmt = append(f.Syntax.Stmt, nil)
+		copy(f.Syntax.Stmt[i+1:], f.Syntax.Stmt[i:])
+		f.Syntax.Stmt[i] = block
+		return block
+	}
+
+	ensureBlock := func(i int) *LineBlock {
+		switch stmt := f.Syntax.Stmt[i].(type) {
+		case *LineBlock:
+			return stmt
+		case *Line:
+			block := &LineBlock{
+				Token: []string{"require"},
+				Line:  []*Line{stmt},
+			}
+			stmt.Token = stmt.Token[1:] // remove "require"
+			stmt.InBlock = true
+			f.Syntax.Stmt[i] = block
+			return block
+		default:
+			panic(fmt.Sprintf("unexpected statement: %v", stmt))
+		}
+	}
+
+	var lastDirectBlock *LineBlock
+	if lastDirectIndex < 0 {
+		if lastIndirectIndex >= 0 {
+			lastDirectIndex = lastIndirectIndex
+			lastIndirectIndex++
+		} else if lastRequireIndex >= 0 {
+			lastDirectIndex = lastRequireIndex + 1
+		} else {
+			lastDirectIndex = len(f.Syntax.Stmt)
+		}
+		lastDirectBlock = insertBlock(lastDirectIndex)
+	} else {
+		lastDirectBlock = ensureBlock(lastDirectIndex)
+	}
+
+	var lastIndirectBlock *LineBlock
+	if lastIndirectIndex < 0 {
+		lastIndirectIndex = lastDirectIndex + 1
+		lastIndirectBlock = insertBlock(lastIndirectIndex)
+	} else {
+		lastIndirectBlock = ensureBlock(lastIndirectIndex)
+	}
+
+	// Delete requirements we don't want anymore.
+	// Update versions and indirect comments on requirements we want to keep.
+	// If a requirement is in last{Direct,Indirect}Block with the wrong
+	// indirect marking after this, or if the requirement is in an single
+	// uncommented mixed block (oneFlatUncommentedBlock), move it to the
+	// correct block.
+	//
+	// Some blocks may be empty after this. Cleanup will remove them.
+	need := make(map[string]*Require)
+	for _, r := range req {
+		need[r.Mod.Path] = r
+	}
+	have := make(map[string]*Require)
+	for _, r := range f.Require {
+		path := r.Mod.Path
+		if need[path] == nil || have[path] != nil {
+			// Requirement not needed, or duplicate requirement. Delete.
+			r.markRemoved()
+			continue
+		}
+		have[r.Mod.Path] = r
+		r.setVersion(need[path].Mod.Version)
+		r.setIndirect(need[path].Indirect)
+		if need[path].Indirect &&
+			(oneFlatUncommentedBlock || lineToBlock[r.Syntax] == lastDirectBlock) {
+			moveReq(r, lastIndirectBlock)
+		} else if !need[path].Indirect &&
+			(oneFlatUncommentedBlock || lineToBlock[r.Syntax] == lastIndirectBlock) {
+			moveReq(r, lastDirectBlock)
+		}
+	}
+
+	// Add new requirements.
+	for path, r := range need {
+		if have[path] == nil {
+			if r.Indirect {
+				moveReq(r, lastIndirectBlock)
+			} else {
+				moveReq(r, lastDirectBlock)
+			}
+			f.Require = append(f.Require, r)
+		}
+	}
+
+	f.SortBlocks()
+}
+
+func (f *File) DropRequire(path string) error {
+	for _, r := range f.Require {
+		if r.Mod.Path == path {
+			r.Syntax.markRemoved()
+			*r = Require{}
+		}
+	}
+	return nil
+}
+
+// AddExclude adds a exclude statement to the mod file. Errors if the provided
+// version is not a canonical version string
+func (f *File) AddExclude(path, vers string) error {
+	if err := checkCanonicalVersion(path, vers); err != nil {
+		return err
+	}
+
+	var hint *Line
+	for _, x := range f.Exclude {
+		if x.Mod.Path == path && x.Mod.Version == vers {
+			return nil
+		}
+		if x.Mod.Path == path {
+			hint = x.Syntax
+		}
+	}
+
+	f.Exclude = append(f.Exclude, &Exclude{Mod: module.Version{Path: path, Version: vers}, Syntax: f.Syntax.addLine(hint, "exclude", AutoQuote(path), vers)})
+	return nil
+}
+
+func (f *File) DropExclude(path, vers string) error {
+	for _, x := range f.Exclude {
+		if x.Mod.Path == path && x.Mod.Version == vers {
+			x.Syntax.markRemoved()
+			*x = Exclude{}
+		}
+	}
+	return nil
+}
+
+func (f *File) AddReplace(oldPath, oldVers, newPath, newVers string) error {
+	return addReplace(f.Syntax, &f.Replace, oldPath, oldVers, newPath, newVers)
+}
+
+func addReplace(syntax *FileSyntax, replace *[]*Replace, oldPath, oldVers, newPath, newVers string) error {
+	need := true
+	old := module.Version{Path: oldPath, Version: oldVers}
+	new := module.Version{Path: newPath, Version: newVers}
+	tokens := []string{"replace", AutoQuote(oldPath)}
+	if oldVers != "" {
+		tokens = append(tokens, oldVers)
+	}
+	tokens = append(tokens, "=>", AutoQuote(newPath))
+	if newVers != "" {
+		tokens = append(tokens, newVers)
+	}
+
+	var hint *Line
+	for _, r := range *replace {
+		if r.Old.Path == oldPath && (oldVers == "" || r.Old.Version == oldVers) {
+			if need {
+				// Found replacement for old; update to use new.
+				r.New = new
+				syntax.updateLine(r.Syntax, tokens...)
+				need = false
+				continue
+			}
+			// Already added; delete other replacements for same.
+			r.Syntax.markRemoved()
+			*r = Replace{}
+		}
+		if r.Old.Path == oldPath {
+			hint = r.Syntax
+		}
+	}
+	if need {
+		*replace = append(*replace, &Replace{Old: old, New: new, Syntax: syntax.addLine(hint, tokens...)})
+	}
+	return nil
+}
+
+func (f *File) 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
+}
+
+// AddRetract adds a retract statement to the mod file. Errors if the provided
+// version interval does not consist of canonical version strings
+func (f *File) AddRetract(vi VersionInterval, rationale string) error {
+	var path string
+	if f.Module != nil {
+		path = f.Module.Mod.Path
+	}
+	if err := checkCanonicalVersion(path, vi.High); err != nil {
+		return err
+	}
+	if err := checkCanonicalVersion(path, vi.Low); err != nil {
+		return err
+	}
+
+	r := &Retract{
+		VersionInterval: vi,
+	}
+	if vi.Low == vi.High {
+		r.Syntax = f.Syntax.addLine(nil, "retract", AutoQuote(vi.Low))
+	} else {
+		r.Syntax = f.Syntax.addLine(nil, "retract", "[", AutoQuote(vi.Low), ",", AutoQuote(vi.High), "]")
+	}
+	if rationale != "" {
+		for _, line := range strings.Split(rationale, "\n") {
+			com := Comment{Token: "// " + line}
+			r.Syntax.Comment().Before = append(r.Syntax.Comment().Before, com)
+		}
+	}
+	return nil
+}
+
+func (f *File) DropRetract(vi VersionInterval) error {
+	for _, r := range f.Retract {
+		if r.VersionInterval == vi {
+			r.Syntax.markRemoved()
+			*r = Retract{}
+		}
+	}
+	return nil
+}
+
+func (f *File) SortBlocks() {
+	f.removeDups() // otherwise sorting is unsafe
+
+	// semanticSortForExcludeVersionV is the Go version (plus leading "v") at which
+	// lines in exclude blocks start to use semantic sort instead of lexicographic sort.
+	// See go.dev/issue/60028.
+	const semanticSortForExcludeVersionV = "v1.21"
+	useSemanticSortForExclude := f.Go != nil && semver.Compare("v"+f.Go.Version, semanticSortForExcludeVersionV) >= 0
+
+	for _, stmt := range f.Syntax.Stmt {
+		block, ok := stmt.(*LineBlock)
+		if !ok {
+			continue
+		}
+		less := lineLess
+		if block.Token[0] == "exclude" && useSemanticSortForExclude {
+			less = lineExcludeLess
+		} else if block.Token[0] == "retract" {
+			less = lineRetractLess
+		}
+		sort.SliceStable(block.Line, func(i, j int) bool {
+			return less(block.Line[i], block.Line[j])
+		})
+	}
+}
+
+// removeDups removes duplicate exclude and replace directives.
+//
+// Earlier exclude directives take priority.
+//
+// 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 *File) removeDups() {
+	removeDups(f.Syntax, &f.Exclude, &f.Replace)
+}
+
+func removeDups(syntax *FileSyntax, exclude *[]*Exclude, replace *[]*Replace) {
+	kill := make(map[*Line]bool)
+
+	// Remove duplicate excludes.
+	if exclude != nil {
+		haveExclude := make(map[module.Version]bool)
+		for _, x := range *exclude {
+			if haveExclude[x.Mod] {
+				kill[x.Syntax] = true
+				continue
+			}
+			haveExclude[x.Mod] = true
+		}
+		var excl []*Exclude
+		for _, x := range *exclude {
+			if !kill[x.Syntax] {
+				excl = append(excl, x)
+			}
+		}
+		*exclude = excl
+	}
+
+	// Remove duplicate replacements.
+	// Later replacements take priority over earlier ones.
+	haveReplace := make(map[module.Version]bool)
+	for i := len(*replace) - 1; i >= 0; i-- {
+		x := (*replace)[i]
+		if haveReplace[x.Old] {
+			kill[x.Syntax] = true
+			continue
+		}
+		haveReplace[x.Old] = true
+	}
+	var repl []*Replace
+	for _, x := range *replace {
+		if !kill[x.Syntax] {
+			repl = append(repl, x)
+		}
+	}
+	*replace = repl
+
+	// Duplicate require and retract directives are not removed.
+
+	// Drop killed statements from the syntax tree.
+	var stmts []Expr
+	for _, stmt := range 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)
+	}
+	syntax.Stmt = stmts
+}
+
+// lineLess returns whether li should be sorted before lj. It sorts
+// lexicographically without assigning any special meaning to tokens.
+func lineLess(li, lj *Line) bool {
+	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)
+}
+
+// lineExcludeLess reports whether li should be sorted before lj for lines in
+// an "exclude" block.
+func lineExcludeLess(li, lj *Line) bool {
+	if len(li.Token) != 2 || len(lj.Token) != 2 {
+		// Not a known exclude specification.
+		// Fall back to sorting lexicographically.
+		return lineLess(li, lj)
+	}
+	// An exclude specification has two tokens: ModulePath and Version.
+	// Compare module path by string order and version by semver rules.
+	if pi, pj := li.Token[0], lj.Token[0]; pi != pj {
+		return pi < pj
+	}
+	return semver.Compare(li.Token[1], lj.Token[1]) < 0
+}
+
+// lineRetractLess returns whether li should be sorted before lj for lines in
+// a "retract" block. It treats each line as a version interval. Single versions
+// are compared as if they were intervals with the same low and high version.
+// Intervals are sorted in descending order, first by low version, then by
+// high version, using semver.Compare.
+func lineRetractLess(li, lj *Line) bool {
+	interval := func(l *Line) VersionInterval {
+		if len(l.Token) == 1 {
+			return VersionInterval{Low: l.Token[0], High: l.Token[0]}
+		} else if len(l.Token) == 5 && l.Token[0] == "[" && l.Token[2] == "," && l.Token[4] == "]" {
+			return VersionInterval{Low: l.Token[1], High: l.Token[3]}
+		} else {
+			// Line in unknown format. Treat as an invalid version.
+			return VersionInterval{}
+		}
+	}
+	vii := interval(li)
+	vij := interval(lj)
+	if cmp := semver.Compare(vii.Low, vij.Low); cmp != 0 {
+		return cmp > 0
+	}
+	return semver.Compare(vii.High, vij.High) > 0
+}
+
+// checkCanonicalVersion returns a non-nil error if vers is not a canonical
+// version string or does not match the major version of path.
+//
+// If path is non-empty, the error text suggests a format with a major version
+// corresponding to the path.
+func checkCanonicalVersion(path, vers string) error {
+	_, pathMajor, pathMajorOk := module.SplitPathVersion(path)
+
+	if vers == "" || vers != module.CanonicalVersion(vers) {
+		if pathMajor == "" {
+			return &module.InvalidVersionError{
+				Version: vers,
+				Err:     fmt.Errorf("must be of the form v1.2.3"),
+			}
+		}
+		return &module.InvalidVersionError{
+			Version: vers,
+			Err:     fmt.Errorf("must be of the form %s.2.3", module.PathMajorPrefix(pathMajor)),
+		}
+	}
+
+	if pathMajorOk {
+		if err := module.CheckPathMajor(vers, pathMajor); err != nil {
+			if pathMajor == "" {
+				// In this context, the user probably wrote "v2.3.4" when they meant
+				// "v2.3.4+incompatible". Suggest that instead of "v0 or v1".
+				return &module.InvalidVersionError{
+					Version: vers,
+					Err:     fmt.Errorf("should be %s+incompatible (or module %s/%v)", vers, path, semver.Major(vers)),
+				}
+			}
+			return err
+		}
+	}
+
+	return nil
+}

+ 285 - 0
vendor/golang.org/x/mod/modfile/work.go

@@ -0,0 +1,285 @@
+// 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)
+}

+ 1 - 0
vendor/modules.txt

@@ -1249,6 +1249,7 @@ golang.org/x/exp/slices
 # golang.org/x/mod v0.13.0
 ## explicit; go 1.18
 golang.org/x/mod/internal/lazyregexp
+golang.org/x/mod/modfile
 golang.org/x/mod/module
 golang.org/x/mod/semver
 # golang.org/x/net v0.17.0