Repository: goplus/gop
Pull request #1075 (2021-12-26 18:58:59)

gop/x/mod/modfile


Patch:
diff --git a/cmd/internal/modload/classfile.go b/cmd/internal/modload/classfile.go
index 87ee75c1..5d173e67 100644
--- a/cmd/internal/modload/classfile.go
+++ b/cmd/internal/modload/classfile.go
@@ -35,12 +35,12 @@ func LoadClassFile() {
 	var claassMod module.Version
 
 	for _, require := range modFile.Require {
-		if require.Mod.Path == modFile.Register.ClassfileMod {
+		if require.Mod.Path == modFile.Register[0].ClassfileMod {
 			claassMod = require.Mod
 		}
 	}
 	for _, replace := range modFile.Replace {
-		if replace.Old.Path == modFile.Register.ClassfileMod {
+		if replace.Old.Path == modFile.Register[0].ClassfileMod {
 			claassMod = replace.New
 		}
 	}
diff --git a/cmd/internal/modload/modload.go b/cmd/internal/modload/modload.go
index 62d5ab54..07f07c29 100644
--- a/cmd/internal/modload/modload.go
+++ b/cmd/internal/modload/modload.go
@@ -305,9 +305,6 @@ func addGopStmt() {
 		return
 	}
 	version := env.MainVersion
-	if !modfile.GopVersionRE.MatchString(version) {
-		log.Fatalf("gop: unrecognized default version %q", version)
-	}
 	if err := modFile.AddGopStmt(version); err != nil {
 		log.Fatalf("gop: internal error: %v", err)
 	}
diff --git a/x/mod/modfile/print.go b/x/mod/modfile/print.go
index e38d9463..4cd364d7 100644
--- a/x/mod/modfile/print.go
+++ b/x/mod/modfile/print.go
@@ -16,169 +16,9 @@
 
 package modfile
 
-import (
-	"bytes"
-	"fmt"
-	"strings"
-)
+import "golang.org/x/mod/modfile"
 
 // Format returns a gop.mod file as a byte slice, formatted in standard style.
 func Format(f *FileSyntax) []byte {
-	pr := &printer{}
-	pr.file(f)
-	return pr.Bytes()
-}
-
-// A printer collects the state during printing of a file or expression.
-type printer struct {
-	bytes.Buffer           // output buffer
-	comment      []Comment // pending end-of-line comments
-	margin       int       // left margin (indent), a number of tabs
-}
-
-// printf prints to the buffer.
-func (p *printer) printf(format string, args ...interface{}) {
-	fmt.Fprintf(p, format, args...)
-}
-
-// indent returns the position on the current line, in bytes, 0-indexed.
-func (p *printer) indent() int {
-	b := p.Bytes()
-	n := 0
-	for n < len(b) && b[len(b)-1-n] != '\n' {
-		n++
-	}
-	return n
-}
-
-// newline ends the current line, flushing end-of-line comments.
-func (p *printer) newline() {
-	if len(p.comment) > 0 {
-		p.printf(" ")
-		for i, com := range p.comment {
-			if i > 0 {
-				p.trim()
-				p.printf("\n")
-				for i := 0; i < p.margin; i++ {
-					p.printf("\t")
-				}
-			}
-			p.printf("%s", strings.TrimSpace(com.Token))
-		}
-		p.comment = p.comment[:0]
-	}
-
-	p.trim()
-	p.printf("\n")
-	for i := 0; i < p.margin; i++ {
-		p.printf("\t")
-	}
-}
-
-// trim removes trailing spaces and tabs from the current line.
-func (p *printer) trim() {
-	// Remove trailing spaces and tabs from line we're about to end.
-	b := p.Bytes()
-	n := len(b)
-	for n > 0 && (b[n-1] == '\t' || b[n-1] == ' ') {
-		n--
-	}
-	p.Truncate(n)
-}
-
-// file formats the given file into the print buffer.
-func (p *printer) file(f *FileSyntax) {
-	for _, com := range f.Before {
-		p.printf("%s", strings.TrimSpace(com.Token))
-		p.newline()
-	}
-
-	for i, stmt := range f.Stmt {
-		switch x := stmt.(type) {
-		case *CommentBlock:
-			// comments already handled
-			p.expr(x)
-
-		default:
-			p.expr(x)
-			p.newline()
-		}
-
-		for _, com := range stmt.Comment().After {
-			p.printf("%s", strings.TrimSpace(com.Token))
-			p.newline()
-		}
-
-		if i+1 < len(f.Stmt) {
-			p.newline()
-		}
-	}
-}
-
-func (p *printer) expr(x Expr) {
-	// Emit line-comments preceding this expression.
-	if before := x.Comment().Before; len(before) > 0 {
-		// Want to print a line comment.
-		// Line comments must be at the current margin.
-		p.trim()
-		if p.indent() > 0 {
-			// There's other text on the line. Start a new line.
-			p.printf("\n")
-		}
-		// Re-indent to margin.
-		for i := 0; i < p.margin; i++ {
-			p.printf("\t")
-		}
-		for _, com := range before {
-			p.printf("%s", strings.TrimSpace(com.Token))
-			p.newline()
-		}
-	}
-
-	switch x := x.(type) {
-	default:
-		panic(fmt.Errorf("printer: unexpected type %T", x))
-
-	case *CommentBlock:
-		// done
-
-	case *LParen:
-		p.printf("(")
-	case *RParen:
-		p.printf(")")
-
-	case *Line:
-		p.tokens(x.Token)
-
-	case *LineBlock:
-		p.tokens(x.Token)
-		p.printf(" ")
-		p.expr(&x.LParen)
-		p.margin++
-		for _, l := range x.Line {
-			p.newline()
-			p.expr(l)
-		}
-		p.margin--
-		p.newline()
-		p.expr(&x.RParen)
-	}
-
-	// Queue end-of-line comments for printing when we
-	// reach the end of the line.
-	p.comment = append(p.comment, x.Comment().Suffix...)
-}
-
-func (p *printer) tokens(tokens []string) {
-	sep := ""
-	for _, t := range tokens {
-		if t == "," || t == ")" || t == "]" || t == "}" {
-			sep = ""
-		}
-		p.printf("%s%s", sep, t)
-		sep = " "
-		if t == "(" || t == "[" || t == "{" {
-			sep = ""
-		}
-	}
+	return modfile.Format(f)
 }
diff --git a/x/mod/modfile/read.go b/x/mod/modfile/read.go
index 85fad8a2..78f4b7b4 100644
--- a/x/mod/modfile/read.go
+++ b/x/mod/modfile/read.go
@@ -16,279 +16,31 @@
 package modfile
 
 import (
-	"bytes"
-	"errors"
-	"fmt"
-	"os"
-	"strconv"
-	"strings"
-	"unicode"
-	"unicode/utf8"
+	"golang.org/x/mod/modfile"
 )
 
 // A Position describes an arbitrary source position in a file, including the
 // file, line, column, and byte offset.
-type Position struct {
-	Line     int // line in input (starting at 1)
-	LineRune int // rune in line (starting at 1)
-	Byte     int // byte in input (starting at 0)
-}
-
-// add returns the position at the end of s, assuming it starts at p.
-func (p Position) add(s string) Position {
-	p.Byte += len(s)
-	if n := strings.Count(s, "\n"); n > 0 {
-		p.Line += n
-		s = s[strings.LastIndex(s, "\n")+1:]
-		p.LineRune = 1
-	}
-	p.LineRune += utf8.RuneCountInString(s)
-	return p
-}
+type Position = modfile.Position
 
 // An Expr represents an input element.
-type Expr interface {
-	// Span returns the start and end position of the expression,
-	// excluding leading or trailing comments.
-	Span() (start, end Position)
-
-	// Comment returns the comments attached to the expression.
-	// This method would normally be named 'Comments' but that
-	// would interfere with embedding a type of the same name.
-	Comment() *Comments
-}
+type Expr = modfile.Expr
 
 // A Comment represents a single // comment.
-type Comment struct {
-	Start  Position
-	Token  string // without trailing newline
-	Suffix bool   // an end of line (not whole line) comment
-}
+type Comment = modfile.Comment
 
 // Comments collects the comments associated with an expression.
-type Comments struct {
-	Before []Comment // whole-line comments before this expression
-	Suffix []Comment // end-of-line comments after this expression
-
-	// For top-level expressions only, After lists whole-line
-	// comments following the expression.
-	After []Comment
-}
-
-// Comment returns the receiver. This isn't useful by itself, but
-// a Comments struct is embedded into all the expression
-// implementation types, and this gives each of those a Comment
-// method to satisfy the Expr interface.
-func (c *Comments) Comment() *Comments {
-	return c
-}
+type Comments = modfile.Comments
 
 // A FileSyntax represents an entire gop.mod file.
-type FileSyntax struct {
-	Name string // file path
-	Comments
-	Stmt []Expr
-}
-
-func (x *FileSyntax) Span() (start, end Position) {
-	if len(x.Stmt) == 0 {
-		return
-	}
-	start, _ = x.Stmt[0].Span()
-	_, end = x.Stmt[len(x.Stmt)-1].Span()
-	return start, end
-}
-
-// addLine adds a line containing the given tokens to the file.
-//
-// If the first token of the hint matches the first token of the
-// line, the new line is added at the end of the block containing hint,
-// extracting hint into a new block if it is not yet in one.
-//
-// If the hint is non-nil buts its first token does not match,
-// the new line is added after the block containing hint
-// (or hint itself, if not in a block).
-//
-// If no hint is provided, addLine appends the line to the end of
-// the last block with a matching first token,
-// or to the end of the file if no such block exists.
-func (x *FileSyntax) addLine(hint Expr, tokens ...string) *Line {
-	if hint == nil {
-		// If no hint given, add to the last statement of the given type.
-	Loop:
-		for i := len(x.Stmt) - 1; i >= 0; i-- {
-			stmt := x.Stmt[i]
-			switch stmt := stmt.(type) {
-			case *Line:
-				if stmt.Token != nil && stmt.Token[0] == tokens[0] {
-					hint = stmt
-					break Loop
-				}
-			case *LineBlock:
-				if stmt.Token[0] == tokens[0] {
-					hint = stmt
-					break Loop
-				}
-			}
-		}
-	}
-
-	newLineAfter := func(i int) *Line {
-		new := &Line{Token: tokens}
-		if i == len(x.Stmt) {
-			x.Stmt = append(x.Stmt, new)
-		} else {
-			x.Stmt = append(x.Stmt, nil)
-			copy(x.Stmt[i+2:], x.Stmt[i+1:])
-			x.Stmt[i+1] = new
-		}
-		return new
-	}
-
-	if hint != nil {
-		for i, stmt := range x.Stmt {
-			switch stmt := stmt.(type) {
-			case *Line:
-				if stmt == hint {
-					if stmt.Token == nil || stmt.Token[0] != tokens[0] {
-						return newLineAfter(i)
-					}
-
-					// Convert line to line block.
-					stmt.InBlock = true
-					block := &LineBlock{Token: stmt.Token[:1], Line: []*Line{stmt}}
-					stmt.Token = stmt.Token[1:]
-					x.Stmt[i] = block
-					new := &Line{Token: tokens[1:], InBlock: true}
-					block.Line = append(block.Line, new)
-					return new
-				}
-
-			case *LineBlock:
-				if stmt == hint {
-					if stmt.Token[0] != tokens[0] {
-						return newLineAfter(i)
-					}
-
-					new := &Line{Token: tokens[1:], InBlock: true}
-					stmt.Line = append(stmt.Line, new)
-					return new
-				}
-
-				for j, line := range stmt.Line {
-					if line == hint {
-						if stmt.Token[0] != tokens[0] {
-							return newLineAfter(i)
-						}
-
-						// Add new line after hint within the block.
-						stmt.Line = append(stmt.Line, nil)
-						copy(stmt.Line[j+2:], stmt.Line[j+1:])
-						new := &Line{Token: tokens[1:], InBlock: true}
-						stmt.Line[j+1] = new
-						return new
-					}
-				}
-			}
-		}
-	}
-
-	new := &Line{Token: tokens}
-	x.Stmt = append(x.Stmt, new)
-	return new
-}
-
-func (x *FileSyntax) updateLine(line *Line, tokens ...string) {
-	if line.InBlock {
-		tokens = tokens[1:]
-	}
-	line.Token = tokens
-}
-
-// markRemoved modifies line so that it (and its end-of-line comment, if any)
-// will be dropped by (*FileSyntax).Cleanup.
-func (line *Line) markRemoved() {
-	line.Token = nil
-	line.Comments.Suffix = nil
-}
-
-// Cleanup cleans up the file syntax x after any edit operations.
-// To avoid quadratic behavior, (*Line).markRemoved marks the line as dead
-// by setting line.Token = nil but does not remove it from the slice
-// in which it appears. After edits have all been indicated,
-// calling Cleanup cleans out the dead lines.
-func (x *FileSyntax) Cleanup() {
-	w := 0
-	for _, stmt := range x.Stmt {
-		switch stmt := stmt.(type) {
-		case *Line:
-			if stmt.Token == nil {
-				continue
-			}
-		case *LineBlock:
-			ww := 0
-			for _, line := range stmt.Line {
-				if line.Token != nil {
-					stmt.Line[ww] = line
-					ww++
-				}
-			}
-			if ww == 0 {
-				continue
-			}
-			if ww == 1 {
-				// Collapse block into single line.
-				line := &Line{
-					Comments: Comments{
-						Before: commentsAdd(stmt.Before, stmt.Line[0].Before),
-						Suffix: commentsAdd(stmt.Line[0].Suffix, stmt.Suffix),
-						After:  commentsAdd(stmt.Line[0].After, stmt.After),
-					},
-					Token: stringsAdd(stmt.Token, stmt.Line[0].Token),
-				}
-				x.Stmt[w] = line
-				w++
-				continue
-			}
-			stmt.Line = stmt.Line[:ww]
-		}
-		x.Stmt[w] = stmt
-		w++
-	}
-	x.Stmt = x.Stmt[:w]
-}
-
-func commentsAdd(x, y []Comment) []Comment {
-	return append(x[:len(x):len(x)], y...)
-}
-
-func stringsAdd(x, y []string) []string {
-	return append(x[:len(x):len(x)], y...)
-}
+type FileSyntax = modfile.FileSyntax
 
 // A CommentBlock represents a top-level block of comments separate
 // from any rule.
-type CommentBlock struct {
-	Comments
-	Start Position
-}
-
-func (x *CommentBlock) Span() (start, end Position) {
-	return x.Start, x.Start
-}
+type CommentBlock = modfile.CommentBlock
 
 // A Line is a single line of tokens.
-type Line struct {
-	Comments
-	Start   Position
-	Token   []string
-	InBlock bool
-	End     Position
-}
-
-func (x *Line) Span() (start, end Position) {
-	return x.Start, x.End
-}
+type Line = modfile.Line
 
 // A LineBlock is a factored block of lines, like
 //
@@ -297,674 +49,19 @@ func (x *Line) Span() (start, end Position) {
 //		"y"
 //	)
 //
-type LineBlock struct {
-	Comments
-	Start  Position
-	LParen LParen
-	Token  []string
-	Line   []*Line
-	RParen RParen
-}
-
-func (x *LineBlock) Span() (start, end Position) {
-	return x.Start, x.RParen.Pos.add(")")
-}
+type LineBlock = modfile.LineBlock
 
 // An LParen represents the beginning of a parenthesized line block.
 // It is a place to store suffix comments.
-type LParen struct {
-	Comments
-	Pos Position
-}
-
-func (x *LParen) Span() (start, end Position) {
-	return x.Pos, x.Pos.add(")")
-}
+type LParen = modfile.LParen
 
 // An RParen represents the end of a parenthesized line block.
 // It is a place to store whole-line (before) comments.
-type RParen struct {
-	Comments
-	Pos Position
-}
-
-func (x *RParen) Span() (start, end Position) {
-	return x.Pos, x.Pos.add(")")
-}
-
-// An input represents a single input file being parsed.
-type input struct {
-	// Lexing state.
-	filename   string    // name of input file, for errors
-	complete   []byte    // entire input
-	remaining  []byte    // remaining input
-	tokenStart []byte    // token being scanned to end of input
-	token      token     // next token to be returned by lex, peek
-	pos        Position  // current input position
-	comments   []Comment // accumulated comments
-
-	// Parser state.
-	file        *FileSyntax // returned top-level syntax tree
-	parseErrors ErrorList   // errors encountered during parsing
-
-	// Comment assignment state.
-	pre  []Expr // all expressions, in preorder traversal
-	post []Expr // all expressions, in postorder traversal
-}
-
-func newInput(filename string, data []byte) *input {
-	return &input{
-		filename:  filename,
-		complete:  data,
-		remaining: data,
-		pos:       Position{Line: 1, LineRune: 1, Byte: 0},
-	}
-}
-
-// parse parses the input file.
-func parse(file string, data []byte) (f *FileSyntax, err error) {
-	// The parser panics for both routine errors like syntax errors
-	// and for programmer bugs like array index errors.
-	// Turn both into error returns. Catching bug panics is
-	// especially important when processing many files.
-	in := newInput(file, data)
-	defer func() {
-		if e := recover(); e != nil && e != &in.parseErrors {
-			in.parseErrors = append(in.parseErrors, Error{
-				Filename: in.filename,
-				Pos:      in.pos,
-				Err:      fmt.Errorf("internal error: %v", e),
-			})
-		}
-		if err == nil && len(in.parseErrors) > 0 {
-			err = in.parseErrors
-		}
-	}()
-
-	// Prime the lexer by reading in the first token. It will be available
-	// in the next peek() or lex() call.
-	in.readToken()
-
-	// Invoke the parser.
-	in.parseFile()
-	if len(in.parseErrors) > 0 {
-		return nil, in.parseErrors
-	}
-	in.file.Name = in.filename
-
-	// Assign comments to nearby syntax.
-	in.assignComments()
-
-	return in.file, nil
-}
-
-// Error is called to report an error.
-// Error does not return: it panics.
-func (in *input) Error(s string) {
-	in.parseErrors = append(in.parseErrors, Error{
-		Filename: in.filename,
-		Pos:      in.pos,
-		Err:      errors.New(s),
-	})
-	panic(&in.parseErrors)
-}
-
-// eof reports whether the input has reached end of file.
-func (in *input) eof() bool {
-	return len(in.remaining) == 0
-}
-
-// peekRune returns the next rune in the input without consuming it.
-func (in *input) peekRune() int {
-	if len(in.remaining) == 0 {
-		return 0
-	}
-	r, _ := utf8.DecodeRune(in.remaining)
-	return int(r)
-}
-
-// peekPrefix reports whether the remaining input begins with the given prefix.
-func (in *input) peekPrefix(prefix string) bool {
-	// This is like bytes.HasPrefix(in.remaining, []byte(prefix))
-	// but without the allocation of the []byte copy of prefix.
-	for i := 0; i < len(prefix); i++ {
-		if i >= len(in.remaining) || in.remaining[i] != prefix[i] {
-			return false
-		}
-	}
-	return true
-}
+type RParen = modfile.RParen
 
-// readRune consumes and returns the next rune in the input.
-func (in *input) readRune() int {
-	if len(in.remaining) == 0 {
-		in.Error("internal lexer error: readRune at EOF")
-	}
-	r, size := utf8.DecodeRune(in.remaining)
-	in.remaining = in.remaining[size:]
-	if r == '\n' {
-		in.pos.Line++
-		in.pos.LineRune = 1
-	} else {
-		in.pos.LineRune++
-	}
-	in.pos.Byte += size
-	return int(r)
-}
-
-type token struct {
-	kind   tokenKind
-	pos    Position
-	endPos Position
-	text   string
-}
-
-type tokenKind int
-
-const (
-	_EOF tokenKind = -(iota + 1)
-	_EOLCOMMENT
-	_IDENT
-	_STRING
-	_COMMENT
-
-	// newlines and punctuation tokens are allowed as ASCII codes.
-)
-
-func (k tokenKind) isComment() bool {
-	return k == _COMMENT || k == _EOLCOMMENT
-}
-
-// isEOL returns whether a token terminates a line.
-func (k tokenKind) isEOL() bool {
-	return k == _EOF || k == _EOLCOMMENT || k == '\n'
-}
-
-// startToken marks the beginning of the next input token.
-// It must be followed by a call to endToken, once the token's text has
-// been consumed using readRune.
-func (in *input) startToken() {
-	in.tokenStart = in.remaining
-	in.token.text = ""
-	in.token.pos = in.pos
-}
-
-// endToken marks the end of an input token.
-// It records the actual token string in tok.text.
-// A single trailing newline (LF or CRLF) will be removed from comment tokens.
-func (in *input) endToken(kind tokenKind) {
-	in.token.kind = kind
-	text := string(in.tokenStart[:len(in.tokenStart)-len(in.remaining)])
-	if kind.isComment() {
-		if strings.HasSuffix(text, "\r\n") {
-			text = text[:len(text)-2]
-		} else {
-			text = strings.TrimSuffix(text, "\n")
-		}
-	}
-	in.token.text = text
-	in.token.endPos = in.pos
-}
-
-// peek returns the kind of the the next token returned by lex.
-func (in *input) peek() tokenKind {
-	return in.token.kind
-}
-
-// lex is called from the parser to obtain the next input token.
-func (in *input) lex() token {
-	tok := in.token
-	in.readToken()
-	return tok
-}
-
-// readToken lexes the next token from the text and stores it in in.token.
-func (in *input) readToken() {
-	// Skip past spaces, stopping at non-space or EOF.
-	for !in.eof() {
-		c := in.peekRune()
-		if c == ' ' || c == '\t' || c == '\r' {
-			in.readRune()
-			continue
-		}
-
-		// Comment runs to end of line.
-		if in.peekPrefix("//") {
-			in.startToken()
-
-			// Is this comment the only thing on its line?
-			// Find the last \n before this // and see if it's all
-			// spaces from there to here.
-			i := bytes.LastIndex(in.complete[:in.pos.Byte], []byte("\n"))
-			suffix := len(bytes.TrimSpace(in.complete[i+1:in.pos.Byte])) > 0
-			in.readRune()
-			in.readRune()
-
-			// Consume comment.
-			for len(in.remaining) > 0 && in.readRune() != '\n' {
-			}
-
-			// If we are at top level (not in a statement), hand the comment to
-			// the parser as a _COMMENT token. The grammar is written
-			// to handle top-level comments itself.
-			if !suffix {
-				in.endToken(_COMMENT)
-				return
-			}
-
-			// Otherwise, save comment for later attachment to syntax tree.
-			in.endToken(_EOLCOMMENT)
-			in.comments = append(in.comments, Comment{in.token.pos, in.token.text, suffix})
-			return
-		}
-
-		if in.peekPrefix("/*") {
-			in.Error("mod files must use // comments (not /* */ comments)")
-		}
-
-		// Found non-space non-comment.
-		break
-	}
-
-	// Found the beginning of the next token.
-	in.startToken()
-
-	// End of file.
-	if in.eof() {
-		in.endToken(_EOF)
-		return
-	}
-
-	// Punctuation tokens.
-	switch c := in.peekRune(); c {
-	case '\n', '(', ')', '[', ']', '{', '}', ',':
-		in.readRune()
-		in.endToken(tokenKind(c))
-		return
-
-	case '"', '`': // quoted string
-		quote := c
-		in.readRune()
-		for {
-			if in.eof() {
-				in.pos = in.token.pos
-				in.Error("unexpected EOF in string")
-			}
-			if in.peekRune() == '\n' {
-				in.Error("unexpected newline in string")
-			}
-			c := in.readRune()
-			if c == quote {
-				break
-			}
-			if c == '\\' && quote != '`' {
-				if in.eof() {
-					in.pos = in.token.pos
-					in.Error("unexpected EOF in string")
-				}
-				in.readRune()
-			}
-		}
-		in.endToken(_STRING)
-		return
-	}
-
-	// Checked all punctuation. Must be identifier token.
-	if c := in.peekRune(); !isIdent(c) {
-		in.Error(fmt.Sprintf("unexpected input character %#q", c))
-	}
-
-	// Scan over identifier.
-	for isIdent(in.peekRune()) {
-		if in.peekPrefix("//") {
-			break
-		}
-		if in.peekPrefix("/*") {
-			in.Error("mod files must use // comments (not /* */ comments)")
-		}
-		in.readRune()
-	}
-	in.endToken(_IDENT)
-}
-
-// isIdent reports whether c is an identifier rune.
-// We treat most printable runes as identifier runes, except for a handful of
-// ASCII punctuation characters.
-func isIdent(c int) bool {
-	switch r := rune(c); r {
-	case ' ', '(', ')', '[', ']', '{', '}', ',':
-		return false
-	default:
-		return !unicode.IsSpace(r) && unicode.IsPrint(r)
-	}
-}
-
-// Comment assignment.
-// We build two lists of all subexpressions, preorder and postorder.
-// The preorder list is ordered by start location, with outer expressions first.
-// The postorder list is ordered by end location, with outer expressions last.
-// We use the preorder list to assign each whole-line comment to the syntax
-// immediately following it, and we use the postorder list to assign each
-// end-of-line comment to the syntax immediately preceding it.
-
-// order walks the expression adding it and its subexpressions to the
-// preorder and postorder lists.
-func (in *input) order(x Expr) {
-	if x != nil {
-		in.pre = append(in.pre, x)
-	}
-	switch x := x.(type) {
-	default:
-		panic(fmt.Errorf("order: unexpected type %T", x))
-	case nil:
-		// nothing
-	case *LParen, *RParen:
-		// nothing
-	case *CommentBlock:
-		// nothing
-	case *Line:
-		// nothing
-	case *FileSyntax:
-		for _, stmt := range x.Stmt {
-			in.order(stmt)
-		}
-	case *LineBlock:
-		in.order(&x.LParen)
-		for _, l := range x.Line {
-			in.order(l)
-		}
-		in.order(&x.RParen)
-	}
-	if x != nil {
-		in.post = append(in.post, x)
-	}
-}
-
-// assignComments attaches comments to nearby syntax.
-func (in *input) assignComments() {
-	const debug = false
-
-	// Generate preorder and postorder lists.
-	in.order(in.file)
-
-	// Split into whole-line comments and suffix comments.
-	var line, suffix []Comment
-	for _, com := range in.comments {
-		if com.Suffix {
-			suffix = append(suffix, com)
-		} else {
-			line = append(line, com)
-		}
-	}
-
-	if debug {
-		for _, c := range line {
-			fmt.Fprintf(os.Stderr, "LINE %q :%d:%d #%d\n", c.Token, c.Start.Line, c.Start.LineRune, c.Start.Byte)
-		}
-	}
-
-	// Assign line comments to syntax immediately following.
-	for _, x := range in.pre {
-		start, _ := x.Span()
-		if debug {
-			fmt.Fprintf(os.Stderr, "pre %T :%d:%d #%d\n", x, start.Line, start.LineRune, start.Byte)
-		}
-		xcom := x.Comment()
-		for len(line) > 0 && start.Byte >= line[0].Start.Byte {
-			if debug {
-				fmt.Fprintf(os.Stderr, "ASSIGN LINE %q #%d\n", line[0].Token, line[0].Start.Byte)
-			}
-			xcom.Before = append(xcom.Before, line[0])
-			line = line[1:]
-		}
-	}
-
-	// Remaining line comments go at end of file.
-	in.file.After = append(in.file.After, line...)
-
-	if debug {
-		for _, c := range suffix {
-			fmt.Fprintf(os.Stderr, "SUFFIX %q :%d:%d #%d\n", c.Token, c.Start.Line, c.Start.LineRune, c.Start.Byte)
-		}
-	}
-
-	// Assign suffix comments to syntax immediately before.
-	for i := len(in.post) - 1; i >= 0; i-- {
-		x := in.post[i]
-
-		start, end := x.Span()
-		if debug {
-			fmt.Fprintf(os.Stderr, "post %T :%d:%d #%d :%d:%d #%d\n", x, start.Line, start.LineRune, start.Byte, end.Line, end.LineRune, end.Byte)
-		}
-
-		// Do not assign suffix comments to end of line block or whole file.
-		// Instead assign them to the last element inside.
-		switch x.(type) {
-		case *FileSyntax:
-			continue
-		}
-
-		// Do not assign suffix comments to something that starts
-		// on an earlier line, so that in
-		//
-		//	x ( y
-		//		z ) // comment
-		//
-		// we assign the comment to z and not to x ( ... ).
-		if start.Line != end.Line {
-			continue
-		}
-		xcom := x.Comment()
-		for len(suffix) > 0 && end.Byte <= suffix[len(suffix)-1].Start.Byte {
-			if debug {
-				fmt.Fprintf(os.Stderr, "ASSIGN SUFFIX %q #%d\n", suffix[len(suffix)-1].Token, suffix[len(suffix)-1].Start.Byte)
-			}
-			xcom.Suffix = append(xcom.Suffix, suffix[len(suffix)-1])
-			suffix = suffix[:len(suffix)-1]
-		}
-	}
-
-	// We assigned suffix comments in reverse.
-	// If multiple suffix comments were appended to the same
-	// expression node, they are now in reverse. Fix that.
-	for _, x := range in.post {
-		reverseComments(x.Comment().Suffix)
-	}
-
-	// Remaining suffix comments go at beginning of file.
-	in.file.Before = append(in.file.Before, suffix...)
-}
-
-// reverseComments reverses the []Comment list.
-func reverseComments(list []Comment) {
-	for i, j := 0, len(list)-1; i < j; i, j = i+1, j-1 {
-		list[i], list[j] = list[j], list[i]
-	}
-}
-
-func (in *input) parseFile() {
-	in.file = new(FileSyntax)
-	var cb *CommentBlock
-	for {
-		switch in.peek() {
-		case '\n':
-			in.lex()
-			if cb != nil {
-				in.file.Stmt = append(in.file.Stmt, cb)
-				cb = nil
-			}
-		case _COMMENT:
-			tok := in.lex()
-			if cb == nil {
-				cb = &CommentBlock{Start: tok.pos}
-			}
-			com := cb.Comment()
-			com.Before = append(com.Before, Comment{Start: tok.pos, Token: tok.text})
-		case _EOF:
-			if cb != nil {
-				in.file.Stmt = append(in.file.Stmt, cb)
-			}
-			return
-		default:
-			in.parseStmt()
-			if cb != nil {
-				in.file.Stmt[len(in.file.Stmt)-1].Comment().Before = cb.Before
-				cb = nil
-			}
-		}
-	}
-}
-
-func (in *input) parseStmt() {
-	tok := in.lex()
-	start := tok.pos
-	end := tok.endPos
-	tokens := []string{tok.text}
-	for {
-		tok := in.lex()
-		switch {
-		case tok.kind.isEOL():
-			in.file.Stmt = append(in.file.Stmt, &Line{
-				Start: start,
-				Token: tokens,
-				End:   end,
-			})
-			return
-
-		case tok.kind == '(':
-			if next := in.peek(); next.isEOL() {
-				// Start of block: no more tokens on this line.
-				in.file.Stmt = append(in.file.Stmt, in.parseLineBlock(start, tokens, tok))
-				return
-			} else if next == ')' {
-				rparen := in.lex()
-				if in.peek().isEOL() {
-					// Empty block.
-					in.lex()
-					in.file.Stmt = append(in.file.Stmt, &LineBlock{
-						Start:  start,
-						Token:  tokens,
-						LParen: LParen{Pos: tok.pos},
-						RParen: RParen{Pos: rparen.pos},
-					})
-					return
-				}
-				// '( )' in the middle of the line, not a block.
-				tokens = append(tokens, tok.text, rparen.text)
-			} else {
-				// '(' in the middle of the line, not a block.
-				tokens = append(tokens, tok.text)
-			}
-
-		default:
-			tokens = append(tokens, tok.text)
-			end = tok.endPos
-		}
-	}
-}
-
-func (in *input) parseLineBlock(start Position, token []string, lparen token) *LineBlock {
-	x := &LineBlock{
-		Start:  start,
-		Token:  token,
-		LParen: LParen{Pos: lparen.pos},
-	}
-	var comments []Comment
-	for {
-		switch in.peek() {
-		case _EOLCOMMENT:
-			// Suffix comment, will be attached later by assignComments.
-			in.lex()
-		case '\n':
-			// Blank line. Add an empty comment to preserve it.
-			in.lex()
-			if len(comments) == 0 && len(x.Line) > 0 || len(comments) > 0 && comments[len(comments)-1].Token != "" {
-				comments = append(comments, Comment{})
-			}
-		case _COMMENT:
-			tok := in.lex()
-			comments = append(comments, Comment{Start: tok.pos, Token: tok.text})
-		case _EOF:
-			in.Error(fmt.Sprintf("syntax error (unterminated block started at %s:%d:%d)", in.filename, x.Start.Line, x.Start.LineRune))
-		case ')':
-			rparen := in.lex()
-			x.RParen.Before = comments
-			x.RParen.Pos = rparen.pos
-			if !in.peek().isEOL() {
-				in.Error("syntax error (expected newline after closing paren)")
-			}
-			in.lex()
-			return x
-		default:
-			l := in.parseLine()
-			x.Line = append(x.Line, l)
-			l.Comment().Before = comments
-			comments = nil
-		}
-	}
-}
-
-func (in *input) parseLine() *Line {
-	tok := in.lex()
-	if tok.kind.isEOL() {
-		in.Error("internal parse error: parseLine at end of line")
-	}
-	start := tok.pos
-	end := tok.endPos
-	tokens := []string{tok.text}
-	for {
-		tok := in.lex()
-		if tok.kind.isEOL() {
-			return &Line{
-				Start:   start,
-				Token:   tokens,
-				End:     end,
-				InBlock: true,
-			}
-		}
-		tokens = append(tokens, tok.text)
-		end = tok.endPos
-	}
-}
-
-var (
-	slashSlash = []byte("//")
-	moduleStr  = []byte("module")
-)
-
-// ModulePath returns the module path from the gomod file text.
+// ModulePath returns the module path from the gopmod file text.
 // If it cannot find a module path, it returns an empty string.
 // It is tolerant of unrelated problems in the gop.mod file.
 func ModulePath(mod []byte) string {
-	for len(mod) > 0 {
-		line := mod
-		mod = nil
-		if i := bytes.IndexByte(line, '\n'); i >= 0 {
-			line, mod = line[:i], line[i+1:]
-		}
-		if i := bytes.Index(line, slashSlash); i >= 0 {
-			line = line[:i]
-		}
-		line = bytes.TrimSpace(line)
-		if !bytes.HasPrefix(line, moduleStr) {
-			continue
-		}
-		line = line[len(moduleStr):]
-		n := len(line)
-		line = bytes.TrimSpace(line)
-		if len(line) == n || len(line) == 0 {
-			continue
-		}
-
-		if line[0] == '"' || line[0] == '`' {
-			p, err := strconv.Unquote(string(line))
-			if err != nil {
-				return "" // malformed quoted string or multiline module path
-			}
-			return p
-		}
-
-		return string(line)
-	}
-	return "" // missing module path
+	return modfile.ModulePath(mod)
 }
diff --git a/x/mod/modfile/rule.go b/x/mod/modfile/rule.go
index 9ba91212..bea02c19 100644
--- a/x/mod/modfile/rule.go
+++ b/x/mod/modfile/rule.go
@@ -19,219 +19,63 @@ package modfile
 import (
 	"errors"
 	"fmt"
-	"path/filepath"
-	"sort"
 	"strconv"
 	"strings"
-	"unicode"
 
+	"golang.org/x/mod/modfile"
 	"golang.org/x/mod/module"
-	"golang.org/x/mod/semver"
-
-	"github.com/goplus/gop/x/mod/internal/lazyregexp"
 )
 
 // A File is the parsed, interpreted form of a gop.mod file.
 type File struct {
-	Module    *Module
-	Go        *Go
+	modfile.File
 	Gop       *Gop
 	Classfile *Classfile
-	Register  *Register
-	Require   []*Require
-	Exclude   []*Exclude
-	Replace   []*Replace
-	Retract   []*Retract
-
-	Syntax *FileSyntax
+	Register  []*Register
 }
 
 // A Module is the module statement.
-type Module struct {
-	Mod        module.Version
-	Deprecated string
-	Syntax     *Line
-}
+type Module = modfile.Module
 
 // A Go is the go statement.
-type Go struct {
-	Version string // "1.23"
-	Syntax  *Line
-}
+type Go = modfile.Go
+
+// A Require is a single require statement.
+type Require = modfile.Require
+
+// An Exclude is a single exclude statement.
+type Exclude = modfile.Exclude
+
+// A Replace is a single replace statement.
+type Replace = modfile.Replace
+
+// A Retract is a single retract statement.
+type Retract = modfile.Retract
 
 // A Gop is the gop statement.
-type Gop struct {
-	Version string // "1.23"
-	Syntax  *Line
-}
+type Gop = modfile.Go
 
 // A Classfile is the classfile statement.
 type Classfile struct {
 	ProjExt  string   // ".gmx"
 	WorkExt  string   // ".spx"
-	PkgPaths []string // paths of classfile
+	PkgPaths []string // package paths of classfile
 	Syntax   *Line
 }
 
 // A Register is the register statement.
 type Register struct {
-	ClassfileMod string // modname of classfile
+	ClassfileMod string // module path of classfile
 	Syntax       *Line
 }
 
-// A Require is a single require statement.
-type Require struct {
-	Mod      module.Version
-	Indirect bool // has "// indirect" comment
-	Syntax   *Line
-}
-
-// An Exclude is a single exclude statement.
-type Exclude struct {
-	Mod    module.Version
-	Syntax *Line
-}
-
-// A Replace is a single replace statement.
-type Replace struct {
-	Old    module.Version
-	New    module.Version
-	Syntax *Line
-}
-
-// A Retract is a single retract statement.
-type Retract struct {
-	VersionInterval
-	Rationale string
-	Syntax    *Line
-}
-
 // A VersionInterval represents a range of versions with upper and lower bounds.
 // Intervals are closed: both bounds are included. When Low is equal to High,
 // the interval may refer to a single version ('v1.2.3') or an interval
 // ('[v1.2.3, v1.2.3]'); both have the same representation.
-type VersionInterval struct {
-	Low, High string
-}
-
-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)
-	}
-	if f.Module == nil {
-		f.Module = &Module{
-			Mod:    module.Version{Path: path},
-			Syntax: f.Syntax.addLine(nil, "module", AutoQuote(path)),
-		}
-	} else {
-		f.Module.Mod.Path = path
-		f.Syntax.updateLine(f.Module.Syntax, "module", AutoQuote(path))
-	}
-	return nil
-}
-
-func (f *File) AddComment(text string) {
-	if f.Syntax == nil {
-		f.Syntax = new(FileSyntax)
-	}
-	f.Syntax.Stmt = append(f.Syntax.Stmt, &CommentBlock{
-		Comments: Comments{
-			Before: []Comment{
-				{
-					Token: text,
-				},
-			},
-		},
-	})
-}
+type VersionInterval = modfile.VersionInterval
 
-type VersionFixer func(path, version string) (string, error)
-
-// errDontFix is returned by a VersionFixer to indicate the version should be
-// left alone, even if it's not canonical.
-var dontFixRetract VersionFixer = func(_, vers string) (string, error) {
-	return vers, nil
-}
+type VersionFixer = modfile.VersionFixer
 
 // Parse parses and returns a gop.mod file.
 //
@@ -258,94 +102,32 @@ func ParseLax(file string, data []byte, fix VersionFixer) (*File, error) {
 }
 
 func parseToFile(file string, data []byte, fix VersionFixer, strict bool) (parsed *File, err error) {
-	fs, err := parse(file, data)
+	f, err := modfile.ParseLax(file, data, fix)
 	if err != nil {
-		return nil, err
-	}
-	f := &File{
-		Syntax: fs,
+		return
 	}
-	var errs ErrorList
-
-	// fix versions in retract directives after the file is parsed.
-	// We need the module path to fix versions, and it might be at the end.
-	defer func() {
-		oldLen := len(errs)
-		f.fixRetract(fix, &errs)
-		if len(errs) > oldLen {
-			parsed, err = nil, errs
-		}
-	}()
+	parsed = &File{File: *f}
 
+	var errs ErrorList
+	var fs = f.Syntax
 	for _, x := range fs.Stmt {
 		switch x := x.(type) {
 		case *Line:
-			f.add(&errs, nil, x, x.Token[0], x.Token[1:], fix, strict)
-
+			parsed.parseVerb(&errs, x.Token[0], x, x.Token[1:], strict)
 		case *LineBlock:
-			if len(x.Token) > 1 {
-				if strict {
-					errs = append(errs, Error{
-						Filename: file,
-						Pos:      x.Start,
-						Err:      fmt.Errorf("unknown block type: %s", strings.Join(x.Token, " ")),
-					})
-				}
-				continue
-			}
-			switch x.Token[0] {
-			default:
-				if strict {
-					errs = append(errs, Error{
-						Filename: file,
-						Pos:      x.Start,
-						Err:      fmt.Errorf("unknown block type: %s", strings.Join(x.Token, " ")),
-					})
-				}
-				continue
-			case "module", "require", "exclude", "replace", "retract":
-				for _, l := range x.Line {
-					f.add(&errs, x, l, x.Token[0], l.Token, fix, strict)
-				}
+			verb := x.Token[0]
+			for _, line := range x.Line {
+				parsed.parseVerb(&errs, verb, line, line.Token, strict)
 			}
 		}
 	}
-
 	if len(errs) > 0 {
 		return nil, errs
 	}
-	return f, nil
+	return
 }
 
-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.
-	// We ignore all unknown directives as well as main-module-only
-	// directives like replace and exclude. It will work better for
-	// forward compatibility if we can depend on modules that have unknown
-	// statements (presumed relevant only when acting as the main module)
-	// and simply ignore those statements.
-	if !strict {
-		switch verb {
-		case "go", "gop", "classfile", "register", "module", "retract", "require":
-			// want these even for dependency gop.mods
-		default:
-			return
-		}
-	}
-
-	wrapModPathError := func(modPath string, err error) {
-		*errs = append(*errs, Error{
-			Filename: f.Syntax.Name,
-			Pos:      line.Start,
-			ModPath:  modPath,
-			Verb:     verb,
-			Err:      err,
-		})
-	}
+func (f *File) parseVerb(errs *ErrorList, verb string, line *Line, args []string, strict bool) {
 	wrapError := func(err error) {
 		*errs = append(*errs, Error{
 			Filename: f.Syntax.Name,
@@ -356,84 +138,28 @@ func (f *File) add(errs *ErrorList, block *LineBlock, line *Line, verb string, a
 	errorf := func(format string, args ...interface{}) {
 		wrapError(fmt.Errorf(format, args...))
 	}
-
 	switch verb {
-	default:
-		errorf("unknown directive: %s", verb)
-
-	case "go":
-		if f.Go != nil {
-			errorf("repeated go statement")
-			return
-		}
-		if len(args) != 1 {
-			errorf("go directive expects exactly one argument")
-			return
-		} else if !GoVersionRE.MatchString(args[0]) {
-			fixed := false
-			if !strict {
-				if m := laxGoVersionRE.FindStringSubmatch(args[0]); m != nil {
-					args[0] = m[1]
-					fixed = true
-				}
-			}
-			if !fixed {
-				errorf("invalid go version '%s': must match format 1.23", args[0])
-				return
-			}
-		}
-
-		f.Go = &Go{Syntax: line}
-		f.Go.Version = args[0]
-
+	case "require", "replace", "exclude", "module", "go", "retract":
 	case "gop":
 		if f.Gop != nil {
-			errorf("repeated gop statement")
+			errorf("repeated go statement")
 			return
 		}
 		if len(args) != 1 {
-			errorf("gop directive expects exactly one argument")
+			errorf("go directive expects exactly one argument")
 			return
-		} else if !GopVersionRE.MatchString(args[0]) {
+		} else if !modfile.GoVersionRE.MatchString(args[0]) {
 			errorf("invalid gop version '%s': must match format 1.23", args[0])
 			return
 		}
-
 		f.Gop = &Gop{Syntax: line}
 		f.Gop.Version = args[0]
-
-	case "classfile":
-		if len(args) <= 2 {
-			errorf("usage: %s [classfileExt...] pkgPathOfClassFile", verb)
-			return
-		}
-		f.Classfile = &Classfile{Syntax: line}
-
-		projExt, err := parseExt(&args[0])
-		if err != nil {
-			errorf("invalid extension: %v", err)
-			return
-		}
-
-		workExt, err := parseExt(&args[1])
-		if err != nil {
-			errorf("invalid extension: %v", err)
+	case "register":
+		if len(args) != 1 {
+			errorf("register directive expects exactly one argument")
 			return
 		}
-		f.Classfile.ProjExt = projExt
-		f.Classfile.WorkExt = workExt
-
-		for i := 2; i < len(args); i++ {
-			s, err := parseString(&args[i])
-			if err != nil {
-				errorf("invalid quoted string: %v", err)
-				return
-			}
-			f.Classfile.PkgPaths = append(f.Classfile.PkgPaths, s)
-		}
-
-	case "register":
-		s, err := parseString(&args[len(args)-1])
+		s, err := parseString(&args[0])
 		if err != nil {
 			errorf("invalid quoted string: %v", err)
 			return
@@ -443,221 +169,41 @@ func (f *File) add(errs *ErrorList, block *LineBlock, line *Line, verb string, a
 			wrapError(err)
 			return
 		}
-		f.Register = &Register{
+		f.Register = append(f.Register, &Register{
 			ClassfileMod: s,
 			Syntax:       line,
-		}
-
-	case "module":
-		if f.Module != nil {
-			errorf("repeated module statement")
-			return
-		}
-		deprecated := parseDeprecation(block, line)
-		f.Module = &Module{
-			Syntax:     line,
-			Deprecated: deprecated,
-		}
-		if len(args) != 1 {
-			errorf("usage: module module/path")
-			return
-		}
-		s, err := parseString(&args[0])
-		if err != nil {
-			errorf("invalid quoted string: %v", err)
-			return
-		}
-		f.Module.Mod = module.Version{Path: s}
-
-	case "require", "exclude":
-		if len(args) != 2 {
-			errorf("usage: %s module/path v1.2.3", verb)
+		})
+	case "classfile":
+		if f.Classfile != nil {
+			errorf("repeated classfile statement")
 			return
 		}
-		s, err := parseString(&args[0])
-		if err != nil {
-			errorf("invalid quoted string: %v", err)
+		if len(args) < 3 {
+			errorf("usage: classfile projExt workExt [classFilePkgPath ...]")
 			return
 		}
-		v, err := parseVersion(verb, s, &args[1], fix)
+		projExt, err := parseExt(&args[0])
 		if err != nil {
 			wrapError(err)
 			return
 		}
-		pathMajor, err := modulePathMajor(s)
+		workExt, err := parseExt(&args[1])
 		if err != nil {
 			wrapError(err)
 			return
 		}
-		if err := module.CheckPathMajor(v, pathMajor); err != nil {
-			wrapModPathError(s, err)
-			return
-		}
-		if verb == "require" {
-			f.Require = append(f.Require, &Require{
-				Mod:      module.Version{Path: s, Version: v},
-				Syntax:   line,
-				Indirect: isIndirect(line),
-			})
-		} else {
-			f.Exclude = append(f.Exclude, &Exclude{
-				Mod:    module.Version{Path: s, Version: v},
-				Syntax: line,
-			})
-		}
-
-	case "replace":
-		replace, wrappederr := parseReplace(f.Syntax.Name, line, verb, args, fix)
-		if wrappederr != nil {
-			*errs = append(*errs, *wrappederr)
-			return
-		}
-		f.Replace = append(f.Replace, replace)
-
-	case "retract":
-		rationale := parseDirectiveComment(block, line)
-		vi, err := parseVersionInterval(verb, "", &args, dontFixRetract)
+		pkgPaths, err := parseStrings(args[2:])
 		if err != nil {
-			if strict {
-				wrapError(err)
-				return
-			} else {
-				// Only report errors parsing intervals in the main module. We may
-				// support additional syntax in the future, such as open and half-open
-				// intervals. Those can't be supported now, because they break the
-				// gop.mod parser, even in lax mode.
-				return
-			}
-		}
-		if len(args) > 0 && strict {
-			// In the future, there may be additional information after the version.
-			errorf("unexpected token after version: %q", args[0])
+			errorf("invalid quoted string: %v", err)
 			return
 		}
-		retract := &Retract{
-			VersionInterval: vi,
-			Rationale:       rationale,
-			Syntax:          line,
-		}
-		f.Retract = append(f.Retract, retract)
-	}
-}
-
-func parseReplace(filename string, line *Line, verb string, args []string, fix VersionFixer) (*Replace, *Error) {
-	wrapModPathError := func(modPath string, err error) *Error {
-		return &Error{
-			Filename: filename,
-			Pos:      line.Start,
-			ModPath:  modPath,
-			Verb:     verb,
-			Err:      err,
-		}
-	}
-	wrapError := func(err error) *Error {
-		return &Error{
-			Filename: filename,
-			Pos:      line.Start,
-			Err:      err,
-		}
-	}
-	errorf := func(format string, args ...interface{}) *Error {
-		return wrapError(fmt.Errorf(format, args...))
-	}
-
-	arrow := 2
-	if len(args) >= 2 && args[1] == "=>" {
-		arrow = 1
-	}
-	if len(args) < arrow+2 || len(args) > arrow+3 || args[arrow] != "=>" {
-		return nil, errorf("usage: %s module/path [v1.2.3] => other/module v1.4\n\t or %s module/path [v1.2.3] => ../local/directory", verb, verb)
-	}
-	s, err := parseString(&args[0])
-	if err != nil {
-		return nil, errorf("invalid quoted string: %v", err)
-	}
-	pathMajor, err := modulePathMajor(s)
-	if err != nil {
-		return nil, wrapModPathError(s, err)
-
-	}
-	var v string
-	if arrow == 2 {
-		v, err = parseVersion(verb, s, &args[1], fix)
-		if err != nil {
-			return nil, wrapError(err)
-		}
-		if err := module.CheckPathMajor(v, pathMajor); err != nil {
-			return nil, wrapModPathError(s, err)
-		}
-	}
-	ns, err := parseString(&args[arrow+1])
-	if err != nil {
-		return nil, errorf("invalid quoted string: %v", err)
-	}
-	nv := ""
-	if len(args) == arrow+2 {
-		if !IsDirectoryPath(ns) {
-			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)
-
+		f.Classfile = &Classfile{
+			ProjExt: projExt, WorkExt: workExt, PkgPaths: pkgPaths, Syntax: line,
 		}
-	}
-	return &Replace{
-		Old:    module.Version{Path: s, Version: v},
-		New:    module.Version{Path: ns, Version: nv},
-		Syntax: line,
-	}, nil
-}
-
-// fixRetract applies fix to each retract directive in f, appending any errors
-// to errs.
-//
-// Most versions are fixed as we parse the file, but for retract directives,
-// the relevant module path is the one specified with the module directive,
-// and that might appear at the end of the file (or not at all).
-func (f *File) fixRetract(fix VersionFixer, errs *ErrorList) {
-	if fix == nil {
-		return
-	}
-	path := ""
-	if f.Module != nil {
-		path = f.Module.Mod.Path
-	}
-	var r *Retract
-	wrapError := func(err error) {
-		*errs = append(*errs, Error{
-			Filename: f.Syntax.Name,
-			Pos:      r.Syntax.Start,
-			Err:      err,
-		})
-	}
-
-	for _, r = range f.Retract {
-		if path == "" {
-			wrapError(errors.New("no module directive found, so retract cannot be used"))
-			return // only print the first one of these
-		}
-
-		args := r.Syntax.Token
-		if args[0] == "retract" {
-			args = args[1:]
-		}
-		vi, err := parseVersionInterval("retract", path, &args, fix)
-		if err != nil {
-			wrapError(err)
+	default:
+		if strict {
+			errorf("unknown directive: %s", verb)
 		}
-		r.VersionInterval = vi
 	}
 }
 
@@ -665,89 +211,19 @@ func (f *File) fixRetract(fix VersionFixer, errs *ErrorList) {
 // as a directory path. Just like on the go command line, relative paths
 // and rooted paths are directory paths; the rest are module paths.
 func IsDirectoryPath(ns string) bool {
-	// Because gop.mod files can move from one system to another,
-	// we check all known path syntaxes, both Unix and Windows.
-	return strings.HasPrefix(ns, "./") || strings.HasPrefix(ns, "../") || strings.HasPrefix(ns, "/") ||
-		strings.HasPrefix(ns, `.\`) || strings.HasPrefix(ns, `..\`) || strings.HasPrefix(ns, `\`) ||
-		len(ns) >= 2 && ('A' <= ns[0] && ns[0] <= 'Z' || 'a' <= ns[0] && ns[0] <= 'z') && ns[1] == ':'
+	return modfile.IsDirectoryPath(ns)
 }
 
 // MustQuote reports whether s must be quoted in order to appear as
 // a single token in a gop.mod line.
 func MustQuote(s string) bool {
-	for _, r := range s {
-		switch r {
-		case ' ', '"', '\'', '`':
-			return true
-
-		case '(', ')', '[', ']', '{', '}', ',':
-			if len(s) > 1 {
-				return true
-			}
-
-		default:
-			if !unicode.IsPrint(r) {
-				return true
-			}
-		}
-	}
-	return s == "" || strings.Contains(s, "//") || strings.Contains(s, "/*")
+	return modfile.MustQuote(s)
 }
 
 // AutoQuote returns s or, if quoting is required for s to appear in a gop.mod,
 // the quotation of s.
 func AutoQuote(s string) string {
-	if MustQuote(s) {
-		return strconv.Quote(s)
-	}
-	return s
-}
-
-func parseVersionInterval(verb string, path string, args *[]string, fix VersionFixer) (VersionInterval, error) {
-	toks := *args
-	if len(toks) == 0 || toks[0] == "(" {
-		return VersionInterval{}, fmt.Errorf("expected '[' or version")
-	}
-	if toks[0] != "[" {
-		v, err := parseVersion(verb, path, &toks[0], fix)
-		if err != nil {
-			return VersionInterval{}, err
-		}
-		*args = toks[1:]
-		return VersionInterval{Low: v, High: v}, nil
-	}
-	toks = toks[1:]
-
-	if len(toks) == 0 {
-		return VersionInterval{}, fmt.Errorf("expected version after '['")
-	}
-	low, err := parseVersion(verb, path, &toks[0], fix)
-	if err != nil {
-		return VersionInterval{}, err
-	}
-	toks = toks[1:]
-
-	if len(toks) == 0 || toks[0] != "," {
-		return VersionInterval{}, fmt.Errorf("expected ',' after version")
-	}
-	toks = toks[1:]
-
-	if len(toks) == 0 {
-		return VersionInterval{}, fmt.Errorf("expected version after ','")
-	}
-	high, err := parseVersion(verb, path, &toks[0], fix)
-	if err != nil {
-		return VersionInterval{}, err
-	}
-	toks = toks[1:]
-
-	if len(toks) == 0 || toks[0] != "]" {
-		return VersionInterval{}, fmt.Errorf("expected ']' after version")
-	}
-	toks = toks[1:]
-
-	*args = toks
-	return VersionInterval{Low: low, High: high}, nil
+	return modfile.AutoQuote(s)
 }
 
 func parseString(s *string) (string, error) {
@@ -767,875 +243,124 @@ func parseString(s *string) (string, error) {
 	return t, nil
 }
 
-var deprecatedRE = lazyregexp.New(`(?s)(?:^|\n\n)Deprecated: *(.*?)(?:$|\n\n)`)
-
-// parseDeprecation extracts the text of comments on a "module" directive and
-// extracts a deprecation message from that.
-//
-// A deprecation message is contained in a paragraph within a block of comments
-// that starts with "Deprecated:" (case sensitive). The message runs until the
-// end of the paragraph and does not include the "Deprecated:" prefix. If the
-// comment block has multiple paragraphs that start with "Deprecated:",
-// parseDeprecation returns the message from the first.
-func parseDeprecation(block *LineBlock, line *Line) string {
-	text := parseDirectiveComment(block, line)
-	m := deprecatedRE.FindStringSubmatch(text)
-	if m == nil {
-		return ""
+func parseStrings(args []string) (arr []string, err error) {
+	arr = make([]string, len(args))
+	for i := range args {
+		if arr[i], err = parseString(&args[i]); err != nil {
+			return
+		}
 	}
-	return m[1]
+	return
 }
 
-// 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()
+func parseExt(s *string) (t string, err error) {
+	t, err = parseString(s)
+	if err != nil {
+		goto failed
 	}
-	groups := [][]Comment{comments.Before, comments.Suffix}
-	var lines []string
-	for _, g := range groups {
-		for _, c := range g {
-			if !strings.HasPrefix(c.Token, "//") {
-				continue // blank line
-			}
-			lines = append(lines, strings.TrimSpace(strings.TrimPrefix(c.Token, "//")))
-		}
+	if len(t) > 1 && t[0] == '.' || t == "" {
+		return
 	}
-	return strings.Join(lines, "\n")
-}
-
-type ErrorList []Error
-
-func (e ErrorList) Error() string {
-	errStrs := make([]string, len(e))
-	for i, err := range e {
-		errStrs[i] = err.Error()
+	err = errors.New("invalid ext format")
+failed:
+	return "", &InvalidExtError{
+		Ext: *s,
+		Err: err,
 	}
-	return strings.Join(errStrs, "\n")
 }
 
-type Error struct {
-	Filename string
-	Pos      Position
-	Verb     string
-	ModPath  string
-	Err      error
+type InvalidExtError struct {
+	Ext string
+	Err error
 }
 
-func (e *Error) Error() string {
-	var pos string
-	if e.Pos.LineRune > 1 {
-		// Don't print LineRune if it's 1 (beginning of line).
-		// It's always 1 except in scanner errors, which are rare.
-		pos = fmt.Sprintf("%s:%d:%d: ", e.Filename, e.Pos.Line, e.Pos.LineRune)
-	} else if e.Pos.Line > 0 {
-		pos = fmt.Sprintf("%s:%d: ", e.Filename, e.Pos.Line)
-	} else if e.Filename != "" {
-		pos = fmt.Sprintf("%s: ", e.Filename)
-	}
-
-	var directive string
-	if e.ModPath != "" {
-		directive = fmt.Sprintf("%s %s: ", e.Verb, e.ModPath)
-	} else if e.Verb != "" {
-		directive = fmt.Sprintf("%s: ", e.Verb)
-	}
-
-	return pos + directive + e.Err.Error()
+func (e *InvalidExtError) Error() string {
+	return fmt.Sprintf("ext %s invalid: %s", e.Ext, e.Err)
 }
 
-func (e *Error) Unwrap() error { return e.Err }
+func (e *InvalidExtError) Unwrap() error { return e.Err }
 
-func parseVersion(verb string, path string, s *string, fix VersionFixer) (string, error) {
-	t, err := parseString(s)
-	if err != nil {
-		return "", &Error{
-			Verb:    verb,
-			ModPath: path,
-			Err: &module.InvalidVersionError{
-				Version: *s,
-				Err:     err,
-			},
-		}
-	}
-	if fix != nil {
-		fixed, err := fix(path, t)
-		if err != nil {
-			if err, ok := err.(*module.ModuleError); ok {
-				return "", &Error{
-					Verb:    verb,
-					ModPath: path,
-					Err:     err.Err,
-				}
-			}
-			return "", err
-		}
-		t = fixed
-	} else {
-		cv := module.CanonicalVersion(t)
-		if cv == "" {
-			return "", &Error{
-				Verb:    verb,
-				ModPath: path,
-				Err: &module.InvalidVersionError{
-					Version: t,
-					Err:     errors.New("must be of the form v1.2.3"),
-				},
-			}
-		}
-		t = cv
-	}
-	*s = t
-	return *s, nil
-}
+type ErrorList = modfile.ErrorList
+type Error = modfile.Error
 
-func parseExt(s *string) (string, error) {
-	t, err := parseString(s)
-	if err != nil {
-		return "", &InvalidExtError{
-			Ext: t,
-			Err: err,
-		}
-	}
-	if t == "" {
-		return "", nil
-	}
-	if len(t) < 2 {
-		return "", &InvalidExtError{
-			Ext: *s,
-			Err: fmt.Errorf("invalid ext format"),
-		}
-	}
-	if t[0] != '.' {
-		return "", &InvalidExtError{
-			Ext: *s,
-			Err: fmt.Errorf("invalid ext format"),
-		}
-	}
-	*s = t
-	return *s, nil
-}
+// -----------------------------------------------------------------------------
 
-func modulePathMajor(path string) (string, error) {
-	_, major, ok := module.SplitPathVersion(path)
-	if !ok {
-		return "", fmt.Errorf("invalid module path")
-	}
-	return major, nil
-}
+const (
+	directiveInvalid = iota
+	directiveModule
+	directiveGo
+	directiveGop
+	directiveClassfile
+)
 
-func (f *File) Format() ([]byte, error) {
-	return Format(f.Syntax), nil
-}
+const (
+	directiveLineBlock = 0x80 + iota
+	directiveRegister
+	directiveRequire
+	directiveExclude
+	directiveReplace
+	directiveRetract
+)
 
-// Cleanup cleans up the file f after any edit operations.
-// To avoid quadratic behavior, modifications like DropRequire
-// clear the entry but do not remove it from the slice.
-// Cleanup cleans out all the cleared entries.
-func (f *File) Cleanup() {
-	w := 0
-	for _, r := range f.Require {
-		if r.Mod.Path != "" {
-			f.Require[w] = r
-			w++
-		}
-	}
-	f.Require = f.Require[:w]
+var directiveWeights = map[string]int{
+	"module":    directiveModule,
+	"go":        directiveGo,
+	"gop":       directiveGop,
+	"classfile": directiveClassfile,
+	"register":  directiveRegister,
+	"require":   directiveRequire,
+	"exclude":   directiveExclude,
+	"replace":   directiveReplace,
+	"retract":   directiveRetract,
+}
 
-	w = 0
-	for _, x := range f.Exclude {
-		if x.Mod.Path != "" {
-			f.Exclude[w] = x
-			w++
-		}
+func getWeight(e Expr) int {
+	if line, ok := e.(*Line); ok {
+		return directiveWeights[line.Token[0]]
 	}
-	f.Exclude = f.Exclude[:w]
-
-	w = 0
-	for _, r := range f.Replace {
-		if r.Old.Path != "" {
-			f.Replace[w] = r
-			w++
-		}
+	if w, ok := directiveWeights[e.(*LineBlock).Token[0]]; ok {
+		return w
 	}
-	f.Replace = f.Replace[:w]
+	return directiveLineBlock
+}
 
-	w = 0
-	for _, r := range f.Retract {
-		if r.Low != "" || r.High != "" {
-			f.Retract[w] = r
-			w++
-		}
+func updateLine(x *FileSyntax, line *Line, tokens ...string) {
+	if line.InBlock {
+		tokens = tokens[1:]
 	}
-	f.Retract = f.Retract[:w]
-
-	f.Syntax.Cleanup()
+	line.Token = tokens
 }
 
-func (f *File) AddGoStmt(version string) error {
-	if !GoVersionRE.MatchString(version) {
-		return fmt.Errorf("invalid language version string %q", version)
-	}
-	if f.Go == nil {
-		var hint Expr
-		if f.Module != nil && f.Module.Syntax != nil {
-			hint = f.Module.Syntax
-		}
-		f.Go = &Go{
-			Version: version,
-			Syntax:  f.Syntax.addLine(hint, "go", version),
+func addLine(x *FileSyntax, tokens ...string) *Line {
+	new := &Line{Token: tokens}
+	w := directiveWeights[tokens[0]]
+	for i, e := range x.Stmt {
+		w2 := getWeight(e)
+		if w <= w2 {
+			x.Stmt = append(x.Stmt, nil)
+			copy(x.Stmt[i+1:], x.Stmt[i:])
+			x.Stmt[i] = new
+			return new
 		}
-	} else {
-		f.Go.Version = version
-		f.Syntax.updateLine(f.Go.Syntax, "go", version)
 	}
-	return nil
+	x.Stmt = append(x.Stmt, new)
+	return new
 }
 
 func (f *File) AddGopStmt(version string) error {
-	if !GopVersionRE.MatchString(version) {
+	if !modfile.GoVersionRE.MatchString(version) {
 		return fmt.Errorf("invalid language version string %q", version)
 	}
 	if f.Gop == nil {
-		var hint Expr
-		if f.Module != nil && f.Module.Syntax != nil {
-			hint = f.Module.Syntax
-		}
 		f.Gop = &Gop{
 			Version: version,
-			Syntax:  f.Syntax.addLine(hint, "gop", version),
+			Syntax:  addLine(f.Syntax, "gop", version),
 		}
 	} else {
 		f.Gop.Version = version
-		f.Syntax.updateLine(f.Gop.Syntax, "gop", version)
-	}
-	return nil
-}
-
-// AddRequire sets the first require line for path to version vers,
-// preserving any existing comments for that line and removing all
-// other lines for path.
-//
-// If no line currently exists for path, AddRequire adds a new line
-// at the end of the last require block.
-func (f *File) AddRequire(path, vers string) error {
-	need := true
-	for _, r := range f.Require {
-		if r.Mod.Path == path {
-			if need {
-				r.Mod.Version = vers
-				f.Syntax.updateLine(r.Syntax, "require", AutoQuote(path), vers)
-				need = false
-			} else {
-				r.Syntax.markRemoved()
-				*r = Require{}
-			}
-		}
-	}
-
-	if need {
-		f.AddNewRequire(path, vers, false)
-	}
-	return nil
-}
-
-// AddNewRequire adds a new require line for path at version vers at the end of
-// the last require block, regardless of any existing require lines for path.
-func (f *File) AddNewRequire(path, vers string, indirect bool) {
-	line := f.Syntax.addLine(nil, "require", AutoQuote(path), vers)
-	r := &Require{
-		Mod:    module.Version{Path: path, Version: vers},
-		Syntax: line,
-	}
-	r.setIndirect(indirect)
-	f.Require = append(f.Require, r)
-}
-
-// SetRequire updates the requirements of f to contain exactly req, preserving
-// the existing block structure and line comment contents (except for 'indirect'
-// markings) for the first requirement on each named module path.
-//
-// The Syntax field is ignored for the requirements in req.
-//
-// Any requirements not already present in the file are added to the block
-// containing the last require line.
-//
-// The requirements in req must specify at most one distinct version for each
-// module path.
-//
-// If any existing requirements may be removed, the caller should call Cleanup
-// after all edits are complete.
-func (f *File) SetRequire(req []*Require) {
-	type elem struct {
-		version  string
-		indirect bool
-	}
-	need := make(map[string]elem)
-	for _, r := range req {
-		if prev, dup := need[r.Mod.Path]; dup && prev.version != r.Mod.Version {
-			panic(fmt.Errorf("SetRequire called with conflicting versions for path %s (%s and %s)", r.Mod.Path, prev.version, r.Mod.Version))
-		}
-		need[r.Mod.Path] = elem{r.Mod.Version, r.Indirect}
-	}
-
-	// Update or delete the existing Require entries to preserve
-	// only the first for each module path in req.
-	for _, r := range f.Require {
-		e, ok := need[r.Mod.Path]
-		if ok {
-			r.setVersion(e.version)
-			r.setIndirect(e.indirect)
-		} else {
-			r.markRemoved()
-		}
-		delete(need, r.Mod.Path)
-	}
-
-	// Add new entries in the last block of the file for any paths that weren't
-	// already present.
-	//
-	// This step is nondeterministic, but the final result will be deterministic
-	// because we will sort the block.
-	for path, e := range need {
-		f.AddNewRequire(path, e.version, e.indirect)
-	}
-
-	f.SortBlocks()
-}
-
-// SetRequireSeparateIndirect updates the requirements of f to contain the given
-// requirements. Comment contents (except for 'indirect' markings) are retained
-// from the first existing requirement for each module path. Like SetRequire,
-// SetRequireSeparateIndirect adds requirements for new paths in req,
-// updates the version and "// indirect" comment on existing requirements,
-// and deletes requirements on paths not in req. Existing duplicate requirements
-// are deleted.
-//
-// As its name suggests, SetRequireSeparateIndirect puts direct and indirect
-// requirements into two separate blocks, one containing only direct
-// requirements, and the other containing only indirect requirements.
-// SetRequireSeparateIndirect may move requirements between these two blocks
-// when their indirect markings change. However, SetRequireSeparateIndirect
-// won't move requirements from other blocks, especially blocks with comments.
-//
-// If the file initially has one uncommented block of requirements,
-// SetRequireSeparateIndirect will split it into a direct-only and indirect-only
-// block. This aids in the transition to separate blocks.
-func (f *File) SetRequireSeparateIndirect(req []*Require) {
-	// hasComments returns whether a line or block has comments
-	// other than "indirect".
-	hasComments := func(c Comments) bool {
-		return len(c.Before) > 0 || len(c.After) > 0 || len(c.Suffix) > 1 ||
-			(len(c.Suffix) == 1 &&
-				strings.TrimSpace(strings.TrimPrefix(c.Suffix[0].Token, string(slashSlash))) != "indirect")
-	}
-
-	// moveReq adds r to block. If r was in another block, moveReq deletes
-	// it from that block and transfers its comments.
-	moveReq := func(r *Require, block *LineBlock) {
-		var line *Line
-		if r.Syntax == nil {
-			line = &Line{Token: []string{AutoQuote(r.Mod.Path), r.Mod.Version}}
-			r.Syntax = line
-			if r.Indirect {
-				r.setIndirect(true)
-			}
-		} else {
-			line = new(Line)
-			*line = *r.Syntax
-			if !line.InBlock && len(line.Token) > 0 && line.Token[0] == "require" {
-				line.Token = line.Token[1:]
-			}
-			r.Syntax.Token = nil // Cleanup will delete the old line.
-			r.Syntax = line
-		}
-		line.InBlock = true
-		block.Line = append(block.Line, line)
-	}
-
-	// Examine existing require lines and blocks.
-	var (
-		// We may insert new requirements into the last uncommented
-		// direct-only and indirect-only blocks. We may also move requirements
-		// to the opposite block if their indirect markings change.
-		lastDirectIndex   = -1
-		lastIndirectIndex = -1
-
-		// If there are no direct-only or indirect-only blocks, a new block may
-		// be inserted after the last require line or block.
-		lastRequireIndex = -1
-
-		// If there's only one require line or block, and it's uncommented,
-		// we'll move its requirements to the direct-only or indirect-only blocks.
-		requireLineOrBlockCount = 0
-
-		// Track the block each requirement belongs to (if any) so we can
-		// move them later.
-		lineToBlock = make(map[*Line]*LineBlock)
-	)
-	for i, stmt := range f.Syntax.Stmt {
-		switch stmt := stmt.(type) {
-		case *Line:
-			if len(stmt.Token) == 0 || stmt.Token[0] != "require" {
-				continue
-			}
-			lastRequireIndex = i
-			requireLineOrBlockCount++
-			if !hasComments(stmt.Comments) {
-				if isIndirect(stmt) {
-					lastIndirectIndex = i
-				} else {
-					lastDirectIndex = i
-				}
-			}
-
-		case *LineBlock:
-			if len(stmt.Token) == 0 || stmt.Token[0] != "require" {
-				continue
-			}
-			lastRequireIndex = i
-			requireLineOrBlockCount++
-			allDirect := len(stmt.Line) > 0 && !hasComments(stmt.Comments)
-			allIndirect := len(stmt.Line) > 0 && !hasComments(stmt.Comments)
-			for _, line := range stmt.Line {
-				lineToBlock[line] = stmt
-				if hasComments(line.Comments) {
-					allDirect = false
-					allIndirect = false
-				} else if isIndirect(line) {
-					allDirect = false
-				} else {
-					allIndirect = false
-				}
-			}
-			if allDirect {
-				lastDirectIndex = i
-			}
-			if allIndirect {
-				lastIndirectIndex = i
-			}
-		}
-	}
-
-	oneFlatUncommentedBlock := requireLineOrBlockCount == 1 &&
-		!hasComments(*f.Syntax.Stmt[lastRequireIndex].Comment())
-
-	// Create direct and indirect blocks if needed. Convert lines into blocks
-	// if needed. If we end up with an empty block or a one-line block,
-	// Cleanup will delete it or convert it to a line later.
-	insertBlock := func(i int) *LineBlock {
-		block := &LineBlock{Token: []string{"require"}}
-		f.Syntax.Stmt = append(f.Syntax.Stmt, nil)
-		copy(f.Syntax.Stmt[i+1:], f.Syntax.Stmt[i:])
-		f.Syntax.Stmt[i] = block
-		return block
-	}
-
-	ensureBlock := func(i int) *LineBlock {
-		switch stmt := f.Syntax.Stmt[i].(type) {
-		case *LineBlock:
-			return stmt
-		case *Line:
-			block := &LineBlock{
-				Token: []string{"require"},
-				Line:  []*Line{stmt},
-			}
-			stmt.Token = stmt.Token[1:] // remove "require"
-			stmt.InBlock = true
-			f.Syntax.Stmt[i] = block
-			return block
-		default:
-			panic(fmt.Sprintf("unexpected statement: %v", stmt))
-		}
-	}
-
-	var lastDirectBlock *LineBlock
-	if lastDirectIndex < 0 {
-		if lastIndirectIndex >= 0 {
-			lastDirectIndex = lastIndirectIndex
-			lastIndirectIndex++
-		} else if lastRequireIndex >= 0 {
-			lastDirectIndex = lastRequireIndex + 1
-		} else {
-			lastDirectIndex = len(f.Syntax.Stmt)
-		}
-		lastDirectBlock = insertBlock(lastDirectIndex)
-	} else {
-		lastDirectBlock = ensureBlock(lastDirectIndex)
-	}
-
-	var lastIndirectBlock *LineBlock
-	if lastIndirectIndex < 0 {
-		lastIndirectIndex = lastDirectIndex + 1
-		lastIndirectBlock = insertBlock(lastIndirectIndex)
-	} else {
-		lastIndirectBlock = ensureBlock(lastIndirectIndex)
-	}
-
-	// Delete requirements we don't want anymore.
-	// Update versions and indirect comments on requirements we want to keep.
-	// If a requirement is in last{Direct,Indirect}Block with the wrong
-	// indirect marking after this, or if the requirement is in an single
-	// uncommented mixed block (oneFlatUncommentedBlock), move it to the
-	// correct block.
-	//
-	// Some blocks may be empty after this. Cleanup will remove them.
-	need := make(map[string]*Require)
-	for _, r := range req {
-		need[r.Mod.Path] = r
-	}
-	have := make(map[string]*Require)
-	for _, r := range f.Require {
-		path := r.Mod.Path
-		if need[path] == nil || have[path] != nil {
-			// Requirement not needed, or duplicate requirement. Delete.
-			r.markRemoved()
-			continue
-		}
-		have[r.Mod.Path] = r
-		r.setVersion(need[path].Mod.Version)
-		r.setIndirect(need[path].Indirect)
-		if need[path].Indirect &&
-			(oneFlatUncommentedBlock || lineToBlock[r.Syntax] == lastDirectBlock) {
-			moveReq(r, lastIndirectBlock)
-		} else if !need[path].Indirect &&
-			(oneFlatUncommentedBlock || lineToBlock[r.Syntax] == lastIndirectBlock) {
-			moveReq(r, lastDirectBlock)
-		}
-	}
-
-	// Add new requirements.
-	for path, r := range need {
-		if have[path] == nil {
-			if r.Indirect {
-				moveReq(r, lastIndirectBlock)
-			} else {
-				moveReq(r, lastDirectBlock)
-			}
-			f.Require = append(f.Require, r)
-		}
-	}
-
-	f.SortBlocks()
-}
-
-func (f *File) DropRequire(path string) error {
-	for _, r := range f.Require {
-		if r.Mod.Path == path {
-			r.Syntax.markRemoved()
-			*r = Require{}
-		}
-	}
-	return nil
-}
-
-// AddExclude adds a exclude statement to the mod file. Errors if the provided
-// version is not a canonical version string
-func (f *File) AddExclude(path, vers string) error {
-	if err := checkCanonicalVersion(path, vers); err != nil {
-		return err
-	}
-
-	var hint *Line
-	for _, x := range f.Exclude {
-		if x.Mod.Path == path && x.Mod.Version == vers {
-			return nil
-		}
-		if x.Mod.Path == path {
-			hint = x.Syntax
-		}
-	}
-
-	f.Exclude = append(f.Exclude, &Exclude{Mod: module.Version{Path: path, Version: vers}, Syntax: f.Syntax.addLine(hint, "exclude", AutoQuote(path), vers)})
-	return nil
-}
-
-func (f *File) DropExclude(path, vers string) error {
-	for _, x := range f.Exclude {
-		if x.Mod.Path == path && x.Mod.Version == vers {
-			x.Syntax.markRemoved()
-			*x = Exclude{}
-		}
-	}
-	return nil
-}
-
-func (f *File) AddReplace(oldPath, oldVers, newPath, newVers string) error {
-	return addReplace(f.Syntax, &f.Replace, oldPath, oldVers, newPath, newVers)
-}
-
-func addReplace(syntax *FileSyntax, replace *[]*Replace, oldPath, oldVers, newPath, newVers string) error {
-	need := true
-	old := module.Version{Path: oldPath, Version: oldVers}
-	new := module.Version{Path: newPath, Version: newVers}
-	tokens := []string{"replace", AutoQuote(oldPath)}
-	if oldVers != "" {
-		tokens = append(tokens, oldVers)
-	}
-	tokens = append(tokens, "=>", AutoQuote(newPath))
-	if newVers != "" {
-		tokens = append(tokens, newVers)
-	}
-
-	var hint *Line
-	for _, r := range *replace {
-		if r.Old.Path == oldPath && (oldVers == "" || r.Old.Version == oldVers) {
-			if need {
-				// Found replacement for old; update to use new.
-				r.New = new
-				syntax.updateLine(r.Syntax, tokens...)
-				need = false
-				continue
-			}
-			// Already added; delete other replacements for same.
-			r.Syntax.markRemoved()
-			*r = Replace{}
-		}
-		if r.Old.Path == oldPath {
-			hint = r.Syntax
-		}
-	}
-	if need {
-		*replace = append(*replace, &Replace{Old: old, New: new, Syntax: syntax.addLine(hint, tokens...)})
-	}
-	return nil
-}
-
-func (f *File) DropReplace(oldPath, oldVers string) error {
-	for _, r := range f.Replace {
-		if r.Old.Path == oldPath && r.Old.Version == oldVers {
-			r.Syntax.markRemoved()
-			*r = Replace{}
-		}
-	}
-	return nil
-}
-
-// AddRetract adds a retract statement to the mod file. Errors if the provided
-// version interval does not consist of canonical version strings
-func (f *File) AddRetract(vi VersionInterval, rationale string) error {
-	var path string
-	if f.Module != nil {
-		path = f.Module.Mod.Path
-	}
-	if err := checkCanonicalVersion(path, vi.High); err != nil {
-		return err
-	}
-	if err := checkCanonicalVersion(path, vi.Low); err != nil {
-		return err
-	}
-
-	r := &Retract{
-		VersionInterval: vi,
-	}
-	if vi.Low == vi.High {
-		r.Syntax = f.Syntax.addLine(nil, "retract", AutoQuote(vi.Low))
-	} else {
-		r.Syntax = f.Syntax.addLine(nil, "retract", "[", AutoQuote(vi.Low), ",", AutoQuote(vi.High), "]")
-	}
-	if rationale != "" {
-		for _, line := range strings.Split(rationale, "\n") {
-			com := Comment{Token: "// " + line}
-			r.Syntax.Comment().Before = append(r.Syntax.Comment().Before, com)
-		}
-	}
-	return nil
-}
-
-func (f *File) DropRetract(vi VersionInterval) error {
-	for _, r := range f.Retract {
-		if r.VersionInterval == vi {
-			r.Syntax.markRemoved()
-			*r = Retract{}
-		}
-	}
-	return nil
-}
-
-func (f *File) SortBlocks() {
-	f.removeDups() // otherwise sorting is unsafe
-
-	for _, stmt := range f.Syntax.Stmt {
-		block, ok := stmt.(*LineBlock)
-		if !ok {
-			continue
-		}
-		less := lineLess
-		if block.Token[0] == "retract" {
-			less = lineRetractLess
-		}
-		sort.SliceStable(block.Line, func(i, j int) bool {
-			return less(block.Line[i], block.Line[j])
-		})
-	}
-}
-
-// removeDups removes duplicate exclude and replace directives.
-//
-// Earlier exclude directives take priority.
-//
-// Later replace directives take priority.
-//
-// require directives are not de-duplicated. That's left up to higher-level
-// logic (MVS).
-//
-// retract directives are not de-duplicated since comments are
-// meaningful, and versions may be retracted multiple times.
-func (f *File) removeDups() {
-	removeDups(f.Syntax, &f.Exclude, &f.Replace)
-}
-
-func removeDups(syntax *FileSyntax, exclude *[]*Exclude, replace *[]*Replace) {
-	kill := make(map[*Line]bool)
-
-	// Remove duplicate excludes.
-	if exclude != nil {
-		haveExclude := make(map[module.Version]bool)
-		for _, x := range *exclude {
-			if haveExclude[x.Mod] {
-				kill[x.Syntax] = true
-				continue
-			}
-			haveExclude[x.Mod] = true
-		}
-		var excl []*Exclude
-		for _, x := range *exclude {
-			if !kill[x.Syntax] {
-				excl = append(excl, x)
-			}
-		}
-		*exclude = excl
-	}
-
-	// Remove duplicate replacements.
-	// Later replacements take priority over earlier ones.
-	haveReplace := make(map[module.Version]bool)
-	for i := len(*replace) - 1; i >= 0; i-- {
-		x := (*replace)[i]
-		if haveReplace[x.Old] {
-			kill[x.Syntax] = true
-			continue
-		}
-		haveReplace[x.Old] = true
-	}
-	var repl []*Replace
-	for _, x := range *replace {
-		if !kill[x.Syntax] {
-			repl = append(repl, x)
-		}
-	}
-	*replace = repl
-
-	// Duplicate require and retract directives are not removed.
-
-	// Drop killed statements from the syntax tree.
-	var stmts []Expr
-	for _, stmt := range syntax.Stmt {
-		switch stmt := stmt.(type) {
-		case *Line:
-			if kill[stmt] {
-				continue
-			}
-		case *LineBlock:
-			var lines []*Line
-			for _, line := range stmt.Line {
-				if !kill[line] {
-					lines = append(lines, line)
-				}
-			}
-			stmt.Line = lines
-			if len(lines) == 0 {
-				continue
-			}
-		}
-		stmts = append(stmts, stmt)
+		updateLine(f.Syntax, f.Gop.Syntax, "gop", version)
 	}
-	syntax.Stmt = stmts
-}
-
-// lineLess returns whether li should be sorted before lj. It sorts
-// lexicographically without assigning any special meaning to tokens.
-func lineLess(li, lj *Line) bool {
-	for k := 0; k < len(li.Token) && k < len(lj.Token); k++ {
-		if li.Token[k] != lj.Token[k] {
-			return li.Token[k] < lj.Token[k]
-		}
-	}
-	return len(li.Token) < len(lj.Token)
-}
-
-// lineRetractLess returns whether li should be sorted before lj for lines in
-// a "retract" block. It treats each line as a version interval. Single versions
-// are compared as if they were intervals with the same low and high version.
-// Intervals are sorted in descending order, first by low version, then by
-// high version, using semver.Compare.
-func lineRetractLess(li, lj *Line) bool {
-	interval := func(l *Line) VersionInterval {
-		if len(l.Token) == 1 {
-			return VersionInterval{Low: l.Token[0], High: l.Token[0]}
-		} else if len(l.Token) == 5 && l.Token[0] == "[" && l.Token[2] == "," && l.Token[4] == "]" {
-			return VersionInterval{Low: l.Token[1], High: l.Token[3]}
-		} else {
-			// Line in unknown format. Treat as an invalid version.
-			return VersionInterval{}
-		}
-	}
-	vii := interval(li)
-	vij := interval(lj)
-	if cmp := semver.Compare(vii.Low, vij.Low); cmp != 0 {
-		return cmp > 0
-	}
-	return semver.Compare(vii.High, vij.High) > 0
-}
-
-// checkCanonicalVersion returns a non-nil error if vers is not a canonical
-// version string or does not match the major version of path.
-//
-// If path is non-empty, the error text suggests a format with a major version
-// corresponding to the path.
-func checkCanonicalVersion(path, vers string) error {
-	_, pathMajor, pathMajorOk := module.SplitPathVersion(path)
-
-	if vers == "" || vers != module.CanonicalVersion(vers) {
-		if pathMajor == "" {
-			return &module.InvalidVersionError{
-				Version: vers,
-				Err:     fmt.Errorf("must be of the form v1.2.3"),
-			}
-		}
-		return &module.InvalidVersionError{
-			Version: vers,
-			Err:     fmt.Errorf("must be of the form %s.2.3", module.PathMajorPrefix(pathMajor)),
-		}
-	}
-
-	if pathMajorOk {
-		if err := module.CheckPathMajor(vers, pathMajor); err != nil {
-			if pathMajor == "" {
-				// In this context, the user probably wrote "v2.3.4" when they meant
-				// "v2.3.4+incompatible". Suggest that instead of "v0 or v1".
-				return &module.InvalidVersionError{
-					Version: vers,
-					Err:     fmt.Errorf("should be %s+incompatible (or module %s/%v)", vers, path, semver.Major(vers)),
-				}
-			}
-			return err
-		}
-	}
-
 	return nil
 }
 
-type InvalidExtError struct {
-	Ext string
-	Err error
-}
-
-func (e *InvalidExtError) Error() string {
-	return fmt.Sprintf("ext %q invalid: %s", e.Ext, e.Err)
-}
-
-func (e *InvalidExtError) Unwrap() error { return e.Err }
+// -----------------------------------------------------------------------------


Test patch:
diff --git a/x/mod/modfile/gop_test.go b/x/mod/modfile/gop_test.go
new file mode 100644
index 00000000..8d98c960
--- /dev/null
+++ b/x/mod/modfile/gop_test.go
@@ -0,0 +1,181 @@
+/*
+ * 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 (
+	"testing"
+
+	"golang.org/x/mod/modfile"
+)
+
+// -----------------------------------------------------------------------------
+
+const gopmodSpx = `
+module spx
+
+go 1.17
+gop 1.1
+
+classfile .gmx .spx github.com/goplus/spx math
+
+require (
+	github.com/ajstarks/svgo v0.0.0-20210927141636-6d70534b1098
+)
+`
+
+func TestGoModCompat(t *testing.T) {
+	const (
+		gopmod = gopmodSpx
+	)
+	f, err := modfile.ParseLax("go.mod", []byte(gopmod), nil)
+	if err != nil || len(f.Syntax.Stmt) != 5 {
+		t.Fatal("modfile.ParseLax failed:", f, err)
+	}
+
+	gop := f.Syntax.Stmt[2].(*modfile.Line)
+	if len(gop.Token) != 2 || gop.Token[0] != "gop" || gop.Token[1] != "1.1" {
+		t.Fatal("modfile.ParseLax gop:", gop)
+	}
+
+	require := f.Syntax.Stmt[4].(*modfile.LineBlock)
+	if len(require.Token) != 1 || require.Token[0] != "require" {
+		t.Fatal("modfile.ParseLax require:", require)
+	}
+	if len(require.Line) != 1 {
+		t.Fatal("modfile.ParseLax require.Line:", require.Line)
+	}
+}
+
+// -----------------------------------------------------------------------------
+
+func TestParse1(t *testing.T) {
+	const (
+		gopmod = gopmodSpx
+	)
+	f, err := ParseLax("github.com/goplus/gop/gop.mod", []byte(gopmod), nil)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	if f.Gop.Version != "1.1" {
+		t.Errorf("gop version expected be 1.1, but %s got", f.Gop.Version)
+	}
+
+	if f.Classfile.ProjExt != ".gmx" {
+		t.Errorf("classfile exts expected be .gmx, but %s got", f.Classfile.ProjExt)
+	}
+	if f.Classfile.WorkExt != ".spx" {
+		t.Errorf("classfile exts expected be .spx, but %s got", f.Classfile.WorkExt)
+	}
+
+	if len(f.Classfile.PkgPaths) != 2 {
+		t.Errorf("classfile pkgpaths length expected be 2, but %d got", len(f.Classfile.PkgPaths))
+	}
+
+	if f.Classfile.PkgPaths[0] != "github.com/goplus/spx" {
+		t.Errorf("classfile path expected be github.com/goplus/spx, but %s got", f.Classfile.PkgPaths[0])
+	}
+	if f.Classfile.PkgPaths[1] != "math" {
+		t.Errorf("classfile path expected be math, but %s got", f.Classfile.PkgPaths[1])
+	}
+}
+
+const gopmodUserProj = `
+module moduleUserProj
+
+go 1.17
+gop 1.1
+
+register github.com/goplus/spx
+
+require (
+    github.com/goplus/spx v1.0
+)
+`
+
+func TestParse2(t *testing.T) {
+	const (
+		gopmod = gopmodUserProj
+	)
+	f, err := Parse("github.com/goplus/gop/gop.mod", []byte(gopmod), nil)
+	if err != nil || len(f.Register) != 1 {
+		t.Fatal("Parse:", f, err)
+		return
+	}
+	if f.Register[0].ClassfileMod != "github.com/goplus/spx" {
+		t.Fatal("Parse => Register:", f.Register)
+	}
+}
+
+func TestParseErr(t *testing.T) {
+	doTestParseErr(t, `gop.mod:3: repeated go statement`, `
+gop 1.1
+gop 1.2
+`)
+	doTestParseErr(t, `gop.mod:2: go directive expects exactly one argument`, `
+gop 1.1 1.2
+`)
+	doTestParseErr(t, `gop.mod:2: invalid gop version '1.x': must match format 1.23`, `
+gop 1.x
+`)
+	doTestParseErr(t, `gop.mod:2: register directive expects exactly one argument`, `
+register 1 2 3
+`)
+	doTestParseErr(t, `gop.mod:2: invalid quoted string: invalid syntax`, `
+register "\?"
+`)
+	doTestParseErr(t, `gop.mod:2: malformed module path "-": leading dash`, `
+register -
+`)
+	doTestParseErr(t, `gop.mod:3: repeated classfile statement`, `
+classfile .gmx .spx github.com/goplus/spx math
+classfile .gmx .spx github.com/goplus/spx math
+`)
+	doTestParseErr(t, `gop.mod:2: usage: classfile projExt workExt [classFilePkgPath ...]`, `
+classfile .gmx .spx
+`)
+	doTestParseErr(t, `gop.mod:2: ext . invalid: invalid ext format`, `
+classfile .gmx . math
+`)
+	doTestParseErr(t, `gop.mod:2: ext "\?" invalid: invalid syntax`, `
+classfile "\?" .spx math
+`)
+	doTestParseErr(t, `gop.mod:2: invalid quoted string: invalid syntax`, `
+classfile .123 .spx "\?"
+`)
+	doTestParseErr(t, `gop.mod:2: unknown directive: unknown`, `
+unknown .spx
+`)
+	doTestParseErr(t, `gop.mod:2: invalid go version '1.x': must match format 1.23`, `
+go 1.x
+`)
+}
+
+func doTestParseErr(t *testing.T, errMsg string, gopmod string) {
+	t.Run(errMsg, func(t *testing.T) {
+		_, err := Parse("gop.mod", []byte(gopmod), nil)
+		if err == nil {
+			t.Fatal("Parse: no error?")
+			return
+		}
+		if err.Error() != errMsg {
+			t.Error("Parse got:", err, "\nExpected:", errMsg)
+		}
+	})
+}
+
+// -----------------------------------------------------------------------------
diff --git a/x/mod/modfile/read_test.go b/x/mod/modfile/read_test.go
index 054013a9..f4254e10 100644
--- a/x/mod/modfile/read_test.go
+++ b/x/mod/modfile/read_test.go
@@ -16,377 +16,9 @@
 package modfile
 
 import (
-	"bytes"
-	"fmt"
-	"io/ioutil"
-	"os"
-	"os/exec"
-	"path/filepath"
-	"reflect"
-	"strings"
 	"testing"
 )
 
-// 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
@@ -430,167 +62,3 @@ func TestModulePath(t *testing.T) {
 		})
 	}
 }
-
-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 a3549db1..26ad0aa0 100644
--- a/x/mod/modfile/rule_test.go
+++ b/x/mod/modfile/rule_test.go
@@ -17,1574 +17,148 @@ package modfile
 
 import (
 	"bytes"
-	"fmt"
-	"strings"
+	"syscall"
 	"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 addGopTests = []struct {
-	desc    string
-	in      string
-	version string
-	out     string
-}{
-	{
-		`module_only`,
-		`module m
-		`,
-		`1.0`,
-		`module m
-		gop 1.0
-		`,
-	},
-	{
-		`module_before_require`,
-		`module m
-		require x.y/a v1.2.3
-		`,
-		`1.0`,
-		`module m
-		gop 1.0
-		require x.y/a v1.2.3
-		`,
-	},
-	{
-		`require_before_module`,
-		`require x.y/a v1.2.3
-		module example.com/inverted
-		`,
-		`1.0`,
-		`require x.y/a v1.2.3
-		module example.com/inverted
-		gop 1.0
-		`,
-	},
-	{
-		`require_only`,
-		`require x.y/a v1.2.3
-		`,
-		`1.0`,
-		`require x.y/a v1.2.3
-		gop 1.0
-		`,
-	},
-}
-
-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
+var addParseExtTests = []struct {
+	desc    string
+	ext     string
+	want    string
+	wantErr string
 }{
 	{
-		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,
+		"spx ok",
+		".spx",
+		".spx",
+		"",
 	},
 	{
-		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,
+		"no ext",
+		"",
+		"",
+		"",
 	},
 	{
-		desc:    `retract_nomod`,
-		in:      `retract 1.0.0`,
-		wantErr: `in:1: no module directive found, so retract cannot be used`,
-		fix:     fixV,
+		"not a ext",
+		"gmx",
+		"",
+		"ext gmx invalid: invalid ext format",
 	},
 }
 
-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 TestParseExt(t *testing.T) {
+	if (&InvalidExtError{Err: syscall.EINVAL}).Unwrap() != syscall.EINVAL {
+		t.Fatal("InvalidExtError.Unwrap failed")
 	}
-}
-
-func TestSetRequire(t *testing.T) {
-	for _, tt := range setRequireTests {
+	for _, tt := range addParseExtTests {
 		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,
-				})
+			ext, err := parseExt(&tt.ext)
+			if err != nil {
+				if err.Error() != tt.wantErr {
+					t.Fatalf("wanterr: %s, but got: %s", tt.wantErr, err)
+				}
 			}
-
-			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))
+			if ext != tt.want {
+				t.Fatalf("want: %s, but got: %s", tt.want, ext)
 			}
 		})
 	}
 }
 
-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 TestIsDirectoryPath(t *testing.T) {
+	if !IsDirectoryPath("./...") {
+		t.Fatal("IsDirectoryPath failed")
 	}
 }
 
-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 TestFormat(t *testing.T) {
+	if b := Format(&FileSyntax{}); len(b) != 0 {
+		t.Fatal("Format failed:", b)
 	}
 }
 
-func TestAddGop(t *testing.T) {
-	for _, tt := range addGopTests {
-		t.Run(tt.desc, func(t *testing.T) {
-			testEdit(t, tt.in, tt.out, true, func(f *File) error {
-				return f.AddGopStmt(tt.version)
-			})
-		})
+func TestMustQuote(t *testing.T) {
+	if !MustQuote("") {
+		t.Fatal("MustQuote failed")
 	}
 }
 
-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)
-			})
-		})
-	}
+var addGopTests = []struct {
+	desc    string
+	in      string
+	version string
+	out     string
+}{
+	{
+		`module_only`,
+		`module m
+		`,
+		`1.14`,
+		`module m
+		gop 1.14
+		`,
+	},
+	{
+		`module_before_require`,
+		`module m
+		require x.y/a v1.2.3
+		`,
+		`1.14`,
+		`module m
+		gop 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`,
+		`gop 1.14
+		require x.y/a v1.2.3
+		module example.com/inverted
+		`,
+	},
+	{
+		`require_only`,
+		`require x.y/a v1.2.3
+		`,
+		`1.14`,
+		`gop 1.14
+		require x.y/a v1.2.3
+		`,
+	},
+	{
+		`replace gop`,
+		`require x.y/a v1.2.3
+		gop 1.10
+		`,
+		`1.14`,
+		`require x.y/a v1.2.3
+		gop 1.14
+		`,
+	},
 }
 
-func TestDropRetract(t *testing.T) {
-	for _, tt := range dropRetractTests {
+func TestAddGop(t *testing.T) {
+	for _, tt := range addGopTests {
 		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
+				return f.AddGopStmt(tt.version)
 			})
 		})
 	}
 }
 
-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 TestAddGopErr(t *testing.T) {
+	if new(File).AddGopStmt("1.x") == nil {
+		t.Fatal("AddGoStmt failed")
 	}
 }
 
@@ -1621,188 +195,4 @@ func testEdit(t *testing.T, in, want string, strict bool, transform func(f *File
 	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 addParseExtTests = []struct {
-	desc    string
-	ext     string
-	want    string
-	wantErr string
-}{
-	{
-		"spx ok",
-		".spx",
-		".spx",
-		"",
-	},
-	{
-		"no ext",
-		"",
-		"",
-		"",
-	},
-	{
-		"not a ext",
-		"gmx",
-		"",
-		"ext \"gmx\" invalid: invalid ext format",
-	},
-}
-
-func TestParseExt(t *testing.T) {
-	for _, tt := range addParseExtTests {
-		t.Run(tt.desc, func(t *testing.T) {
-			ext, err := parseExt(&tt.ext)
-			if err != nil {
-				if err.Error() != tt.wantErr {
-					t.Fatalf("wanterr %s,but got %s", tt.wantErr, err)
-				}
-			}
-			if ext != tt.want {
-				t.Fatalf("want %s,but got %s", tt.want, ext)
-			}
-		})
-	}
-}
-
-var gopmod = `
-module spx
-
-go 1.17
-gop 1.1
-
-classfile .gmx .spx github.com/goplus/spx math
-
-require (
-    github.com/ajstarks/svgo v0.0.0-20210927141636-6d70534b1098
-)
-`
-
-var gopmod2 = `
-module moduleUserProj
-
-go 1.17
-gop 1.1
-
-register github.com/goplus/spx
-
-require (
-    github.com/goplus/spx v1.0
-)
-`
-
-func TestParse(t *testing.T) {
-	f, err := Parse("github.com/goplus/gop/gop.mod", []byte(gopmod), func(path, vers string) (resolved string, err error) {
-		return vers, nil
-	})
-	if err != nil {
-		t.Error(err)
-		return
-	}
-	if f.Gop.Version != "1.1" {
-		t.Errorf("gop version expected be 1.1, but %s got", f.Gop.Version)
-	}
-
-	if f.Classfile.ProjExt != ".gmx" {
-		t.Errorf("classfile exts expected be .gmx, but %s got", f.Classfile.ProjExt)
-	}
-	if f.Classfile.WorkExt != ".spx" {
-		t.Errorf("classfile exts expected be .spx, but %s got", f.Classfile.WorkExt)
-	}
-
-	if len(f.Classfile.PkgPaths) != 2 {
-		t.Errorf("classfile pkgpaths length expected be 2, but %d got", len(f.Classfile.PkgPaths))
-	}
-
-	if f.Classfile.PkgPaths[0] != "github.com/goplus/spx" {
-		t.Errorf("classfile path expected be github.com/goplus/spx, but %s got", f.Classfile.PkgPaths[0])
-	}
-	if f.Classfile.PkgPaths[1] != "math" {
-		t.Errorf("classfile path expected be math, but %s got", f.Classfile.PkgPaths[1])
-	}
-
-	f2, err := Parse("github.com/goplus/gop/gop.mod", []byte(gopmod2), func(path, vers string) (resolved string, err error) {
-		return vers, nil
-	})
-
-	if err != nil {
-		t.Error(err)
-		return
-	}
-
-	if f2.Register.ClassfileMod != "github.com/goplus/spx" {
-		t.Errorf("register classfile mod expected be github.com/goplus/spx, but %s got", f2.Register.ClassfileMod)
-	}
-}
+// -----------------------------------------------------------------------------
diff --git a/x/mod/modfile/testdata/block.golden b/x/mod/modfile/testdata/block.golden
deleted file mode 100644
index 430c42e6..00000000
--- a/x/mod/modfile/testdata/block.golden
+++ /dev/null
@@ -1,36 +0,0 @@
-// 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
deleted file mode 100644
index a19990e8..00000000
--- a/x/mod/modfile/testdata/block.in
+++ /dev/null
@@ -1,33 +0,0 @@
-// 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
deleted file mode 100644
index 75f3b844..00000000
--- a/x/mod/modfile/testdata/comment.golden
+++ /dev/null
@@ -1,10 +0,0 @@
-// 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
deleted file mode 100644
index bfc2492b..00000000
--- a/x/mod/modfile/testdata/comment.in
+++ /dev/null
@@ -1,8 +0,0 @@
-// 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
deleted file mode 100644
index e69de29b..00000000
diff --git a/x/mod/modfile/testdata/empty.in b/x/mod/modfile/testdata/empty.in
deleted file mode 100644
index e69de29b..00000000
diff --git a/x/mod/modfile/testdata/gopkg.in.golden b/x/mod/modfile/testdata/gopkg.in.golden
deleted file mode 100644
index 41669b3a..00000000
--- a/x/mod/modfile/testdata/gopkg.in.golden
+++ /dev/null
@@ -1,6 +0,0 @@
-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
deleted file mode 100644
index 78ba9439..00000000
--- a/x/mod/modfile/testdata/module.golden
+++ /dev/null
@@ -1,1 +0,0 @@
-module abc
diff --git a/x/mod/modfile/testdata/module.in b/x/mod/modfile/testdata/module.in
deleted file mode 100644
index 08f38362..00000000
--- a/x/mod/modfile/testdata/module.in
+++ /dev/null
@@ -1,1 +0,0 @@
-module "abc"
diff --git a/x/mod/modfile/testdata/replace.golden b/x/mod/modfile/testdata/replace.golden
deleted file mode 100644
index 2e56d920..00000000
--- a/x/mod/modfile/testdata/replace.golden
+++ /dev/null
@@ -1,12 +0,0 @@
-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
deleted file mode 100644
index f5bef26d..00000000
--- a/x/mod/modfile/testdata/replace.in
+++ /dev/null
@@ -1,12 +0,0 @@
-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
deleted file mode 100644
index e1d9c728..00000000
--- a/x/mod/modfile/testdata/replace2.golden
+++ /dev/null
@@ -1,10 +0,0 @@
-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
deleted file mode 100644
index 78646986..00000000
--- a/x/mod/modfile/testdata/replace2.in
+++ /dev/null
@@ -1,10 +0,0 @@
-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
deleted file mode 100644
index f5d709e6..00000000
--- a/x/mod/modfile/testdata/retract.golden
+++ /dev/null
@@ -1,11 +0,0 @@
-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
deleted file mode 100644
index fa4a1f4f..00000000
--- a/x/mod/modfile/testdata/retract.in
+++ /dev/null
@@ -1,11 +0,0 @@
-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
deleted file mode 100644
index 8a5c7258..00000000
--- a/x/mod/modfile/testdata/rule1.golden
+++ /dev/null
@@ -1,7 +0,0 @@
-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
deleted file mode 100644
index ac9bbbfd..00000000
--- a/x/mod/modfile/testdata/work/comment.golden
+++ /dev/null
@@ -1,10 +0,0 @@
-// 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
deleted file mode 100644
index df6d702e..00000000
--- a/x/mod/modfile/testdata/work/comment.in
+++ /dev/null
@@ -1,8 +0,0 @@
-// 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
deleted file mode 100644
index e69de29b..00000000
diff --git a/x/mod/modfile/testdata/work/empty.in b/x/mod/modfile/testdata/work/empty.in
deleted file mode 100644
index e69de29b..00000000
diff --git a/x/mod/modfile/testdata/work/replace.golden b/x/mod/modfile/testdata/work/replace.golden
deleted file mode 100644
index 534c1dae..00000000
--- a/x/mod/modfile/testdata/work/replace.golden
+++ /dev/null
@@ -1,12 +0,0 @@
-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
deleted file mode 100644
index dc434d70..00000000
--- a/x/mod/modfile/testdata/work/replace.in
+++ /dev/null
@@ -1,12 +0,0 @@
-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
deleted file mode 100644
index ca03dfd0..00000000
--- a/x/mod/modfile/testdata/work/replace2.golden
+++ /dev/null
@@ -1,10 +0,0 @@
-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
deleted file mode 100644
index cf34eb43..00000000
--- a/x/mod/modfile/testdata/work/replace2.in
+++ /dev/null
@@ -1,10 +0,0 @@
-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
deleted file mode 100644
index 032bac18..00000000
--- a/x/mod/modfile/testdata/work/use.golden
+++ /dev/null
@@ -1,7 +0,0 @@
-use ../foo
-
-use (
-	/bar
-
-	baz
-)
diff --git a/x/mod/modfile/testdata/work/use.in b/x/mod/modfile/testdata/work/use.in
deleted file mode 100644
index cb790ddf..00000000
--- a/x/mod/modfile/testdata/work/use.in
+++ /dev/null
@@ -1,7 +0,0 @@
-use "../foo"
-
-use (
-	"/bar"
-
-	"baz"
-)