Repository: goplus/gop
Pull request #922 (2021-11-22 06:48:05)

add modfile test
Use the latest modfile version and test


Patch:
diff --git a/x/mod/modfile/read.go b/x/mod/modfile/read.go
index 3cc9a1b3..85fad8a2 100644
--- a/x/mod/modfile/read.go
+++ b/x/mod/modfile/read.go
@@ -13,7 +13,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package modfile
 
 import (
@@ -206,12 +205,15 @@ func (x *FileSyntax) updateLine(line *Line, tokens ...string) {
 	line.Token = tokens
 }
 
-func (x *FileSyntax) removeLine(line *Line) {
+// 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, removeLine marks the line as dead
+// 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.
diff --git a/x/mod/modfile/rule.go b/x/mod/modfile/rule.go
index c602667d..6061c458 100644
--- a/x/mod/modfile/rule.go
+++ b/x/mod/modfile/rule.go
@@ -48,8 +48,9 @@ type File struct {
 
 // A Module is the module statement.
 type Module struct {
-	Mod    module.Version
-	Syntax *Line
+	Mod        module.Version
+	Deprecated string
+	Syntax     *Line
 }
 
 // A Go is the go statement.
@@ -113,6 +114,86 @@ type VersionInterval struct {
 	Low, High string
 }
 
+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 gop.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)
@@ -152,8 +233,15 @@ var dontFixRetract VersionFixer = func(_, vers string) (string, error) {
 	return vers, nil
 }
 
-// Parse parses the data, reported in errors as being from file,
-// into a File struct. It applies fix, if non-nil, to canonicalize all module versions found.
+// Parse parses and returns a gop.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)
 }
@@ -231,6 +319,7 @@ func parseToFile(file string, data []byte, fix VersionFixer, strict bool) (parse
 
 var GoVersionRE = lazyregexp.New(`^([1-9][0-9]*)\.(0|[1-9][0-9]*)$`)
 var GopVersionRE = lazyregexp.New(`^([1-9][0-9]*)\.(0|[1-9][0-9]*)$`)
+var laxGoVersionRE = lazyregexp.New(`^v?(([1-9][0-9]*)\.(0|[1-9][0-9]*))([^0-9].*)$`)
 
 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.
@@ -281,8 +370,17 @@ func (f *File) add(errs *ErrorList, block *LineBlock, line *Line, verb string, a
 			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
+			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}
@@ -355,7 +453,11 @@ func (f *File) add(errs *ErrorList, block *LineBlock, line *Line, verb string, a
 			errorf("repeated module statement")
 			return
 		}
-		f.Module = &Module{Syntax: line}
+		deprecated := parseDeprecation(block, line)
+		f.Module = &Module{
+			Syntax:     line,
+			Deprecated: deprecated,
+		}
 		if len(args) != 1 {
 			errorf("usage: module module/path")
 			return
@@ -405,71 +507,15 @@ func (f *File) add(errs *ErrorList, block *LineBlock, line *Line, verb string, a
 		}
 
 	case "replace":
-		arrow := 2
-		if len(args) >= 2 && args[1] == "=>" {
-			arrow = 1
-		}
-		if len(args) < arrow+2 || len(args) > arrow+3 || args[arrow] != "=>" {
-			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)
-			return
-		}
-		s, err := parseString(&args[0])
-		if err != nil {
-			errorf("invalid quoted string: %v", err)
-			return
-		}
-		pathMajor, err := modulePathMajor(s)
-		if err != nil {
-			wrapModPathError(s, err)
-			return
-		}
-		var v string
-		if arrow == 2 {
-			v, err = parseVersion(verb, s, &args[1], fix)
-			if err != nil {
-				wrapError(err)
-				return
-			}
-			if err := module.CheckPathMajor(v, pathMajor); err != nil {
-				wrapModPathError(s, err)
-				return
-			}
-		}
-		ns, err := parseString(&args[arrow+1])
-		if err != nil {
-			errorf("invalid quoted string: %v", err)
+		replace, wrappederr := parseReplace(f.Syntax.Name, line, verb, args, fix)
+		if wrappederr != nil {
+			*errs = append(*errs, *wrappederr)
 			return
 		}
-		nv := ""
-		if len(args) == arrow+2 {
-			if !IsDirectoryPath(ns) {
-				errorf("replacement module without version must be directory path (rooted or starting with ./ or ../)")
-				return
-			}
-			if filepath.Separator == '/' && strings.Contains(ns, `\`) {
-				errorf("replacement directory appears to be Windows path (on a non-windows system)")
-				return
-			}
-		}
-		if len(args) == arrow+3 {
-			nv, err = parseVersion(verb, ns, &args[arrow+2], fix)
-			if err != nil {
-				wrapError(err)
-				return
-			}
-			if IsDirectoryPath(ns) {
-				errorf("replacement module directory path %q cannot have version", ns)
-				return
-			}
-		}
-		f.Replace = append(f.Replace, &Replace{
-			Old:    module.Version{Path: s, Version: v},
-			New:    module.Version{Path: ns, Version: nv},
-			Syntax: line,
-		})
+		f.Replace = append(f.Replace, replace)
 
 	case "retract":
-		rationale := parseRetractRationale(block, line)
+		rationale := parseDirectiveComment(block, line)
 		vi, err := parseVersionInterval(verb, "", &args, dontFixRetract)
 		if err != nil {
 			if strict {
@@ -497,6 +543,83 @@ func (f *File) add(errs *ErrorList, block *LineBlock, line *Line, verb string, a
 	}
 }
 
+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) {
+			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.
 //
@@ -538,56 +661,61 @@ func (f *File) fixRetract(fix VersionFixer, errs *ErrorList) {
 	}
 }
 
-// isIndirect reports whether line has a "// indirect" comment,
-// meaning it is in gop.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
+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,
+		})
 	}
-	f := strings.Fields(strings.TrimPrefix(line.Suffix[0].Token, string(slashSlash)))
-	return (len(f) == 1 && f[0] == "indirect" || len(f) > 1 && f[0] == "indirect;")
-}
-
-// setIndirect sets line to have (or not have) a "// indirect" comment.
-func setIndirect(line *Line, indirect bool) {
-	if isIndirect(line) == indirect {
-		return
+	errorf := func(format string, args ...interface{}) {
+		wrapError(fmt.Errorf(format, args...))
 	}
-	if indirect {
-		// Adding comment.
-		if len(line.Suffix) == 0 {
-			// New comment.
-			line.Suffix = []Comment{{Token: "// indirect", Suffix: true}}
+
+	switch verb {
+	default:
+		errorf("unknown directive: %s", verb)
+
+	case "go":
+		if f.Go != nil {
+			errorf("repeated go statement")
 			return
 		}
-
-		com := &line.Suffix[0]
-		text := strings.TrimSpace(strings.TrimPrefix(com.Token, string(slashSlash)))
-		if text == "" {
-			// Empty comment.
-			com.Token = "// indirect"
+		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
 		}
 
-		// Insert at beginning of existing comment.
-		com.Token = "// indirect; " + text
-		return
-	}
+		f.Go = &Go{Syntax: line}
+		f.Go.Version = args[0]
 
-	// Removing comment.
-	f := strings.Fields(line.Suffix[0].Token)
-	if len(f) == 2 {
-		// Remove whole comment.
-		line.Suffix = nil
-		return
-	}
+	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,
+		})
 
-	// Remove comment prefix.
-	com := &line.Suffix[0]
-	i := strings.Index(com.Token, "indirect;")
-	com.Token = "//" + com.Token[i+len("indirect;"):]
+	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
@@ -696,10 +824,29 @@ func parseString(s *string) (string, error) {
 	return t, nil
 }
 
-// parseRetractRationale extracts the rationale for a retract directive from the
-// surrounding comments. If the line does not have comments and is part of a
-// block that does have comments, the block's comments are used.
-func parseRetractRationale(block *LineBlock, line *Line) string {
+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()
@@ -902,6 +1049,12 @@ func (f *File) AddGoStmt(version string) error {
 	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 {
@@ -911,7 +1064,7 @@ func (f *File) AddRequire(path, vers string) error {
 				f.Syntax.updateLine(r.Syntax, "require", AutoQuote(path), vers)
 				need = false
 			} else {
-				f.Syntax.removeLine(r.Syntax)
+				r.Syntax.markRemoved()
 				*r = Require{}
 			}
 		}
@@ -923,77 +1076,290 @@ func (f *File) AddRequire(path, vers string) error {
 	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)
-	setIndirect(line, indirect)
-	f.Require = append(f.Require, &Require{module.Version{Path: path, Version: vers}, indirect, line})
+	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) {
-	need := make(map[string]string)
-	indirect := make(map[string]bool)
+	type elem struct {
+		version  string
+		indirect bool
+	}
+	need := make(map[string]elem)
 	for _, r := range req {
-		need[r.Mod.Path] = r.Mod.Version
-		indirect[r.Mod.Path] = r.Indirect
+		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 {
-		if v, ok := need[r.Mod.Path]; ok {
-			r.Mod.Version = v
-			r.Indirect = indirect[r.Mod.Path]
+		e, ok := need[r.Mod.Path]
+		if ok {
+			r.setVersion(e.version)
+			r.setIndirect(e.indirect)
 		} else {
-			*r = Require{}
+			r.markRemoved()
 		}
+		delete(need, r.Mod.Path)
 	}
 
-	var newStmts []Expr
-	for _, stmt := range f.Syntax.Stmt {
+	// 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 *LineBlock:
-			if len(stmt.Token) > 0 && stmt.Token[0] == "require" {
-				var newLines []*Line
-				for _, line := range stmt.Line {
-					if p, err := parseString(&line.Token[0]); err == nil && need[p] != "" {
-						if len(line.Comments.Before) == 1 && len(line.Comments.Before[0].Token) == 0 {
-							line.Comments.Before = line.Comments.Before[:0]
-						}
-						line.Token[1] = need[p]
-						delete(need, p)
-						setIndirect(line, indirect[p])
-						newLines = append(newLines, line)
-					}
-				}
-				if len(newLines) == 0 {
-					continue // drop stmt
+		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
 				}
-				stmt.Line = newLines
 			}
 
-		case *Line:
-			if len(stmt.Token) > 0 && stmt.Token[0] == "require" {
-				if p, err := parseString(&stmt.Token[1]); err == nil && need[p] != "" {
-					stmt.Token[2] = need[p]
-					delete(need, p)
-					setIndirect(stmt, indirect[p])
+		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 {
-					continue // drop stmt
+					allIndirect = false
 				}
 			}
+			if allDirect {
+				lastDirectIndex = i
+			}
+			if allIndirect {
+				lastIndirectIndex = i
+			}
 		}
-		newStmts = append(newStmts, stmt)
 	}
-	f.Syntax.Stmt = newStmts
 
-	for path, vers := range need {
-		f.AddNewRequire(path, vers, indirect[path])
+	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 {
-			f.Syntax.removeLine(r.Syntax)
+			r.Syntax.markRemoved()
 			*r = Require{}
 		}
 	}
@@ -1024,7 +1390,7 @@ func (f *File) AddExclude(path, vers string) error {
 func (f *File) DropExclude(path, vers string) error {
 	for _, x := range f.Exclude {
 		if x.Mod.Path == path && x.Mod.Version == vers {
-			f.Syntax.removeLine(x.Syntax)
+			x.Syntax.markRemoved()
 			*x = Exclude{}
 		}
 	}
@@ -1032,6 +1398,10 @@ func (f *File) DropExclude(path, vers string) error {
 }
 
 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}
@@ -1045,17 +1415,17 @@ func (f *File) AddReplace(oldPath, oldVers, newPath, newVers string) error {
 	}
 
 	var hint *Line
-	for _, r := range f.Replace {
+	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
-				f.Syntax.updateLine(r.Syntax, tokens...)
+				syntax.updateLine(r.Syntax, tokens...)
 				need = false
 				continue
 			}
 			// Already added; delete other replacements for same.
-			f.Syntax.removeLine(r.Syntax)
+			r.Syntax.markRemoved()
 			*r = Replace{}
 		}
 		if r.Old.Path == oldPath {
@@ -1063,7 +1433,7 @@ func (f *File) AddReplace(oldPath, oldVers, newPath, newVers string) error {
 		}
 	}
 	if need {
-		f.Replace = append(f.Replace, &Replace{Old: old, New: new, Syntax: f.Syntax.addLine(hint, tokens...)})
+		*replace = append(*replace, &Replace{Old: old, New: new, Syntax: syntax.addLine(hint, tokens...)})
 	}
 	return nil
 }
@@ -1071,7 +1441,7 @@ func (f *File) AddReplace(oldPath, oldVers, newPath, newVers string) error {
 func (f *File) DropReplace(oldPath, oldVers string) error {
 	for _, r := range f.Replace {
 		if r.Old.Path == oldPath && r.Old.Version == oldVers {
-			f.Syntax.removeLine(r.Syntax)
+			r.Syntax.markRemoved()
 			*r = Replace{}
 		}
 	}
@@ -1112,7 +1482,7 @@ func (f *File) AddRetract(vi VersionInterval, rationale string) error {
 func (f *File) DropRetract(vi VersionInterval) error {
 	for _, r := range f.Retract {
 		if r.VersionInterval == vi {
-			f.Syntax.removeLine(r.Syntax)
+			r.Syntax.markRemoved()
 			*r = Retract{}
 		}
 	}
@@ -1149,30 +1519,36 @@ func (f *File) SortBlocks() {
 // 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.
-	haveExclude := make(map[module.Version]bool)
-	for _, x := range f.Exclude {
-		if haveExclude[x.Mod] {
-			kill[x.Syntax] = true
-			continue
+	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
 		}
-		haveExclude[x.Mod] = true
-	}
-	var excl []*Exclude
-	for _, x := range f.Exclude {
-		if !kill[x.Syntax] {
-			excl = append(excl, x)
+		var excl []*Exclude
+		for _, x := range *exclude {
+			if !kill[x.Syntax] {
+				excl = append(excl, x)
+			}
 		}
+		*exclude = excl
 	}
-	f.Exclude = excl
 
 	// Remove duplicate replacements.
 	// Later replacements take priority over earlier ones.
 	haveReplace := make(map[module.Version]bool)
-	for i := len(f.Replace) - 1; i >= 0; i-- {
-		x := f.Replace[i]
+	for i := len(*replace) - 1; i >= 0; i-- {
+		x := (*replace)[i]
 		if haveReplace[x.Old] {
 			kill[x.Syntax] = true
 			continue
@@ -1180,18 +1556,18 @@ func (f *File) removeDups() {
 		haveReplace[x.Old] = true
 	}
 	var repl []*Replace
-	for _, x := range f.Replace {
+	for _, x := range *replace {
 		if !kill[x.Syntax] {
 			repl = append(repl, x)
 		}
 	}
-	f.Replace = repl
+	*replace = repl
 
 	// Duplicate require and retract directives are not removed.
 
 	// Drop killed statements from the syntax tree.
 	var stmts []Expr
-	for _, stmt := range f.Syntax.Stmt {
+	for _, stmt := range syntax.Stmt {
 		switch stmt := stmt.(type) {
 		case *Line:
 			if kill[stmt] {
@@ -1211,7 +1587,7 @@ func (f *File) removeDups() {
 		}
 		stmts = append(stmts, stmt)
 	}
-	f.Syntax.Stmt = stmts
+	syntax.Stmt = stmts
 }
 
 // lineLess returns whether li should be sorted before lj. It sorts
diff --git a/x/mod/modfile/work.go b/x/mod/modfile/work.go
new file mode 100644
index 00000000..23ae6ef2
--- /dev/null
+++ b/x/mod/modfile/work.go
@@ -0,0 +1,245 @@
+/*
+ * Copyright (c) 2021 The GoPlus Authors (goplus.org). All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package modfile
+
+import (
+	"fmt"
+	"sort"
+	"strings"
+)
+
+// A WorkFile is the parsed, interpreted form of a go.work file.
+type WorkFile struct {
+	Go      *Go
+	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 string %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 that's 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) 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)
+}


Test patch:
diff --git a/x/mod/modfile/read_test.go b/x/mod/modfile/read_test.go
new file mode 100644
index 00000000..4b1d656b
--- /dev/null
+++ b/x/mod/modfile/read_test.go
@@ -0,0 +1,602 @@
+/*
+ * Copyright (c) 2021 The GoPlus Authors (goplus.org). All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package modfile
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"reflect"
+	"strings"
+	"testing"
+)
+
+// exists reports whether the named file exists.
+func exists(name string) bool {
+	_, err := os.Stat(name)
+	return err == nil
+}
+
+// Test that reading and then writing the golden files
+// does not change their output.
+func TestPrintGolden(t *testing.T) {
+	outs, err := filepath.Glob("testdata/*.golden")
+	if err != nil {
+		t.Fatal(err)
+	}
+	for _, out := range outs {
+		out := out
+		name := strings.TrimSuffix(filepath.Base(out), ".golden")
+		t.Run(name, func(t *testing.T) {
+			t.Parallel()
+			testPrint(t, out, out)
+		})
+	}
+}
+
+// testPrint is a helper for testing the printer.
+// It reads the file named in, reformats it, and compares
+// the result to the file named out.
+func testPrint(t *testing.T, in, out string) {
+	data, err := ioutil.ReadFile(in)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+
+	golden, err := ioutil.ReadFile(out)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+
+	base := "testdata/" + filepath.Base(in)
+	f, err := parse(in, data)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+
+	ndata := Format(f)
+
+	if !bytes.Equal(ndata, golden) {
+		t.Errorf("formatted %s incorrectly: diff shows -golden, +ours", base)
+		tdiff(t, string(golden), string(ndata))
+		return
+	}
+}
+
+// TestParsePunctuation verifies that certain ASCII punctuation characters
+// (brackets, commas) are lexed as separate tokens, even when they're
+// surrounded by identifier characters.
+func TestParsePunctuation(t *testing.T) {
+	for _, test := range []struct {
+		desc, src, want string
+	}{
+		{"paren", "require ()", "require ( )"},
+		{"brackets", "require []{},", "require [ ] { } ,"},
+		{"mix", "require a[b]c{d}e,", "require a [ b ] c { d } e ,"},
+		{"block_mix", "require (\n\ta[b]\n)", "require ( a [ b ] )"},
+		{"interval", "require [v1.0.0, v1.1.0)", "require [ v1.0.0 , v1.1.0 )"},
+	} {
+		t.Run(test.desc, func(t *testing.T) {
+			f, err := parse("gop.mod", []byte(test.src))
+			if err != nil {
+				t.Fatalf("parsing %q: %v", test.src, err)
+			}
+			var tokens []string
+			for _, stmt := range f.Stmt {
+				switch stmt := stmt.(type) {
+				case *Line:
+					tokens = append(tokens, stmt.Token...)
+				case *LineBlock:
+					tokens = append(tokens, stmt.Token...)
+					tokens = append(tokens, "(")
+					for _, line := range stmt.Line {
+						tokens = append(tokens, line.Token...)
+					}
+					tokens = append(tokens, ")")
+				default:
+					t.Fatalf("parsing %q: unexpected statement of type %T", test.src, stmt)
+				}
+			}
+			got := strings.Join(tokens, " ")
+			if got != test.want {
+				t.Errorf("parsing %q: got %q, want %q", test.src, got, test.want)
+			}
+		})
+	}
+}
+
+func TestParseLax(t *testing.T) {
+	badFile := []byte(`module m
+		surprise attack
+		x y (
+			z
+		)
+		exclude v1.2.3
+		replace <-!!!
+		retract v1.2.3 v1.2.4
+		retract (v1.2.3, v1.2.4]
+		retract v1.2.3 (
+			key1 value1
+			key2 value2
+		)
+		require good v1.0.0
+	`)
+	f, err := ParseLax("file", badFile, nil)
+	if err != nil {
+		t.Fatalf("ParseLax did not ignore irrelevant errors: %v", err)
+	}
+	if f.Module == nil || f.Module.Mod.Path != "m" {
+		t.Errorf("module directive was not parsed")
+	}
+	if len(f.Require) != 1 || f.Require[0].Mod.Path != "good" {
+		t.Errorf("require directive at end of file was not parsed")
+	}
+}
+
+// Test that when files in the testdata directory are parsed
+// and printed and parsed again, we get the same parse tree
+// both times.
+func TestPrintParse(t *testing.T) {
+	outs, err := filepath.Glob("testdata/*")
+	if err != nil {
+		t.Fatal(err)
+	}
+	for _, out := range outs {
+		out := out
+		name := filepath.Base(out)
+		if !strings.HasSuffix(out, ".in") && !strings.HasSuffix(out, ".golden") {
+			continue
+		}
+		t.Run(name, func(t *testing.T) {
+			t.Parallel()
+			data, err := ioutil.ReadFile(out)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			base := "testdata/" + filepath.Base(out)
+			f, err := parse(base, data)
+			if err != nil {
+				t.Fatalf("parsing original: %v", err)
+			}
+
+			ndata := Format(f)
+			f2, err := parse(base, ndata)
+			if err != nil {
+				t.Fatalf("parsing reformatted: %v", err)
+			}
+
+			eq := eqchecker{file: base}
+			if err := eq.check(f, f2); err != nil {
+				t.Errorf("not equal (parse/Format/parse): %v", err)
+			}
+
+			pf1, err := Parse(base, data, nil)
+			if err != nil {
+				switch base {
+				case "testdata/replace2.in", "testdata/gopkg.in.golden":
+					t.Errorf("should parse %v: %v", base, err)
+				}
+			}
+			if err == nil {
+				pf2, err := Parse(base, ndata, nil)
+				if err != nil {
+					t.Fatalf("Parsing reformatted: %v", err)
+				}
+				eq := eqchecker{file: base}
+				if err := eq.check(pf1, pf2); err != nil {
+					t.Errorf("not equal (parse/Format/Parse): %v", err)
+				}
+
+				ndata2, err := pf1.Format()
+				if err != nil {
+					t.Errorf("reformat: %v", err)
+				}
+				pf3, err := Parse(base, ndata2, nil)
+				if err != nil {
+					t.Fatalf("Parsing reformatted2: %v", err)
+				}
+				eq = eqchecker{file: base}
+				if err := eq.check(pf1, pf3); err != nil {
+					t.Errorf("not equal (Parse/Format/Parse): %v", err)
+				}
+				ndata = ndata2
+			}
+
+			if strings.HasSuffix(out, ".in") {
+				golden, err := ioutil.ReadFile(strings.TrimSuffix(out, ".in") + ".golden")
+				if err != nil {
+					t.Fatal(err)
+				}
+				if !bytes.Equal(ndata, golden) {
+					t.Errorf("formatted %s incorrectly: diff shows -golden, +ours", base)
+					tdiff(t, string(golden), string(ndata))
+					return
+				}
+			}
+		})
+	}
+}
+
+// An eqchecker holds state for checking the equality of two parse trees.
+type eqchecker struct {
+	file string
+	pos  Position
+}
+
+// errorf returns an error described by the printf-style format and arguments,
+// inserting the current file position before the error text.
+func (eq *eqchecker) errorf(format string, args ...interface{}) error {
+	return fmt.Errorf("%s:%d: %s", eq.file, eq.pos.Line,
+		fmt.Sprintf(format, args...))
+}
+
+// check checks that v and w represent the same parse tree.
+// If not, it returns an error describing the first difference.
+func (eq *eqchecker) check(v, w interface{}) error {
+	return eq.checkValue(reflect.ValueOf(v), reflect.ValueOf(w))
+}
+
+var (
+	posType      = reflect.TypeOf(Position{})
+	commentsType = reflect.TypeOf(Comments{})
+)
+
+// checkValue checks that v and w represent the same parse tree.
+// If not, it returns an error describing the first difference.
+func (eq *eqchecker) checkValue(v, w reflect.Value) error {
+	// inner returns the innermost expression for v.
+	// if v is a non-nil interface value, it returns the concrete
+	// value in the interface.
+	inner := func(v reflect.Value) reflect.Value {
+		for {
+			if v.Kind() == reflect.Interface && !v.IsNil() {
+				v = v.Elem()
+				continue
+			}
+			break
+		}
+		return v
+	}
+
+	v = inner(v)
+	w = inner(w)
+	if v.Kind() == reflect.Invalid && w.Kind() == reflect.Invalid {
+		return nil
+	}
+	if v.Kind() == reflect.Invalid {
+		return eq.errorf("nil interface became %s", w.Type())
+	}
+	if w.Kind() == reflect.Invalid {
+		return eq.errorf("%s became nil interface", v.Type())
+	}
+
+	if v.Type() != w.Type() {
+		return eq.errorf("%s became %s", v.Type(), w.Type())
+	}
+
+	if p, ok := v.Interface().(Expr); ok {
+		eq.pos, _ = p.Span()
+	}
+
+	switch v.Kind() {
+	default:
+		return eq.errorf("unexpected type %s", v.Type())
+
+	case reflect.Bool, reflect.Int, reflect.String:
+		vi := v.Interface()
+		wi := w.Interface()
+		if vi != wi {
+			return eq.errorf("%v became %v", vi, wi)
+		}
+
+	case reflect.Slice:
+		vl := v.Len()
+		wl := w.Len()
+		for i := 0; i < vl || i < wl; i++ {
+			if i >= vl {
+				return eq.errorf("unexpected %s", w.Index(i).Type())
+			}
+			if i >= wl {
+				return eq.errorf("missing %s", v.Index(i).Type())
+			}
+			if err := eq.checkValue(v.Index(i), w.Index(i)); err != nil {
+				return err
+			}
+		}
+
+	case reflect.Struct:
+		// Fields in struct must match.
+		t := v.Type()
+		n := t.NumField()
+		for i := 0; i < n; i++ {
+			tf := t.Field(i)
+			switch {
+			default:
+				if err := eq.checkValue(v.Field(i), w.Field(i)); err != nil {
+					return err
+				}
+
+			case tf.Type == posType: // ignore positions
+			case tf.Type == commentsType: // ignore comment assignment
+			}
+		}
+
+	case reflect.Ptr, reflect.Interface:
+		if v.IsNil() != w.IsNil() {
+			if v.IsNil() {
+				return eq.errorf("unexpected %s", w.Elem().Type())
+			}
+			return eq.errorf("missing %s", v.Elem().Type())
+		}
+		if err := eq.checkValue(v.Elem(), w.Elem()); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// diff returns the output of running diff on b1 and b2.
+func diff(b1, b2 []byte) (data []byte, err error) {
+	f1, err := ioutil.TempFile("", "testdiff")
+	if err != nil {
+		return nil, err
+	}
+	defer os.Remove(f1.Name())
+	defer f1.Close()
+
+	f2, err := ioutil.TempFile("", "testdiff")
+	if err != nil {
+		return nil, err
+	}
+	defer os.Remove(f2.Name())
+	defer f2.Close()
+
+	f1.Write(b1)
+	f2.Write(b2)
+
+	data, err = exec.Command("diff", "-u", f1.Name(), f2.Name()).CombinedOutput()
+	if len(data) > 0 {
+		// diff exits with a non-zero status when the files don't match.
+		// Ignore that failure as long as we get output.
+		err = nil
+	}
+	return
+}
+
+// tdiff logs the diff output to t.Error.
+func tdiff(t *testing.T, a, b string) {
+	data, err := diff([]byte(a), []byte(b))
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	t.Error(string(data))
+}
+
+var modulePathTests = []struct {
+	input    []byte
+	expected string
+}{
+	{input: []byte("module \"github.com/rsc/vgotest\""), expected: "github.com/rsc/vgotest"},
+	{input: []byte("module github.com/rsc/vgotest"), expected: "github.com/rsc/vgotest"},
+	{input: []byte("module  \"github.com/rsc/vgotest\""), expected: "github.com/rsc/vgotest"},
+	{input: []byte("module  github.com/rsc/vgotest"), expected: "github.com/rsc/vgotest"},
+	{input: []byte("module `github.com/rsc/vgotest`"), expected: "github.com/rsc/vgotest"},
+	{input: []byte("module \"github.com/rsc/vgotest/v2\""), expected: "github.com/rsc/vgotest/v2"},
+	{input: []byte("module github.com/rsc/vgotest/v2"), expected: "github.com/rsc/vgotest/v2"},
+	{input: []byte("module \"gopkg.in/yaml.v2\""), expected: "gopkg.in/yaml.v2"},
+	{input: []byte("module gopkg.in/yaml.v2"), expected: "gopkg.in/yaml.v2"},
+	{input: []byte("module \"gopkg.in/check.v1\"\n"), expected: "gopkg.in/check.v1"},
+	{input: []byte("module \"gopkg.in/check.v1\n\""), expected: ""},
+	{input: []byte("module gopkg.in/check.v1\n"), expected: "gopkg.in/check.v1"},
+	{input: []byte("module \"gopkg.in/check.v1\"\r\n"), expected: "gopkg.in/check.v1"},
+	{input: []byte("module gopkg.in/check.v1\r\n"), expected: "gopkg.in/check.v1"},
+	{input: []byte("module \"gopkg.in/check.v1\"\n\n"), expected: "gopkg.in/check.v1"},
+	{input: []byte("module gopkg.in/check.v1\n\n"), expected: "gopkg.in/check.v1"},
+	{input: []byte("module \n\"gopkg.in/check.v1\"\n\n"), expected: ""},
+	{input: []byte("module \ngopkg.in/check.v1\n\n"), expected: ""},
+	{input: []byte("module \"gopkg.in/check.v1\"asd"), expected: ""},
+	{input: []byte("module \n\"gopkg.in/check.v1\"\n\n"), expected: ""},
+	{input: []byte("module \ngopkg.in/check.v1\n\n"), expected: ""},
+	{input: []byte("module \"gopkg.in/check.v1\"asd"), expected: ""},
+	{input: []byte("module  \nmodule a/b/c "), expected: "a/b/c"},
+	{input: []byte("module \"   \""), expected: "   "},
+	{input: []byte("module   "), expected: ""},
+	{input: []byte("module \"  a/b/c  \""), expected: "  a/b/c  "},
+	{input: []byte("module \"github.com/rsc/vgotest1\" // with a comment"), expected: "github.com/rsc/vgotest1"},
+}
+
+func TestModulePath(t *testing.T) {
+	for _, test := range modulePathTests {
+		t.Run(string(test.input), func(t *testing.T) {
+			result := ModulePath(test.input)
+			if result != test.expected {
+				t.Fatalf("ModulePath(%q): %s, want %s", string(test.input), result, test.expected)
+			}
+		})
+	}
+}
+
+func TestGoVersion(t *testing.T) {
+	tests := []struct {
+		desc, input string
+		ok          bool
+		laxOK       bool // ok=true implies laxOK=true; only set if ok=false
+	}{
+		{desc: "empty", input: "module m\ngo \n", ok: false},
+		{desc: "one", input: "module m\ngo 1\n", ok: false},
+		{desc: "two", input: "module m\ngo 1.22\n", ok: true},
+		{desc: "three", input: "module m\ngo 1.22.333", ok: false},
+		{desc: "before", input: "module m\ngo v1.2\n", ok: false},
+		{desc: "after", input: "module m\ngo 1.2rc1\n", ok: false},
+		{desc: "space", input: "module m\ngo 1.2 3.4\n", ok: false},
+		{desc: "alt1", input: "module m\ngo 1.2.3\n", ok: false, laxOK: true},
+		{desc: "alt2", input: "module m\ngo 1.2rc1\n", ok: false, laxOK: true},
+		{desc: "alt3", input: "module m\ngo 1.2beta1\n", ok: false, laxOK: true},
+		{desc: "alt4", input: "module m\ngo 1.2.beta1\n", ok: false, laxOK: true},
+		{desc: "alt1", input: "module m\ngo v1.2.3\n", ok: false, laxOK: true},
+		{desc: "alt2", input: "module m\ngo v1.2rc1\n", ok: false, laxOK: true},
+		{desc: "alt3", input: "module m\ngo v1.2beta1\n", ok: false, laxOK: true},
+		{desc: "alt4", input: "module m\ngo v1.2.beta1\n", ok: false, laxOK: true},
+		{desc: "alt1", input: "module m\ngo v1.2\n", ok: false, laxOK: true},
+	}
+	t.Run("Strict", func(t *testing.T) {
+		for _, test := range tests {
+			t.Run(test.desc, func(t *testing.T) {
+				if _, err := Parse("gop.mod", []byte(test.input), nil); err == nil && !test.ok {
+					t.Error("unexpected success")
+				} else if err != nil && test.ok {
+					t.Errorf("unexpected error: %v", err)
+				}
+			})
+		}
+	})
+	t.Run("Lax", func(t *testing.T) {
+		for _, test := range tests {
+			t.Run(test.desc, func(t *testing.T) {
+				if _, err := Parse("gop.mod", []byte(test.input), nil); err == nil && !(test.ok || test.laxOK) {
+					t.Error("unexpected success")
+				} else if err != nil && test.ok {
+					t.Errorf("unexpected error: %v", err)
+				}
+			})
+		}
+	})
+}
+
+func TestComments(t *testing.T) {
+	for _, test := range []struct {
+		desc, input, want string
+	}{
+		{
+			desc: "comment_only",
+			input: `
+// a
+// b
+`,
+			want: `
+comments before "// a"
+comments before "// b"
+`,
+		}, {
+			desc: "line",
+			input: `
+// a
+
+// b
+module m // c
+// d
+
+// e
+`,
+			want: `
+comments before "// a"
+line before "// b"
+line suffix "// c"
+comments before "// d"
+comments before "// e"
+`,
+		}, {
+			desc: "block",
+			input: `
+// a
+
+// b
+block ( // c
+	// d
+
+	// e
+	x // f
+	// g
+
+	// h
+) // i
+// j
+
+// k
+`,
+			want: `
+comments before "// a"
+block before "// b"
+lparen suffix "// c"
+blockline before "// d"
+blockline before ""
+blockline before "// e"
+blockline suffix "// f"
+rparen before "// g"
+rparen before ""
+rparen before "// h"
+rparen suffix "// i"
+comments before "// j"
+comments before "// k"
+`,
+		}, {
+			desc:  "cr_removed",
+			input: "// a\r\r\n",
+			want:  `comments before "// a\r"`,
+		},
+	} {
+		t.Run(test.desc, func(t *testing.T) {
+			f, err := ParseLax("gop.mod", []byte(test.input), nil)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			buf := &bytes.Buffer{}
+			printComments := func(prefix string, cs *Comments) {
+				for _, c := range cs.Before {
+					fmt.Fprintf(buf, "%s before %q\n", prefix, c.Token)
+				}
+				for _, c := range cs.Suffix {
+					fmt.Fprintf(buf, "%s suffix %q\n", prefix, c.Token)
+				}
+				for _, c := range cs.After {
+					fmt.Fprintf(buf, "%s after %q\n", prefix, c.Token)
+				}
+			}
+
+			printComments("file", &f.Syntax.Comments)
+			for _, stmt := range f.Syntax.Stmt {
+				switch stmt := stmt.(type) {
+				case *CommentBlock:
+					printComments("comments", stmt.Comment())
+				case *Line:
+					printComments("line", stmt.Comment())
+				case *LineBlock:
+					printComments("block", stmt.Comment())
+					printComments("lparen", stmt.LParen.Comment())
+					for _, line := range stmt.Line {
+						printComments("blockline", line.Comment())
+					}
+					printComments("rparen", stmt.RParen.Comment())
+				}
+			}
+
+			got := strings.TrimSpace(buf.String())
+			want := strings.TrimSpace(test.want)
+			if got != want {
+				t.Errorf("got:\n%s\nwant:\n%s", got, want)
+			}
+		})
+	}
+}
diff --git a/x/mod/modfile/rule_test.go b/x/mod/modfile/rule_test.go
index 8ee6017b..4245c505 100644
--- a/x/mod/modfile/rule_test.go
+++ b/x/mod/modfile/rule_test.go
@@ -13,13 +13,1630 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package modfile
 
 import (
+	"bytes"
+	"fmt"
+	"strings"
 	"testing"
+
+	"golang.org/x/mod/module"
 )
 
+var addRequireTests = []struct {
+	desc string
+	in   string
+	path string
+	vers string
+	out  string
+}{
+	{
+		`existing`,
+		`
+		module m
+		require x.y/z v1.2.3
+		`,
+		"x.y/z", "v1.5.6",
+		`
+		module m
+		require x.y/z v1.5.6
+		`,
+	},
+	{
+		`existing2`,
+		`
+		module m
+		require (
+			x.y/z v1.2.3 // first
+			x.z/a v0.1.0 // first-a
+		)
+		require x.y/z v1.4.5 // second
+		require (
+			x.y/z v1.6.7 // third
+			x.z/a v0.2.0 // third-a
+		)
+		`,
+		"x.y/z", "v1.8.9",
+		`
+		module m
+
+		require (
+			x.y/z v1.8.9 // first
+			x.z/a v0.1.0 // first-a
+		)
+
+		require x.z/a v0.2.0 // third-a
+		`,
+	},
+	{
+		`new`,
+		`
+		module m
+		require x.y/z v1.2.3
+		`,
+		"x.y/w", "v1.5.6",
+		`
+		module m
+		require (
+			x.y/z v1.2.3
+			x.y/w v1.5.6
+		)
+		`,
+	},
+	{
+		`new2`,
+		`
+		module m
+		require x.y/z v1.2.3
+		require x.y/q/v2 v2.3.4
+		`,
+		"x.y/w", "v1.5.6",
+		`
+		module m
+		require x.y/z v1.2.3
+		require (
+			x.y/q/v2 v2.3.4
+			x.y/w v1.5.6
+		)
+		`,
+	},
+}
+
+type require struct {
+	path, vers string
+	indirect   bool
+}
+
+var setRequireTests = []struct {
+	desc string
+	in   string
+	mods []require
+	out  string
+}{
+	{
+		`https://golang.org/issue/45932`,
+		`module m
+		require (
+			x.y/a v1.2.3 //indirect
+			x.y/b v1.2.3
+			x.y/c v1.2.3
+		)
+		`,
+		[]require{
+			{"x.y/a", "v1.2.3", false},
+			{"x.y/b", "v1.2.3", false},
+			{"x.y/c", "v1.2.3", false},
+		},
+		`module m
+		require (
+			x.y/a v1.2.3
+			x.y/b v1.2.3
+			x.y/c v1.2.3
+		)
+		`,
+	},
+	{
+		`existing`,
+		`module m
+		require (
+			x.y/b v1.2.3
+
+			x.y/a v1.2.3
+			x.y/d v1.2.3
+		)
+		`,
+		[]require{
+			{"x.y/a", "v1.2.3", false},
+			{"x.y/b", "v1.2.3", false},
+			{"x.y/c", "v1.2.3", false},
+		},
+		`module m
+		require (
+			x.y/a v1.2.3
+			x.y/b v1.2.3
+			x.y/c v1.2.3
+		)
+		`,
+	},
+	{
+		`existing_indirect`,
+		`module m
+		require (
+			x.y/a v1.2.3
+			x.y/b v1.2.3 //
+			x.y/c v1.2.3 //c
+			x.y/d v1.2.3 //   c
+			x.y/e v1.2.3 // indirect
+			x.y/f v1.2.3 //indirect
+			x.y/g v1.2.3 //	indirect
+		)
+		`,
+		[]require{
+			{"x.y/a", "v1.2.3", true},
+			{"x.y/b", "v1.2.3", true},
+			{"x.y/c", "v1.2.3", true},
+			{"x.y/d", "v1.2.3", true},
+			{"x.y/e", "v1.2.3", true},
+			{"x.y/f", "v1.2.3", true},
+			{"x.y/g", "v1.2.3", true},
+		},
+		`module m
+		require (
+			x.y/a v1.2.3 // indirect
+			x.y/b v1.2.3 // indirect
+			x.y/c v1.2.3 // indirect; c
+			x.y/d v1.2.3 // indirect; c
+			x.y/e v1.2.3 // indirect
+			x.y/f v1.2.3 //indirect
+			x.y/g v1.2.3 //	indirect
+		)
+		`,
+	},
+	{
+		`existing_multi`,
+		`module m
+		require x.y/a v1.2.3
+		require x.y/b v1.2.3
+		require x.y/c v1.0.0 // not v1.2.3!
+		require x.y/d v1.2.3 // comment kept
+		require x.y/e v1.2.3 // comment kept
+		require x.y/f v1.2.3 // indirect
+		require x.y/g v1.2.3 // indirect
+		`,
+		[]require{
+			{"x.y/h", "v1.2.3", false},
+			{"x.y/a", "v1.2.3", false},
+			{"x.y/b", "v1.2.3", false},
+			{"x.y/c", "v1.2.3", false},
+			{"x.y/d", "v1.2.3", false},
+			{"x.y/e", "v1.2.3", true},
+			{"x.y/f", "v1.2.3", false},
+			{"x.y/g", "v1.2.3", false},
+		},
+		`module m
+		require x.y/a v1.2.3
+
+		require x.y/b v1.2.3
+
+		require x.y/c v1.2.3 // not v1.2.3!
+
+		require x.y/d v1.2.3 // comment kept
+
+		require x.y/e v1.2.3 // indirect; comment kept
+
+		require x.y/f v1.2.3
+
+		require (
+			x.y/g v1.2.3
+			x.y/h v1.2.3
+		)
+		`,
+	},
+	{
+		`existing_duplicate`,
+		`module m
+		require (
+			x.y/a v1.0.0 // zero
+			x.y/a v1.1.0 // one
+			x.y/a v1.2.3 // two
+		)
+		`,
+		[]require{
+			{"x.y/a", "v1.2.3", true},
+		},
+		`module m
+		require x.y/a v1.2.3 // indirect; zero
+		`,
+	},
+	{
+		`existing_duplicate_multi`,
+		`module m
+		require x.y/a v1.0.0 // zero
+		require x.y/a v1.1.0 // one
+		require x.y/a v1.2.3 // two
+		`,
+		[]require{
+			{"x.y/a", "v1.2.3", true},
+		},
+		`module m
+		require x.y/a v1.2.3 // indirect; zero
+		`,
+	},
+}
+
+var setRequireSeparateIndirectTests = []struct {
+	desc string
+	in   string
+	mods []require
+	out  string
+}{
+	{
+		`https://golang.org/issue/45932`,
+		`module m
+		require (
+			x.y/a v1.2.3 //indirect
+			x.y/b v1.2.3
+			x.y/c v1.2.3
+		)
+		`,
+		[]require{
+			{"x.y/a", "v1.2.3", false},
+			{"x.y/b", "v1.2.3", false},
+			{"x.y/c", "v1.2.3", false},
+		},
+		`module m
+		require (
+			x.y/a v1.2.3
+			x.y/b v1.2.3
+			x.y/c v1.2.3
+		)
+		`,
+	},
+	{
+		`existing`,
+		`module m
+		require (
+			x.y/b v1.2.3
+
+			x.y/a v1.2.3
+			x.y/d v1.2.3
+		)
+		`,
+		[]require{
+			{"x.y/a", "v1.2.3", false},
+			{"x.y/b", "v1.2.3", false},
+			{"x.y/c", "v1.2.3", false},
+		},
+		`module m
+		require (
+			x.y/a v1.2.3
+			x.y/b v1.2.3
+			x.y/c v1.2.3
+		)
+		`,
+	},
+	{
+		`existing_indirect`,
+		`module m
+		require (
+			x.y/a v1.2.3
+			x.y/b v1.2.3 //
+			x.y/c v1.2.3 //c
+			x.y/d v1.2.3 //   c
+			x.y/e v1.2.3 // indirect
+			x.y/f v1.2.3 //indirect
+			x.y/g v1.2.3 //	indirect
+		)
+		`,
+		[]require{
+			{"x.y/a", "v1.2.3", true},
+			{"x.y/b", "v1.2.3", true},
+			{"x.y/c", "v1.2.3", true},
+			{"x.y/d", "v1.2.3", true},
+			{"x.y/e", "v1.2.3", true},
+			{"x.y/f", "v1.2.3", true},
+			{"x.y/g", "v1.2.3", true},
+		},
+		`module m
+		require (
+			x.y/a v1.2.3 // indirect
+			x.y/b v1.2.3 // indirect
+			x.y/c v1.2.3 // indirect; c
+			x.y/d v1.2.3 // indirect; c
+			x.y/e v1.2.3 // indirect
+			x.y/f v1.2.3 //indirect
+			x.y/g v1.2.3 //	indirect
+		)
+		`,
+	},
+	{
+		`existing_line`,
+		`module m
+		require x.y/a v1.0.0
+		require x.y/c v1.0.0 // indirect
+		`,
+		[]require{
+			{"x.y/a", "v1.2.3", false},
+			{"x.y/b", "v1.2.3", false},
+			{"x.y/c", "v1.2.3", true},
+			{"x.y/d", "v1.2.3", true},
+		},
+		`module m
+		require (
+			x.y/a v1.2.3
+			x.y/b v1.2.3
+		)
+		require (
+			x.y/c v1.2.3 // indirect
+			x.y/d v1.2.3 // indirect
+		)`,
+	},
+	{
+		`existing_multi`,
+		`module m
+		require x.y/a v1.2.3
+		require x.y/b v1.2.3 // demoted to indirect
+		require x.y/c v1.0.0 // not v1.2.3!
+		require x.y/d v1.2.3 // comment kept
+		require x.y/e v1.2.3 // comment kept
+		require x.y/f v1.2.3 // indirect; promoted to direct
+		// promoted to direct
+		require x.y/g v1.2.3 // indirect
+		require x.y/i v1.2.3 // indirect
+		require x.y/j v1.2.3 // indirect
+		`,
+		[]require{
+			{"x.y/h", "v1.2.3", false}, // out of alphabetical order
+			{"x.y/i", "v1.2.3", true},
+			{"x.y/j", "v1.2.3", true},
+			{"x.y/a", "v1.2.3", false},
+			{"x.y/b", "v1.2.3", true},
+			{"x.y/c", "v1.2.3", false},
+			{"x.y/d", "v1.2.3", false},
+			{"x.y/e", "v1.2.3", true},
+			{"x.y/f", "v1.2.3", false},
+			{"x.y/g", "v1.2.3", false},
+		},
+		`module m
+		require (
+			x.y/a v1.2.3
+			x.y/h v1.2.3
+		)
+		require x.y/b v1.2.3 // indirect; demoted to indirect
+		require x.y/c v1.2.3 // not v1.2.3!
+		require x.y/d v1.2.3 // comment kept
+		require x.y/e v1.2.3 // indirect; comment kept
+		require x.y/f v1.2.3 // promoted to direct
+		// promoted to direct
+		require x.y/g v1.2.3
+		require x.y/i v1.2.3 // indirect
+		require x.y/j v1.2.3 // indirect
+		`,
+	},
+	{
+		`existing_duplicate`,
+		`module m
+		require (
+			x.y/a v1.0.0 // zero
+			x.y/a v1.1.0 // one
+			x.y/a v1.2.3 // two
+		)
+		`,
+		[]require{
+			{"x.y/a", "v1.2.3", true},
+		},
+		`module m
+		require x.y/a v1.2.3 // indirect; zero
+		`,
+	},
+	{
+		`existing_duplicate_multi`,
+		`module m
+		require x.y/a v1.0.0 // zero
+		require x.y/a v1.1.0 // one
+		require x.y/a v1.2.3 // two
+		`,
+		[]require{
+			{"x.y/a", "v1.2.3", true},
+		},
+		`module m
+		require x.y/a v1.2.3 // indirect; zero
+		`,
+	},
+	{
+		`existing_duplicate_mix_indirect`,
+		`module m
+		require (
+			x.y/a v1.0.0 // zero
+			x.y/a v1.1.0 // indirect; one
+			x.y/a v1.2.3 // indirect; two
+		)
+		`,
+		[]require{
+			{"x.y/a", "v1.2.3", true},
+		},
+		`module m
+		require x.y/a v1.2.3 // indirect; zero
+		`,
+	},
+	{
+		`existing_duplicate_mix_direct`,
+		`module m
+		require (
+			x.y/a v1.0.0 // indirect; zero
+			x.y/a v1.1.0 // one
+			x.y/a v1.2.3 // two
+		)
+		`,
+		[]require{
+			{"x.y/a", "v1.2.3", false},
+		},
+		`module m
+		require x.y/a v1.2.3 // zero
+		`,
+	},
+	{
+		`add_indirect_after_last_direct`,
+		`module m
+		require (
+			x.y/a v1.0.0 // comment a preserved
+			x.y/d v1.0.0 // comment d preserved
+		)
+		require (
+			x.y/b v1.0.0 // comment b preserved
+			x.y/e v1.0.0 // comment e preserved
+		)
+		go 1.17
+		`,
+		[]require{
+			{"x.y/a", "v1.2.3", false},
+			{"x.y/b", "v1.2.3", false},
+			{"x.y/c", "v1.2.3", true},
+			{"x.y/d", "v1.2.3", false},
+			{"x.y/e", "v1.2.3", false},
+			{"x.y/f", "v1.2.3", true},
+		},
+		`module m
+		require (
+			x.y/a v1.2.3 // comment a preserved
+			x.y/d v1.2.3 // comment d preserved
+		)
+		require (
+			x.y/b v1.2.3 // comment b preserved
+			x.y/e v1.2.3 // comment e preserved
+		)
+		require (
+			x.y/c v1.2.3 // indirect
+			x.y/f v1.2.3 // indirect
+		)
+		go 1.17
+		`,
+	},
+	{
+		`add_direct_before_first_indirect`,
+		`module m
+		require (
+			x.y/b v1.0.0 // indirect; comment b preserved
+			x.y/e v1.0.0 // indirect; comment d preserved
+		)
+		require (
+			x.y/c v1.0.0 // indirect; comment c preserved
+			x.y/f v1.0.0 // indirect; comment e preserved
+		)
+		`,
+		[]require{
+			{"x.y/a", "v1.2.3", false},
+			{"x.y/b", "v1.2.3", true},
+			{"x.y/c", "v1.2.3", true},
+			{"x.y/d", "v1.2.3", false},
+			{"x.y/e", "v1.2.3", true},
+			{"x.y/f", "v1.2.3", true},
+		},
+		`module m
+		require (
+			x.y/b v1.2.3 // indirect; comment b preserved
+			x.y/e v1.2.3 // indirect; comment d preserved
+		)
+		require (
+			x.y/c v1.2.3 // indirect; comment c preserved
+			x.y/f v1.2.3 // indirect; comment e preserved
+		)
+		require (
+			x.y/a v1.2.3
+			x.y/d v1.2.3
+		)
+		`,
+	},
+	{
+		`add_indirect_after_mixed`,
+		`module m
+		require (
+			x.y/a v1.0.0
+			x.y/b v1.0.0 // indirect
+		)
+		`,
+		[]require{
+			{"x.y/a", "v1.2.3", false},
+			{"x.y/b", "v1.2.3", true},
+			{"x.y/c", "v1.2.3", true},
+			{"x.y/d", "v1.2.3", false},
+			{"x.y/e", "v1.2.3", true},
+		},
+		`module m
+		require (
+			x.y/a v1.2.3
+			x.y/d v1.2.3
+		)
+		require (
+			x.y/b v1.2.3 // indirect
+			x.y/c v1.2.3 // indirect
+			x.y/e v1.2.3 // indirect
+		)
+		`,
+	},
+	{
+		`preserve_block_comment_indirect_to_direct`,
+		`module m
+		// save
+		require (
+			x.y/a v1.2.3 // indirect
+		)
+		`,
+		[]require{
+			{"x.y/a", "v1.2.3", false},
+		},
+		`module m
+
+		// save
+		require x.y/a v1.2.3
+		`,
+	},
+	{
+		`preserve_block_comment_direct_to_indirect`,
+		`module m
+		// save
+		require (
+			x.y/a v1.2.3
+		)
+		`,
+		[]require{
+			{"x.y/a", "v1.2.3", true},
+		},
+		`module m
+
+		// save
+		require x.y/a v1.2.3 // indirect
+		`,
+	},
+	{
+		`regroup_flat_uncommented_block`,
+		`module m
+		require (
+			x.y/a v1.0.0 // a
+			x.y/b v1.0.0 // indirect; b
+			x.y/c v1.0.0 // indirect
+		)`,
+		[]require{
+			{"x.y/a", "v1.2.3", false},
+			{"x.y/b", "v1.2.3", true},
+			{"x.y/c", "v1.2.3", true},
+			{"x.y/d", "v1.2.3", false},
+		},
+		`module m
+		require (
+			x.y/a v1.2.3 // a
+			x.y/d v1.2.3
+		)
+		require (
+			x.y/b v1.2.3 // indirect; b
+			x.y/c v1.2.3 // indirect
+		)`,
+	},
+	{
+		`dont_regroup_flat_commented_block`,
+		`module m
+		// dont regroup
+		require (
+			x.y/a v1.0.0
+			x.y/b v1.0.0 // indirect
+			x.y/c v1.0.0 // indirect
+		)`,
+		[]require{
+			{"x.y/a", "v1.2.3", false},
+			{"x.y/b", "v1.2.3", true},
+			{"x.y/c", "v1.2.3", true},
+			{"x.y/d", "v1.2.3", false},
+		},
+		`module m
+		// dont regroup
+		require (
+			x.y/a v1.2.3
+			x.y/b v1.2.3 // indirect
+			x.y/c v1.2.3 // indirect
+		)
+		require x.y/d v1.2.3`,
+	},
+}
+
+var addGoTests = []struct {
+	desc    string
+	in      string
+	version string
+	out     string
+}{
+	{
+		`module_only`,
+		`module m
+		`,
+		`1.14`,
+		`module m
+		go 1.14
+		`,
+	},
+	{
+		`module_before_require`,
+		`module m
+		require x.y/a v1.2.3
+		`,
+		`1.14`,
+		`module m
+		go 1.14
+		require x.y/a v1.2.3
+		`,
+	},
+	{
+		`require_before_module`,
+		`require x.y/a v1.2.3
+		module example.com/inverted
+		`,
+		`1.14`,
+		`require x.y/a v1.2.3
+		module example.com/inverted
+		go 1.14
+		`,
+	},
+	{
+		`require_only`,
+		`require x.y/a v1.2.3
+		`,
+		`1.14`,
+		`require x.y/a v1.2.3
+		go 1.14
+		`,
+	},
+}
+
+var addExcludeTests = []struct {
+	desc    string
+	in      string
+	path    string
+	version string
+	out     string
+}{
+	{
+		`compatible`,
+		`module m
+		`,
+		`example.com`,
+		`v1.2.3`,
+		`module m
+		exclude example.com v1.2.3
+		`,
+	},
+	{
+		`gopkg.in v0`,
+		`module m
+		`,
+		`gopkg.in/foo.v0`,
+		`v0.2.3`,
+		`module m
+		exclude gopkg.in/foo.v0 v0.2.3
+		`,
+	},
+	{
+		`gopkg.in v1`,
+		`module m
+		`,
+		`gopkg.in/foo.v1`,
+		`v1.2.3`,
+		`module m
+		exclude gopkg.in/foo.v1 v1.2.3
+		`,
+	},
+}
+
+var addRetractTests = []struct {
+	desc      string
+	in        string
+	low       string
+	high      string
+	rationale string
+	out       string
+}{
+	{
+		`new_singleton`,
+		`module m
+		`,
+		`v1.2.3`,
+		`v1.2.3`,
+		``,
+		`module m
+		retract v1.2.3
+		`,
+	},
+	{
+		`new_interval`,
+		`module m
+		`,
+		`v1.0.0`,
+		`v1.1.0`,
+		``,
+		`module m
+		retract [v1.0.0, v1.1.0]`,
+	},
+	{
+		`duplicate_with_rationale`,
+		`module m
+		retract v1.2.3
+		`,
+		`v1.2.3`,
+		`v1.2.3`,
+		`bad`,
+		`module m
+		retract (
+			v1.2.3
+			// bad
+			v1.2.3
+		)
+		`,
+	},
+	{
+		`duplicate_multiline_rationale`,
+		`module m
+		retract [v1.2.3, v1.2.3]
+		`,
+		`v1.2.3`,
+		`v1.2.3`,
+		`multi
+line`,
+		`module m
+		retract	(
+			[v1.2.3, v1.2.3]
+			// multi
+			// line
+			v1.2.3
+		)
+		`,
+	},
+	{
+		`duplicate_interval`,
+		`module m
+		retract [v1.0.0, v1.1.0]
+		`,
+		`v1.0.0`,
+		`v1.1.0`,
+		``,
+		`module m
+		retract (
+			[v1.0.0, v1.1.0]
+			[v1.0.0, v1.1.0]
+		)
+		`,
+	},
+	{
+		`duplicate_singleton`,
+		`module m
+		retract v1.2.3
+		`,
+		`v1.2.3`,
+		`v1.2.3`,
+		``,
+		`module m
+		retract	(
+			v1.2.3
+			v1.2.3
+		)
+		`,
+	},
+}
+
+var dropRetractTests = []struct {
+	desc string
+	in   string
+	low  string
+	high string
+	out  string
+}{
+	{
+		`singleton_no_match`,
+		`module m
+		retract v1.2.3
+		`,
+		`v1.0.0`,
+		`v1.0.0`,
+		`module m
+		retract v1.2.3
+		`,
+	},
+	{
+		`singleton_match_one`,
+		`module m
+		retract v1.2.2
+		retract v1.2.3
+		retract v1.2.4
+		`,
+		`v1.2.3`,
+		`v1.2.3`,
+		`module m
+		retract v1.2.2
+		retract v1.2.4
+		`,
+	},
+	{
+		`singleton_match_all`,
+		`module m
+		retract v1.2.3 // first
+		retract v1.2.3 // second
+		`,
+		`v1.2.3`,
+		`v1.2.3`,
+		`module m
+		`,
+	},
+	{
+		`interval_match`,
+		`module m
+		retract [v1.2.3, v1.2.3]
+		`,
+		`v1.2.3`,
+		`v1.2.3`,
+		`module m
+		`,
+	},
+	{
+		`interval_superset_no_match`,
+		`module m
+		retract [v1.0.0, v1.1.0]
+		`,
+		`v1.0.0`,
+		`v1.2.0`,
+		`module m
+		retract [v1.0.0, v1.1.0]
+		`,
+	},
+	{
+		`singleton_match_middle`,
+		`module m
+		retract v1.2.3
+		`,
+		`v1.2.3`,
+		`v1.2.3`,
+		`module m
+		`,
+	},
+	{
+		`interval_match_middle_block`,
+		`module m
+		retract (
+			v1.0.0
+			[v1.1.0, v1.2.0]
+			v1.3.0
+		)
+		`,
+		`v1.1.0`,
+		`v1.2.0`,
+		`module m
+		retract (
+			v1.0.0
+			v1.3.0
+		)
+		`,
+	},
+	{
+		`interval_match_all`,
+		`module m
+		retract [v1.0.0, v1.1.0]
+		retract [v1.0.0, v1.1.0]
+		`,
+		`v1.0.0`,
+		`v1.1.0`,
+		`module m
+		`,
+	},
+}
+
+var retractRationaleTests = []struct {
+	desc, in, want string
+}{
+	{
+		`no_comment`,
+		`module m
+		retract v1.0.0`,
+		``,
+	},
+	{
+		`prefix_one`,
+		`module m
+		//   prefix
+		retract v1.0.0 
+		`,
+		`prefix`,
+	},
+	{
+		`prefix_multiline`,
+		`module m
+		//  one
+		//
+		//     two
+		//
+		// three  
+		retract v1.0.0`,
+		`one
+
+two
+
+three`,
+	},
+	{
+		`suffix`,
+		`module m
+		retract v1.0.0 // suffix
+		`,
+		`suffix`,
+	},
+	{
+		`prefix_suffix_after`,
+		`module m
+		// prefix
+		retract v1.0.0 // suffix
+		`,
+		`prefix
+suffix`,
+	},
+	{
+		`block_only`,
+		`// block
+		retract (
+			v1.0.0
+		)
+		`,
+		`block`,
+	},
+	{
+		`block_and_line`,
+		`// block
+		retract (
+			// line
+			v1.0.0
+		)
+		`,
+		`line`,
+	},
+}
+
+var moduleDeprecatedTests = []struct {
+	desc, in, want string
+}{
+	// retractRationaleTests exercises some of the same code, so these tests
+	// don't exhaustively cover comment extraction.
+	{
+		`no_comment`,
+		`module m`,
+		``,
+	},
+	{
+		`other_comment`,
+		`// yo
+		module m`,
+		``,
+	},
+	{
+		`deprecated_no_colon`,
+		`//Deprecated
+		module m`,
+		``,
+	},
+	{
+		`deprecated_no_space`,
+		`//Deprecated:blah
+		module m`,
+		`blah`,
+	},
+	{
+		`deprecated_simple`,
+		`// Deprecated: blah
+		module m`,
+		`blah`,
+	},
+	{
+		`deprecated_lowercase`,
+		`// deprecated: blah
+		module m`,
+		``,
+	},
+	{
+		`deprecated_multiline`,
+		`// Deprecated: one
+		// two
+		module m`,
+		"one\ntwo",
+	},
+	{
+		`deprecated_mixed`,
+		`// some other comment
+		// Deprecated: blah
+		module m`,
+		``,
+	},
+	{
+		`deprecated_middle`,
+		`// module m is Deprecated: blah
+		module m`,
+		``,
+	},
+	{
+		`deprecated_multiple`,
+		`// Deprecated: a
+		// Deprecated: b
+		module m`,
+		"a\nDeprecated: b",
+	},
+	{
+		`deprecated_paragraph`,
+		`// Deprecated: a
+		// b
+		//
+		// c
+		module m`,
+		"a\nb",
+	},
+	{
+		`deprecated_paragraph_space`,
+		`// Deprecated: the next line has a space
+		// 
+		// c
+		module m`,
+		"the next line has a space",
+	},
+	{
+		`deprecated_suffix`,
+		`module m // Deprecated: blah`,
+		`blah`,
+	},
+	{
+		`deprecated_mixed_suffix`,
+		`// some other comment
+		module m // Deprecated: blah`,
+		``,
+	},
+	{
+		`deprecated_mixed_suffix_paragraph`,
+		`// some other comment
+		//
+		module m // Deprecated: blah`,
+		`blah`,
+	},
+	{
+		`deprecated_block`,
+		`// Deprecated: blah
+		module (
+			m
+		)`,
+		`blah`,
+	},
+}
+
+var sortBlocksTests = []struct {
+	desc, in, out string
+	strict        bool
+}{
+	{
+		`exclude_duplicates_removed`,
+		`module m
+		exclude x.y/z v1.0.0 // a
+		exclude x.y/z v1.0.0 // b
+		exclude (
+			x.y/w v1.1.0
+			x.y/z v1.0.0 // c
+		)
+		`,
+		`module m
+		exclude x.y/z v1.0.0 // a
+		exclude (
+			x.y/w v1.1.0
+		)`,
+		true,
+	},
+	{
+		`replace_duplicates_removed`,
+		`module m
+		replace x.y/z v1.0.0 => ./a
+		replace x.y/z v1.1.0 => ./b
+		replace (
+			x.y/z v1.0.0 => ./c
+		)
+		`,
+		`module m
+		replace x.y/z v1.1.0 => ./b
+		replace (
+			x.y/z v1.0.0 => ./c
+		)
+		`,
+		true,
+	},
+	{
+		`retract_duplicates_not_removed`,
+		`module m
+		// block
+		retract (
+			v1.0.0 // one
+			v1.0.0 // two
+		)`,
+		`module m
+		// block
+		retract (
+			v1.0.0 // one
+			v1.0.0 // two
+		)`,
+		true,
+	},
+	// Tests below this point just check sort order.
+	// Non-retract blocks are sorted lexicographically in ascending order.
+	// retract blocks are sorted using semver in descending order.
+	{
+		`sort_lexicographically`,
+		`module m
+		sort (
+			aa
+			cc
+			bb
+			zz
+			v1.2.0
+			v1.11.0
+		)`,
+		`module m
+		sort (
+			aa
+			bb
+			cc
+			v1.11.0
+			v1.2.0
+			zz
+		)
+		`,
+		false,
+	},
+	{
+		`sort_retract`,
+		`module m
+		retract (
+			[v1.2.0, v1.3.0]
+			[v1.1.0, v1.3.0]
+			[v1.1.0, v1.2.0]
+			v1.0.0
+			v1.1.0
+			v1.2.0
+			v1.3.0
+			v1.4.0
+		)
+		`,
+		`module m
+		retract (
+			v1.4.0
+			v1.3.0
+			[v1.2.0, v1.3.0]
+			v1.2.0
+			[v1.1.0, v1.3.0]
+			[v1.1.0, v1.2.0]
+			v1.1.0
+			v1.0.0
+		)
+		`,
+		false,
+	},
+}
+
+var addRetractValidateVersionTests = []struct {
+	desc      string
+	path      string
+	low, high string
+	wantErr   string
+}{
+	{
+		`blank_version`,
+		`example.com/m`,
+		``,
+		``,
+		`version "" invalid: must be of the form v1.2.3`,
+	},
+	{
+		`missing prefix`,
+		`example.com/m`,
+		`1.0.0`,
+		`1.0.0`,
+		`version "1.0.0" invalid: must be of the form v1.2.3`,
+	},
+	{
+		`non-canonical`,
+		`example.com/m`,
+		`v1.2`,
+		`v1.2`,
+		`version "v1.2" invalid: must be of the form v1.2.3`,
+	},
+	{
+		`invalid range`,
+		`example.com/m`,
+		`v1.2.3`,
+		`v1.3`,
+		`version "v1.3" invalid: must be of the form v1.2.3`,
+	},
+	{
+		`mismatched major`,
+		`example.com/m/v2`,
+		`v1.0.0`,
+		`v1.0.0`,
+		`version "v1.0.0" invalid: should be v2, not v1`,
+	},
+	{
+		`missing +incompatible`,
+		`example.com/m`,
+		`v2.0.0`,
+		`v2.0.0`,
+		`version "v2.0.0" invalid: should be v2.0.0+incompatible (or module example.com/m/v2)`,
+	},
+}
+
+var addExcludeValidateVersionTests = []struct {
+	desc    string
+	path    string
+	version string
+	wantErr string
+}{
+	{
+		`blank version`,
+		`example.com/m`,
+		``,
+		`version "" invalid: must be of the form v1.2.3`,
+	},
+	{
+		`missing prefix`,
+		`example.com/m`,
+		`1.0.0`,
+		`version "1.0.0" invalid: must be of the form v1.2.3`,
+	},
+	{
+		`non-canonical`,
+		`example.com/m`,
+		`v1.2`,
+		`version "v1.2" invalid: must be of the form v1.2.3`,
+	},
+	{
+		`mismatched major`,
+		`example.com/m/v2`,
+		`v1.2.3`,
+		`version "v1.2.3" invalid: should be v2, not v1`,
+	},
+	{
+		`missing +incompatible`,
+		`example.com/m`,
+		`v2.3.4`,
+		`version "v2.3.4" invalid: should be v2.3.4+incompatible (or module example.com/m/v2)`,
+	},
+}
+
+var fixVersionTests = []struct {
+	desc, in, want, wantErr string
+	fix                     VersionFixer
+}{
+	{
+		desc: `require`,
+		in:   `require example.com/m 1.0.0`,
+		want: `require example.com/m v1.0.0`,
+		fix:  fixV,
+	},
+	{
+		desc: `replace`,
+		in:   `replace example.com/m 1.0.0 => example.com/m 1.1.0`,
+		want: `replace example.com/m v1.0.0 => example.com/m v1.1.0`,
+		fix:  fixV,
+	},
+	{
+		desc: `exclude`,
+		in:   `exclude example.com/m 1.0.0`,
+		want: `exclude example.com/m v1.0.0`,
+		fix:  fixV,
+	},
+	{
+		desc: `retract_single`,
+		in: `module example.com/m
+		retract 1.0.0`,
+		want: `module example.com/m
+		retract v1.0.0`,
+		fix: fixV,
+	},
+	{
+		desc: `retract_interval`,
+		in: `module example.com/m
+		retract [1.0.0, 1.1.0]`,
+		want: `module example.com/m
+		retract [v1.0.0, v1.1.0]`,
+		fix: fixV,
+	},
+	{
+		desc:    `retract_nomod`,
+		in:      `retract 1.0.0`,
+		wantErr: `in:1: no module directive found, so retract cannot be used`,
+		fix:     fixV,
+	},
+}
+
+func fixV(path, version string) (string, error) {
+	if path != "example.com/m" {
+		return "", fmt.Errorf("module path must be example.com/m")
+	}
+	return "v" + version, nil
+}
+
+func TestAddRequire(t *testing.T) {
+	for _, tt := range addRequireTests {
+		t.Run(tt.desc, func(t *testing.T) {
+			testEdit(t, tt.in, tt.out, true, func(f *File) error {
+				err := f.AddRequire(tt.path, tt.vers)
+				f.Cleanup()
+				return err
+			})
+		})
+	}
+}
+
+func TestSetRequire(t *testing.T) {
+	for _, tt := range setRequireTests {
+		t.Run(tt.desc, func(t *testing.T) {
+			var mods []*Require
+			for _, mod := range tt.mods {
+				mods = append(mods, &Require{
+					Mod: module.Version{
+						Path:    mod.path,
+						Version: mod.vers,
+					},
+					Indirect: mod.indirect,
+				})
+			}
+
+			f := testEdit(t, tt.in, tt.out, true, func(f *File) error {
+				f.SetRequire(mods)
+				f.Cleanup()
+				return nil
+			})
+
+			if len(f.Require) != len(mods) {
+				t.Errorf("after Cleanup, len(Require) = %v; want %v", len(f.Require), len(mods))
+			}
+		})
+	}
+}
+
+func TestSetRequireSeparateIndirect(t *testing.T) {
+	for _, tt := range setRequireSeparateIndirectTests {
+		t.Run(tt.desc, func(t *testing.T) {
+			var mods []*Require
+			for _, mod := range tt.mods {
+				mods = append(mods, &Require{
+					Mod: module.Version{
+						Path:    mod.path,
+						Version: mod.vers,
+					},
+					Indirect: mod.indirect,
+				})
+			}
+
+			f := testEdit(t, tt.in, tt.out, true, func(f *File) error {
+				f.SetRequireSeparateIndirect(mods)
+				f.Cleanup()
+				return nil
+			})
+
+			if len(f.Require) != len(mods) {
+				t.Errorf("after Cleanup, len(Require) = %v; want %v", len(f.Require), len(mods))
+			}
+		})
+	}
+}
+
+func TestAddGo(t *testing.T) {
+	for _, tt := range addGoTests {
+		t.Run(tt.desc, func(t *testing.T) {
+			testEdit(t, tt.in, tt.out, true, func(f *File) error {
+				return f.AddGoStmt(tt.version)
+			})
+		})
+	}
+}
+
+func TestAddExclude(t *testing.T) {
+	for _, tt := range addExcludeTests {
+		t.Run(tt.desc, func(t *testing.T) {
+			testEdit(t, tt.in, tt.out, true, func(f *File) error {
+				return f.AddExclude(tt.path, tt.version)
+			})
+		})
+	}
+}
+
+func TestAddRetract(t *testing.T) {
+	for _, tt := range addRetractTests {
+		t.Run(tt.desc, func(t *testing.T) {
+			testEdit(t, tt.in, tt.out, true, func(f *File) error {
+				return f.AddRetract(VersionInterval{Low: tt.low, High: tt.high}, tt.rationale)
+			})
+		})
+	}
+}
+
+func TestDropRetract(t *testing.T) {
+	for _, tt := range dropRetractTests {
+		t.Run(tt.desc, func(t *testing.T) {
+			testEdit(t, tt.in, tt.out, true, func(f *File) error {
+				if err := f.DropRetract(VersionInterval{Low: tt.low, High: tt.high}); err != nil {
+					return err
+				}
+				f.Cleanup()
+				return nil
+			})
+		})
+	}
+}
+
+func TestRetractRationale(t *testing.T) {
+	for _, tt := range retractRationaleTests {
+		t.Run(tt.desc, func(t *testing.T) {
+			f, err := Parse("in", []byte(tt.in), nil)
+			if err != nil {
+				t.Fatal(err)
+			}
+			if len(f.Retract) != 1 {
+				t.Fatalf("got %d retract directives; want 1", len(f.Retract))
+			}
+			if got := f.Retract[0].Rationale; got != tt.want {
+				t.Errorf("got %q; want %q", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestModuleDeprecated(t *testing.T) {
+	for _, tt := range moduleDeprecatedTests {
+		t.Run(tt.desc, func(t *testing.T) {
+			f, err := Parse("in", []byte(tt.in), nil)
+			if err != nil {
+				t.Fatal(err)
+			}
+			if f.Module.Deprecated != tt.want {
+				t.Errorf("got %q; want %q", f.Module.Deprecated, tt.want)
+			}
+		})
+	}
+}
+
+func TestSortBlocks(t *testing.T) {
+	for _, tt := range sortBlocksTests {
+		t.Run(tt.desc, func(t *testing.T) {
+			testEdit(t, tt.in, tt.out, tt.strict, func(f *File) error {
+				f.SortBlocks()
+				return nil
+			})
+		})
+	}
+}
+
+func testEdit(t *testing.T, in, want string, strict bool, transform func(f *File) error) *File {
+	t.Helper()
+	parse := Parse
+	if !strict {
+		parse = ParseLax
+	}
+	f, err := parse("in", []byte(in), nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+	g, err := parse("out", []byte(want), nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+	golden, err := g.Format()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if err := transform(f); err != nil {
+		t.Fatal(err)
+	}
+	out, err := f.Format()
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !bytes.Equal(out, golden) {
+		t.Errorf("have:\n%s\nwant:\n%s", out, golden)
+	}
+
+	return f
+}
+
+func TestAddRetractValidateVersion(t *testing.T) {
+	for _, tt := range addRetractValidateVersionTests {
+		t.Run(tt.desc, func(t *testing.T) {
+			f := new(File)
+			if tt.path != "" {
+				if err := f.AddModuleStmt(tt.path); err != nil {
+					t.Fatal(err)
+				}
+				t.Logf("module %s", AutoQuote(tt.path))
+			}
+			interval := VersionInterval{Low: tt.low, High: tt.high}
+			if err := f.AddRetract(interval, ``); err == nil || err.Error() != tt.wantErr {
+				errStr := "<nil>"
+				if err != nil {
+					errStr = fmt.Sprintf("%#q", err)
+				}
+				t.Fatalf("f.AddRetract(%+v, ``) = %s\nwant %#q", interval, errStr, tt.wantErr)
+			}
+		})
+	}
+}
+
+func TestAddExcludeValidateVersion(t *testing.T) {
+	for _, tt := range addExcludeValidateVersionTests {
+		t.Run(tt.desc, func(t *testing.T) {
+			f, err := Parse("in", []byte("module m"), nil)
+			if err != nil {
+				t.Fatal(err)
+			}
+			if err = f.AddExclude(tt.path, tt.version); err == nil || err.Error() != tt.wantErr {
+				errStr := "<nil>"
+				if err != nil {
+					errStr = fmt.Sprintf("%#q", err)
+				}
+				t.Fatalf("f.AddExclude(%q, %q) = %s\nwant %#q", tt.path, tt.version, errStr, tt.wantErr)
+			}
+		})
+	}
+}
+
+func TestFixVersion(t *testing.T) {
+	for _, tt := range fixVersionTests {
+		t.Run(tt.desc, func(t *testing.T) {
+			inFile, err := Parse("in", []byte(tt.in), tt.fix)
+			if err != nil {
+				if tt.wantErr == "" {
+					t.Fatalf("unexpected error: %v", err)
+				}
+				if errMsg := err.Error(); !strings.Contains(errMsg, tt.wantErr) {
+					t.Fatalf("got error %q; want error containing %q", errMsg, tt.wantErr)
+				}
+				return
+			}
+			got, err := inFile.Format()
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			outFile, err := Parse("out", []byte(tt.want), nil)
+			if err != nil {
+				t.Fatal(err)
+			}
+			want, err := outFile.Format()
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			if !bytes.Equal(got, want) {
+				t.Fatalf("got:\n%s\nwant:\n%s", got, want)
+			}
+		})
+	}
+}
+
 var gopmod = `
 module spx
 
diff --git a/x/mod/modfile/testdata/block.golden b/x/mod/modfile/testdata/block.golden
new file mode 100644
index 00000000..430c42e6
--- /dev/null
+++ b/x/mod/modfile/testdata/block.golden
@@ -0,0 +1,36 @@
+// comment
+x "y" z
+
+// block
+block ( // block-eol
+	// x-before-line
+
+	"x" (y // x-eol
+	"x") y // y-eol
+	"x1"
+	"x2"
+	// line
+	"x3"
+	"x4"
+
+	"x5"
+
+	// y-line
+	"y" //  y-eol
+
+	"z" // z-eol
+) // block-eol2
+
+block1 (
+)
+
+block2 (x y z)
+
+block3 "w" (
+) // empty block
+
+block4 "x" () "y" // not a block
+
+block5 ("z" // also not a block
+
+// eof
diff --git a/x/mod/modfile/testdata/block.in b/x/mod/modfile/testdata/block.in
new file mode 100644
index 00000000..a19990e8
--- /dev/null
+++ b/x/mod/modfile/testdata/block.in
@@ -0,0 +1,33 @@
+// comment
+x "y" z
+
+// block
+block ( // block-eol
+	// x-before-line
+	
+	"x" ( y // x-eol
+	"x" ) y // y-eol
+	"x1"
+	"x2"
+	// line
+	"x3"
+	"x4"
+	
+	"x5"
+	
+	// y-line
+	"y" //  y-eol
+	
+	"z" // z-eol
+) // block-eol2
+
+
+block1()
+
+block2 (x y z)
+
+block3 "w" ( ) // empty block
+block4 "x" ( ) "y" // not a block
+block5 ( "z" // also not a block
+
+// eof
diff --git a/x/mod/modfile/testdata/comment.golden b/x/mod/modfile/testdata/comment.golden
new file mode 100644
index 00000000..75f3b844
--- /dev/null
+++ b/x/mod/modfile/testdata/comment.golden
@@ -0,0 +1,10 @@
+// comment
+module "x" // eol
+
+// mid comment
+
+// comment 2
+// comment 2 line 2
+module "y" // eoy
+
+// comment 3
diff --git a/x/mod/modfile/testdata/comment.in b/x/mod/modfile/testdata/comment.in
new file mode 100644
index 00000000..bfc2492b
--- /dev/null
+++ b/x/mod/modfile/testdata/comment.in
@@ -0,0 +1,8 @@
+// comment
+module "x" // eol
+// mid comment
+
+// comment 2
+// comment 2 line 2
+module "y" // eoy
+// comment 3
diff --git a/x/mod/modfile/testdata/empty.golden b/x/mod/modfile/testdata/empty.golden
new file mode 100644
index 00000000..e69de29b
diff --git a/x/mod/modfile/testdata/empty.in b/x/mod/modfile/testdata/empty.in
new file mode 100644
index 00000000..e69de29b
diff --git a/x/mod/modfile/testdata/gopkg.in.golden b/x/mod/modfile/testdata/gopkg.in.golden
new file mode 100644
index 00000000..41669b3a
--- /dev/null
+++ b/x/mod/modfile/testdata/gopkg.in.golden
@@ -0,0 +1,6 @@
+module x
+
+require (
+	gopkg.in/mgo.v2 v2.0.0-20160818020120-3f83fa500528
+	gopkg.in/yaml.v2 v2.2.1
+)
diff --git a/x/mod/modfile/testdata/module.golden b/x/mod/modfile/testdata/module.golden
new file mode 100644
index 00000000..78ba9439
--- /dev/null
+++ b/x/mod/modfile/testdata/module.golden
@@ -0,0 +1,1 @@
+module abc
diff --git a/x/mod/modfile/testdata/module.in b/x/mod/modfile/testdata/module.in
new file mode 100644
index 00000000..08f38362
--- /dev/null
+++ b/x/mod/modfile/testdata/module.in
@@ -0,0 +1,1 @@
+module "abc"
diff --git a/x/mod/modfile/testdata/replace.golden b/x/mod/modfile/testdata/replace.golden
new file mode 100644
index 00000000..2e56d920
--- /dev/null
+++ b/x/mod/modfile/testdata/replace.golden
@@ -0,0 +1,12 @@
+module abc
+
+replace xyz v1.2.3 => /tmp/z
+
+replace xyz v1.3.4 => my/xyz v1.3.4-me
+
+replace (
+	w v1.0.0 => "./a,"
+	w v1.0.1 => "./a()"
+	w v1.0.2 => "./a[]"
+	w v1.0.3 => "./a{}"
+)
diff --git a/x/mod/modfile/testdata/replace.in b/x/mod/modfile/testdata/replace.in
new file mode 100644
index 00000000..f5bef26d
--- /dev/null
+++ b/x/mod/modfile/testdata/replace.in
@@ -0,0 +1,12 @@
+module "abc"
+
+replace "xyz" v1.2.3 => "/tmp/z"
+
+replace "xyz" v1.3.4 => "my/xyz" v1.3.4-me
+
+replace (
+	"w" v1.0.0 => "./a,"
+	"w" v1.0.1 => "./a()"
+	"w" v1.0.2 => "./a[]"
+	"w" v1.0.3 => "./a{}"
+)
diff --git a/x/mod/modfile/testdata/replace2.golden b/x/mod/modfile/testdata/replace2.golden
new file mode 100644
index 00000000..e1d9c728
--- /dev/null
+++ b/x/mod/modfile/testdata/replace2.golden
@@ -0,0 +1,10 @@
+module abc
+
+replace (
+	xyz v1.2.3 => /tmp/z
+	xyz v1.3.4 => my/xyz v1.3.4-me
+	xyz v1.4.5 => "/tmp/my dir"
+	xyz v1.5.6 => my/xyz v1.5.6
+
+	xyz => my/other/xyz v1.5.4
+)
diff --git a/x/mod/modfile/testdata/replace2.in b/x/mod/modfile/testdata/replace2.in
new file mode 100644
index 00000000..78646986
--- /dev/null
+++ b/x/mod/modfile/testdata/replace2.in
@@ -0,0 +1,10 @@
+module "abc"
+
+replace (
+	"xyz" v1.2.3 => "/tmp/z"
+	"xyz" v1.3.4 => "my/xyz" "v1.3.4-me"
+	xyz "v1.4.5" => "/tmp/my dir"
+	xyz v1.5.6 => my/xyz v1.5.6
+
+	xyz => my/other/xyz v1.5.4
+)
diff --git a/x/mod/modfile/testdata/retract.golden b/x/mod/modfile/testdata/retract.golden
new file mode 100644
index 00000000..f5d709e6
--- /dev/null
+++ b/x/mod/modfile/testdata/retract.golden
@@ -0,0 +1,11 @@
+module abc
+
+retract v1.2.3
+
+retract [v1.2.3, v1.2.4]
+
+retract (
+	v1.2.3
+
+	[v1.2.3, v1.2.4]
+)
diff --git a/x/mod/modfile/testdata/retract.in b/x/mod/modfile/testdata/retract.in
new file mode 100644
index 00000000..fa4a1f4f
--- /dev/null
+++ b/x/mod/modfile/testdata/retract.in
@@ -0,0 +1,11 @@
+module abc
+
+retract "v1.2.3"
+
+retract [ "v1.2.3" , "v1.2.4" ]
+
+retract (
+	"v1.2.3"
+
+	[ "v1.2.3" , "v1.2.4" ]
+)
diff --git a/x/mod/modfile/testdata/rule1.golden b/x/mod/modfile/testdata/rule1.golden
new file mode 100644
index 00000000..8a5c7258
--- /dev/null
+++ b/x/mod/modfile/testdata/rule1.golden
@@ -0,0 +1,7 @@
+module "x"
+
+module "y"
+
+require "x"
+
+require x
diff --git a/x/mod/modfile/testdata/work/comment.golden b/x/mod/modfile/testdata/work/comment.golden
new file mode 100644
index 00000000..ac9bbbfd
--- /dev/null
+++ b/x/mod/modfile/testdata/work/comment.golden
@@ -0,0 +1,10 @@
+// comment
+use x // eol
+
+// mid comment
+
+// comment 2
+// comment 2 line 2
+use y // eoy
+
+// comment 3
diff --git a/x/mod/modfile/testdata/work/comment.in b/x/mod/modfile/testdata/work/comment.in
new file mode 100644
index 00000000..df6d702e
--- /dev/null
+++ b/x/mod/modfile/testdata/work/comment.in
@@ -0,0 +1,8 @@
+// comment
+use "x" // eol
+// mid comment
+
+// comment 2
+// comment 2 line 2
+use "y" // eoy
+// comment 3
diff --git a/x/mod/modfile/testdata/work/empty.golden b/x/mod/modfile/testdata/work/empty.golden
new file mode 100644
index 00000000..e69de29b
diff --git a/x/mod/modfile/testdata/work/empty.in b/x/mod/modfile/testdata/work/empty.in
new file mode 100644
index 00000000..e69de29b
diff --git a/x/mod/modfile/testdata/work/replace.golden b/x/mod/modfile/testdata/work/replace.golden
new file mode 100644
index 00000000..534c1dae
--- /dev/null
+++ b/x/mod/modfile/testdata/work/replace.golden
@@ -0,0 +1,12 @@
+use abc
+
+replace xyz v1.2.3 => /tmp/z
+
+replace xyz v1.3.4 => my/xyz v1.3.4-me
+
+replace (
+	w v1.0.0 => "./a,"
+	w v1.0.1 => "./a()"
+	w v1.0.2 => "./a[]"
+	w v1.0.3 => "./a{}"
+)
diff --git a/x/mod/modfile/testdata/work/replace.in b/x/mod/modfile/testdata/work/replace.in
new file mode 100644
index 00000000..dc434d70
--- /dev/null
+++ b/x/mod/modfile/testdata/work/replace.in
@@ -0,0 +1,12 @@
+use "abc"
+
+replace "xyz" v1.2.3 => "/tmp/z"
+
+replace "xyz" v1.3.4 => "my/xyz" v1.3.4-me
+
+replace (
+	"w" v1.0.0 => "./a,"
+	"w" v1.0.1 => "./a()"
+	"w" v1.0.2 => "./a[]"
+	"w" v1.0.3 => "./a{}"
+)
diff --git a/x/mod/modfile/testdata/work/replace2.golden b/x/mod/modfile/testdata/work/replace2.golden
new file mode 100644
index 00000000..ca03dfd0
--- /dev/null
+++ b/x/mod/modfile/testdata/work/replace2.golden
@@ -0,0 +1,10 @@
+use abc
+
+replace (
+	xyz v1.2.3 => /tmp/z
+	xyz v1.3.4 => my/xyz v1.3.4-me
+	xyz v1.4.5 => "/tmp/my dir"
+	xyz v1.5.6 => my/xyz v1.5.6
+
+	xyz => my/other/xyz v1.5.4
+)
diff --git a/x/mod/modfile/testdata/work/replace2.in b/x/mod/modfile/testdata/work/replace2.in
new file mode 100644
index 00000000..cf34eb43
--- /dev/null
+++ b/x/mod/modfile/testdata/work/replace2.in
@@ -0,0 +1,10 @@
+use "abc"
+
+replace (
+	"xyz" v1.2.3 => "/tmp/z"
+	"xyz" v1.3.4 => "my/xyz" "v1.3.4-me"
+	xyz "v1.4.5" => "/tmp/my dir"
+	xyz v1.5.6 => my/xyz v1.5.6
+
+	xyz => my/other/xyz v1.5.4
+)
diff --git a/x/mod/modfile/testdata/work/use.golden b/x/mod/modfile/testdata/work/use.golden
new file mode 100644
index 00000000..032bac18
--- /dev/null
+++ b/x/mod/modfile/testdata/work/use.golden
@@ -0,0 +1,7 @@
+use ../foo
+
+use (
+	/bar
+
+	baz
+)
diff --git a/x/mod/modfile/testdata/work/use.in b/x/mod/modfile/testdata/work/use.in
new file mode 100644
index 00000000..cb790ddf
--- /dev/null
+++ b/x/mod/modfile/testdata/work/use.in
@@ -0,0 +1,7 @@
+use "../foo"
+
+use (
+	"/bar"
+
+	"baz"
+)
diff --git a/x/mod/modfile/work_test.go b/x/mod/modfile/work_test.go
new file mode 100644
index 00000000..bf5f4e6b
--- /dev/null
+++ b/x/mod/modfile/work_test.go
@@ -0,0 +1,413 @@
+/*
+ * Copyright (c) 2021 The GoPlus Authors (goplus.org). All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package modfile
+
+import (
+	"bytes"
+	"io/ioutil"
+	"path/filepath"
+	"strings"
+	"testing"
+)
+
+// TODO(#45713): Update these tests once AddUse sets the module path.
+var workAddUseTests = []struct {
+	desc       string
+	in         string
+	path       string
+	modulePath string
+	out        string
+}{
+	{
+		`empty`,
+		``,
+		`foo`, `bar`,
+		`use foo`,
+	},
+	{
+		`go_stmt_only`,
+		`go 1.17
+		`,
+		`foo`, `bar`,
+		`go 1.17
+		use foo
+		`,
+	},
+	{
+		`use_line_present`,
+		`go 1.17
+		use baz`,
+		`foo`, `bar`,
+		`go 1.17
+		use (
+			baz
+		  foo
+		)
+		`,
+	},
+	{
+		`use_block_present`,
+		`go 1.17
+		use (
+			baz
+			quux
+		)
+		`,
+		`foo`, `bar`,
+		`go 1.17
+		use (
+			baz
+		  quux
+			foo
+		)
+		`,
+	},
+	{
+		`use_and_replace_present`,
+		`go 1.17
+		use baz
+		replace a => ./b
+		`,
+		`foo`, `bar`,
+		`go 1.17
+		use (
+			baz
+			foo
+		)
+		replace a => ./b
+		`,
+	},
+}
+
+var workDropUseTests = []struct {
+	desc string
+	in   string
+	path string
+	out  string
+}{
+	{
+		`empty`,
+		``,
+		`foo`,
+		``,
+	},
+	{
+		`go_stmt_only`,
+		`go 1.17
+		`,
+		`foo`,
+		`go 1.17
+		`,
+	},
+	{
+		`single_use`,
+		`go 1.17
+		use foo`,
+		`foo`,
+		`go 1.17
+		`,
+	},
+	{
+		`use_block`,
+		`go 1.17
+		use (
+			foo
+			bar
+			baz
+		)`,
+		`bar`,
+		`go 1.17
+		use (
+			foo
+			baz
+		)`,
+	},
+	{
+		`use_multi`,
+		`go 1.17
+		use (
+			foo
+			bar
+			baz
+		)
+		use foo
+		use quux
+		use foo`,
+		`foo`,
+		`go 1.17
+		use (
+			bar
+			baz
+		)
+		use quux`,
+	},
+}
+
+var workAddGoTests = []struct {
+	desc    string
+	in      string
+	version string
+	out     string
+}{
+	{
+		`empty`,
+		``,
+		`1.17`,
+		`go 1.17
+		`,
+	},
+	{
+		`comment`,
+		`// this is a comment`,
+		`1.17`,
+		`// this is a comment
+
+		go 1.17`,
+	},
+	{
+		`use_after_replace`,
+		`
+		replace example.com/foo => ../bar
+		use foo
+		`,
+		`1.17`,
+		`
+		go 1.17
+		replace example.com/foo => ../bar
+		use foo
+		`,
+	},
+	{
+		`use_before_replace`,
+		`use foo
+		replace example.com/foo => ../bar
+		`,
+		`1.17`,
+		`
+		go 1.17
+		use foo
+		replace example.com/foo => ../bar
+		`,
+	},
+	{
+		`use_only`,
+		`use foo
+		`,
+		`1.17`,
+		`
+		go 1.17
+		use foo
+		`,
+	},
+	{
+		`already_have_go`,
+		`go 1.17
+		`,
+		`1.18`,
+		`
+		go 1.18
+		`,
+	},
+}
+
+var workSortBlocksTests = []struct {
+	desc, in, out string
+}{
+	{
+		`use_duplicates_not_removed`,
+		`go 1.17
+		use foo
+		use bar
+		use (
+			foo
+		)`,
+		`go 1.17
+		use foo
+		use bar
+		use (
+			foo
+		)`,
+	},
+	{
+		`replace_duplicates_removed`,
+		`go 1.17
+		use foo
+		replace x.y/z v1.0.0 => ./a
+		replace x.y/z v1.1.0 => ./b
+		replace (
+			x.y/z v1.0.0 => ./c
+		)
+		`,
+		`go 1.17
+		use foo
+		replace x.y/z v1.1.0 => ./b
+		replace (
+			x.y/z v1.0.0 => ./c
+		)
+		`,
+	},
+}
+
+func TestAddUse(t *testing.T) {
+	for _, tt := range workAddUseTests {
+		t.Run(tt.desc, func(t *testing.T) {
+			testWorkEdit(t, tt.in, tt.out, func(f *WorkFile) error {
+				return f.AddUse(tt.path, tt.modulePath)
+			})
+		})
+	}
+}
+
+func TestDropUse(t *testing.T) {
+	for _, tt := range workDropUseTests {
+		t.Run(tt.desc, func(t *testing.T) {
+			testWorkEdit(t, tt.in, tt.out, func(f *WorkFile) error {
+				if err := f.DropUse(tt.path); err != nil {
+					return err
+				}
+				f.Cleanup()
+				return nil
+			})
+		})
+	}
+}
+
+func TestWorkAddGo(t *testing.T) {
+	for _, tt := range workAddGoTests {
+		t.Run(tt.desc, func(t *testing.T) {
+			testWorkEdit(t, tt.in, tt.out, func(f *WorkFile) error {
+				return f.AddGoStmt(tt.version)
+			})
+		})
+	}
+}
+
+func TestWorkSortBlocks(t *testing.T) {
+	for _, tt := range workSortBlocksTests {
+		t.Run(tt.desc, func(t *testing.T) {
+			testWorkEdit(t, tt.in, tt.out, func(f *WorkFile) error {
+				f.SortBlocks()
+				return nil
+			})
+		})
+	}
+}
+
+// Test that when files in the testdata directory are parsed
+// and printed and parsed again, we get the same parse tree
+// both times.
+func TestWorkPrintParse(t *testing.T) {
+	outs, err := filepath.Glob("testdata/work/*")
+	if err != nil {
+		t.Fatal(err)
+	}
+	for _, out := range outs {
+		out := out
+		name := filepath.Base(out)
+		t.Run(name, func(t *testing.T) {
+			t.Parallel()
+			data, err := ioutil.ReadFile(out)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			base := "testdata/work/" + filepath.Base(out)
+			f, err := parse(base, data)
+			if err != nil {
+				t.Fatalf("parsing original: %v", err)
+			}
+
+			ndata := Format(f)
+			f2, err := parse(base, ndata)
+			if err != nil {
+				t.Fatalf("parsing reformatted: %v", err)
+			}
+
+			eq := eqchecker{file: base}
+			if err := eq.check(f, f2); err != nil {
+				t.Errorf("not equal (parse/Format/parse): %v", err)
+			}
+
+			pf1, err := ParseWork(base, data, nil)
+			if err != nil {
+				switch base {
+				case "testdata/replace2.in", "testdata/gopkg.in.golden":
+					t.Errorf("should parse %v: %v", base, err)
+				}
+			}
+			if err == nil {
+				pf2, err := ParseWork(base, ndata, nil)
+				if err != nil {
+					t.Fatalf("Parsing reformatted: %v", err)
+				}
+				eq := eqchecker{file: base}
+				if err := eq.check(pf1, pf2); err != nil {
+					t.Errorf("not equal (parse/Format/Parse): %v", err)
+				}
+
+				ndata2 := Format(pf1.Syntax)
+				pf3, err := ParseWork(base, ndata2, nil)
+				if err != nil {
+					t.Fatalf("Parsing reformatted2: %v", err)
+				}
+				eq = eqchecker{file: base}
+				if err := eq.check(pf1, pf3); err != nil {
+					t.Errorf("not equal (Parse/Format/Parse): %v", err)
+				}
+				ndata = ndata2
+			}
+
+			if strings.HasSuffix(out, ".in") {
+				golden, err := ioutil.ReadFile(strings.TrimSuffix(out, ".in") + ".golden")
+				if err != nil {
+					t.Fatal(err)
+				}
+				if !bytes.Equal(ndata, golden) {
+					t.Errorf("formatted %s incorrectly: diff shows -golden, +ours", base)
+					tdiff(t, string(golden), string(ndata))
+					return
+				}
+			}
+		})
+	}
+}
+
+func testWorkEdit(t *testing.T, in, want string, transform func(f *WorkFile) error) *WorkFile {
+	t.Helper()
+	parse := ParseWork
+	f, err := parse("in", []byte(in), nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+	g, err := parse("out", []byte(want), nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+	golden := Format(g.Syntax)
+
+	if err := transform(f); err != nil {
+		t.Fatal(err)
+	}
+	out := Format(f.Syntax)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !bytes.Equal(out, golden) {
+		t.Errorf("have:\n%s\nwant:\n%s", out, golden)
+	}
+
+	return f
+}