github.com/gernest/nezuko@v0.1.2/internal/modfile/rule.go (about)

     1  // Copyright 2018 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package modfile
     6  
     7  import (
     8  	"bytes"
     9  	"errors"
    10  	"fmt"
    11  	"path/filepath"
    12  	"regexp"
    13  	"sort"
    14  	"strconv"
    15  	"strings"
    16  	"unicode"
    17  
    18  	"github.com/gernest/nezuko/internal/module"
    19  	"github.com/gernest/nezuko/internal/semver"
    20  )
    21  
    22  // A File is the parsed, interpreted form of a z.mod file.
    23  type File struct {
    24  	Module  *Module
    25  	Exports *Exports
    26  	Require []*Require
    27  	Exclude []*Exclude
    28  	Replace []*Replace
    29  
    30  	Syntax *FileSyntax
    31  }
    32  
    33  // A Module is the module statement.
    34  type Module struct {
    35  	Mod    module.Version
    36  	Syntax *Line
    37  }
    38  
    39  // A Go is the go statement.
    40  type Go struct {
    41  	Version string // "1.23"
    42  	Syntax  *Line
    43  }
    44  
    45  // Exports exports is the exports statement
    46  type Exports struct {
    47  	Name   string
    48  	Syntax *Line
    49  }
    50  
    51  // A Require is a single require statement.
    52  type Require struct {
    53  	Mod      module.Version
    54  	Indirect bool // has "// indirect" comment
    55  	Syntax   *Line
    56  }
    57  
    58  // An Exclude is a single exclude statement.
    59  type Exclude struct {
    60  	Mod    module.Version
    61  	Syntax *Line
    62  }
    63  
    64  // A Replace is a single replace statement.
    65  type Replace struct {
    66  	Old    module.Version
    67  	New    module.Version
    68  	Syntax *Line
    69  }
    70  
    71  func (f *File) AddModuleStmt(path string) error {
    72  	if f.Syntax == nil {
    73  		f.Syntax = new(FileSyntax)
    74  	}
    75  	if f.Module == nil {
    76  		f.Module = &Module{
    77  			Mod:    module.Version{Path: path},
    78  			Syntax: f.Syntax.addLine(nil, "module", AutoQuote(path)),
    79  		}
    80  	} else {
    81  		f.Module.Mod.Path = path
    82  		f.Syntax.updateLine(f.Module.Syntax, "module", AutoQuote(path))
    83  	}
    84  	return nil
    85  }
    86  
    87  func (f *File) AddComment(text string) {
    88  	if f.Syntax == nil {
    89  		f.Syntax = new(FileSyntax)
    90  	}
    91  	f.Syntax.Stmt = append(f.Syntax.Stmt, &CommentBlock{
    92  		Comments: Comments{
    93  			Before: []Comment{
    94  				{
    95  					Token: text,
    96  				},
    97  			},
    98  		},
    99  	})
   100  }
   101  
   102  type VersionFixer func(path, version string) (string, error)
   103  
   104  // Parse parses the data, reported in errors as being from file,
   105  // into a File struct. It applies fix, if non-nil, to canonicalize all module versions found.
   106  func Parse(file string, data []byte, fix VersionFixer) (*File, error) {
   107  	return parseToFile(file, data, fix, true)
   108  }
   109  
   110  // ParseLax is like Parse but ignores unknown statements.
   111  // It is used when parsing z.mod files other than the main module,
   112  // under the theory that most statement types we add in the future will
   113  // only apply in the main module, like exclude and replace,
   114  // and so we get better gradual deployments if old go commands
   115  // simply ignore those statements when found in z.mod files
   116  // in dependencies.
   117  func ParseLax(file string, data []byte, fix VersionFixer) (*File, error) {
   118  	return parseToFile(file, data, fix, false)
   119  }
   120  
   121  func parseToFile(file string, data []byte, fix VersionFixer, strict bool) (*File, error) {
   122  	fs, err := parse(file, data)
   123  	if err != nil {
   124  		return nil, err
   125  	}
   126  	f := &File{
   127  		Syntax: fs,
   128  	}
   129  
   130  	var errs bytes.Buffer
   131  	for _, x := range fs.Stmt {
   132  		switch x := x.(type) {
   133  		case *Line:
   134  			f.add(&errs, x, x.Token[0], x.Token[1:], fix, strict)
   135  
   136  		case *LineBlock:
   137  			if len(x.Token) > 1 {
   138  				if strict {
   139  					fmt.Fprintf(&errs, "%s:%d: unknown block type: %s\n", file, x.Start.Line, strings.Join(x.Token, " "))
   140  				}
   141  				continue
   142  			}
   143  			switch x.Token[0] {
   144  			default:
   145  				if strict {
   146  					fmt.Fprintf(&errs, "%s:%d: unknown block type: %s\n", file, x.Start.Line, strings.Join(x.Token, " "))
   147  				}
   148  				continue
   149  			case "module", "require", "exclude", "replace":
   150  				for _, l := range x.Line {
   151  					f.add(&errs, l, x.Token[0], l.Token, fix, strict)
   152  				}
   153  			}
   154  		}
   155  	}
   156  
   157  	if errs.Len() > 0 {
   158  		return nil, errors.New(strings.TrimRight(errs.String(), "\n"))
   159  	}
   160  	return f, nil
   161  }
   162  
   163  var GoVersionRE = regexp.MustCompile(`([1-9][0-9]*)\.(0|[1-9][0-9]*)`)
   164  
   165  func (f *File) add(errs *bytes.Buffer, line *Line, verb string, args []string, fix VersionFixer, strict bool) {
   166  	// If strict is false, this module is a dependency.
   167  	// We ignore all unknown directives as well as main-module-only
   168  	// directives like replace and exclude. It will work better for
   169  	// forward compatibility if we can depend on modules that have unknown
   170  	// statements (presumed relevant only when acting as the main module)
   171  	// and simply ignore those statements.
   172  	if !strict {
   173  		switch verb {
   174  		case "module", "require", "go":
   175  			// want these even for dependency z.mods
   176  		default:
   177  			return
   178  		}
   179  	}
   180  
   181  	switch verb {
   182  	default:
   183  		fmt.Fprintf(errs, "%s:%d: unknown directive: %s\n", f.Syntax.Name, line.Start.Line, verb)
   184  
   185  	case "exports":
   186  		if f.Exports != nil {
   187  			fmt.Fprintf(errs, "%s:%d: repeated go statement\n", f.Syntax.Name, line.Start.Line)
   188  			return
   189  		}
   190  		if len(args) != 1 || !IsValidExport(args[0]) {
   191  			fmt.Fprintf(errs, "%s:%d: usage: go 1.23\n", f.Syntax.Name, line.Start.Line)
   192  			return
   193  		}
   194  		f.Exports = &Exports{Syntax: line}
   195  		f.Exports.Name = args[0]
   196  	case "module":
   197  		if f.Module != nil {
   198  			fmt.Fprintf(errs, "%s:%d: repeated module statement\n", f.Syntax.Name, line.Start.Line)
   199  			return
   200  		}
   201  		f.Module = &Module{Syntax: line}
   202  		if len(args) != 1 {
   203  
   204  			fmt.Fprintf(errs, "%s:%d: usage: module module/path [version]\n", f.Syntax.Name, line.Start.Line)
   205  			return
   206  		}
   207  		s, err := parseString(&args[0])
   208  		if err != nil {
   209  			fmt.Fprintf(errs, "%s:%d: invalid quoted string: %v\n", f.Syntax.Name, line.Start.Line, err)
   210  			return
   211  		}
   212  		f.Module.Mod = module.Version{Path: s}
   213  	case "require", "exclude":
   214  		if len(args) != 2 {
   215  			fmt.Fprintf(errs, "%s:%d: usage: %s module/path v1.2.3\n", f.Syntax.Name, line.Start.Line, verb)
   216  			return
   217  		}
   218  		s, err := parseString(&args[0])
   219  		if err != nil {
   220  			fmt.Fprintf(errs, "%s:%d: invalid quoted string: %v\n", f.Syntax.Name, line.Start.Line, err)
   221  			return
   222  		}
   223  		old := args[1]
   224  		v, err := parseVersion(s, &args[1], fix)
   225  		if err != nil {
   226  			fmt.Fprintf(errs, "%s:%d: invalid module version %q: %v\n", f.Syntax.Name, line.Start.Line, old, err)
   227  			return
   228  		}
   229  		pathMajor, err := modulePathMajor(s)
   230  		if err != nil {
   231  			fmt.Fprintf(errs, "%s:%d: %v\n", f.Syntax.Name, line.Start.Line, err)
   232  			return
   233  		}
   234  		if !module.MatchPathMajor(v, pathMajor) {
   235  			if pathMajor == "" {
   236  				pathMajor = "v0 or v1"
   237  			}
   238  			fmt.Fprintf(errs, "%s:%d: invalid module: %s should be %s, not %s (%s)\n", f.Syntax.Name, line.Start.Line, s, pathMajor, semver.Major(v), v)
   239  			return
   240  		}
   241  		if verb == "require" {
   242  			f.Require = append(f.Require, &Require{
   243  				Mod:      module.Version{Path: s, Version: v},
   244  				Syntax:   line,
   245  				Indirect: isIndirect(line),
   246  			})
   247  		} else {
   248  			f.Exclude = append(f.Exclude, &Exclude{
   249  				Mod:    module.Version{Path: s, Version: v},
   250  				Syntax: line,
   251  			})
   252  		}
   253  	case "replace":
   254  		arrow := 2
   255  		if len(args) >= 2 && args[1] == "=>" {
   256  			arrow = 1
   257  		}
   258  		if len(args) < arrow+2 || len(args) > arrow+3 || args[arrow] != "=>" {
   259  			fmt.Fprintf(errs, "%s:%d: usage: %s module/path [v1.2.3] => other/module v1.4\n\t or %s module/path [v1.2.3] => ../local/directory\n", f.Syntax.Name, line.Start.Line, verb, verb)
   260  			return
   261  		}
   262  		s, err := parseString(&args[0])
   263  		if err != nil {
   264  			fmt.Fprintf(errs, "%s:%d: invalid quoted string: %v\n", f.Syntax.Name, line.Start.Line, err)
   265  			return
   266  		}
   267  		pathMajor, err := modulePathMajor(s)
   268  		if err != nil {
   269  			fmt.Fprintf(errs, "%s:%d: %v\n", f.Syntax.Name, line.Start.Line, err)
   270  			return
   271  		}
   272  		var v string
   273  		if arrow == 2 {
   274  			old := args[1]
   275  			v, err = parseVersion(s, &args[1], fix)
   276  			if err != nil {
   277  				fmt.Fprintf(errs, "%s:%d: invalid module version %v: %v\n", f.Syntax.Name, line.Start.Line, old, err)
   278  				return
   279  			}
   280  			if !module.MatchPathMajor(v, pathMajor) {
   281  				if pathMajor == "" {
   282  					pathMajor = "v0 or v1"
   283  				}
   284  				fmt.Fprintf(errs, "%s:%d: invalid module: %s should be %s, not %s (%s)\n", f.Syntax.Name, line.Start.Line, s, pathMajor, semver.Major(v), v)
   285  				return
   286  			}
   287  		}
   288  		ns, err := parseString(&args[arrow+1])
   289  		if err != nil {
   290  			fmt.Fprintf(errs, "%s:%d: invalid quoted string: %v\n", f.Syntax.Name, line.Start.Line, err)
   291  			return
   292  		}
   293  		nv := ""
   294  		if len(args) == arrow+2 {
   295  			if !IsDirectoryPath(ns) {
   296  				fmt.Fprintf(errs, "%s:%d: replacement module without version must be directory path (rooted or starting with ./ or ../)\n", f.Syntax.Name, line.Start.Line)
   297  				return
   298  			}
   299  			if filepath.Separator == '/' && strings.Contains(ns, `\`) {
   300  				fmt.Fprintf(errs, "%s:%d: replacement directory appears to be Windows path (on a non-windows system)\n", f.Syntax.Name, line.Start.Line)
   301  				return
   302  			}
   303  		}
   304  		if len(args) == arrow+3 {
   305  			old := args[arrow+1]
   306  			nv, err = parseVersion(ns, &args[arrow+2], fix)
   307  			if err != nil {
   308  				fmt.Fprintf(errs, "%s:%d: invalid module version %v: %v\n", f.Syntax.Name, line.Start.Line, old, err)
   309  				return
   310  			}
   311  			if IsDirectoryPath(ns) {
   312  				fmt.Fprintf(errs, "%s:%d: replacement module directory path %q cannot have version\n", f.Syntax.Name, line.Start.Line, ns)
   313  				return
   314  			}
   315  		}
   316  		f.Replace = append(f.Replace, &Replace{
   317  			Old:    module.Version{Path: s, Version: v},
   318  			New:    module.Version{Path: ns, Version: nv},
   319  			Syntax: line,
   320  		})
   321  	}
   322  }
   323  
   324  // isIndirect reports whether line has a "// indirect" comment,
   325  // meaning it is in z.mod only for its effect on indirect dependencies,
   326  // so that it can be dropped entirely once the effective version of the
   327  // indirect dependency reaches the given minimum version.
   328  func isIndirect(line *Line) bool {
   329  	if len(line.Suffix) == 0 {
   330  		return false
   331  	}
   332  	f := strings.Fields(line.Suffix[0].Token)
   333  	return (len(f) == 2 && f[1] == "indirect" || len(f) > 2 && f[1] == "indirect;") && f[0] == "//"
   334  }
   335  
   336  // setIndirect sets line to have (or not have) a "// indirect" comment.
   337  func setIndirect(line *Line, indirect bool) {
   338  	if isIndirect(line) == indirect {
   339  		return
   340  	}
   341  	if indirect {
   342  		// Adding comment.
   343  		if len(line.Suffix) == 0 {
   344  			// New comment.
   345  			line.Suffix = []Comment{{Token: "// indirect", Suffix: true}}
   346  			return
   347  		}
   348  		// Insert at beginning of existing comment.
   349  		com := &line.Suffix[0]
   350  		space := " "
   351  		if len(com.Token) > 2 && com.Token[2] == ' ' || com.Token[2] == '\t' {
   352  			space = ""
   353  		}
   354  		com.Token = "// indirect;" + space + com.Token[2:]
   355  		return
   356  	}
   357  
   358  	// Removing comment.
   359  	f := strings.Fields(line.Suffix[0].Token)
   360  	if len(f) == 2 {
   361  		// Remove whole comment.
   362  		line.Suffix = nil
   363  		return
   364  	}
   365  
   366  	// Remove comment prefix.
   367  	com := &line.Suffix[0]
   368  	i := strings.Index(com.Token, "indirect;")
   369  	com.Token = "//" + com.Token[i+len("indirect;"):]
   370  }
   371  
   372  // IsDirectoryPath reports whether the given path should be interpreted
   373  // as a directory path. Just like on the go command line, relative paths
   374  // and rooted paths are directory paths; the rest are module paths.
   375  func IsDirectoryPath(ns string) bool {
   376  	// Because z.mod files can move from one system to another,
   377  	// we check all known path syntaxes, both Unix and Windows.
   378  	return strings.HasPrefix(ns, "./") || strings.HasPrefix(ns, "../") || strings.HasPrefix(ns, "/") ||
   379  		strings.HasPrefix(ns, `.\`) || strings.HasPrefix(ns, `..\`) || strings.HasPrefix(ns, `\`) ||
   380  		len(ns) >= 2 && ('A' <= ns[0] && ns[0] <= 'Z' || 'a' <= ns[0] && ns[0] <= 'z') && ns[1] == ':'
   381  }
   382  
   383  // MustQuote reports whether s must be quoted in order to appear as
   384  // a single token in a z.mod line.
   385  func MustQuote(s string) bool {
   386  	for _, r := range s {
   387  		if !unicode.IsPrint(r) || r == ' ' || r == '"' || r == '\'' || r == '`' {
   388  			return true
   389  		}
   390  	}
   391  	return s == "" || strings.Contains(s, "//") || strings.Contains(s, "/*")
   392  }
   393  
   394  // AutoQuote returns s or, if quoting is required for s to appear in a z.mod,
   395  // the quotation of s.
   396  func AutoQuote(s string) string {
   397  	if MustQuote(s) {
   398  		return strconv.Quote(s)
   399  	}
   400  	return s
   401  }
   402  
   403  func parseString(s *string) (string, error) {
   404  	t := *s
   405  	if strings.HasPrefix(t, `"`) {
   406  		var err error
   407  		if t, err = strconv.Unquote(t); err != nil {
   408  			return "", err
   409  		}
   410  	} else if strings.ContainsAny(t, "\"'`") {
   411  		// Other quotes are reserved both for possible future expansion
   412  		// and to avoid confusion. For example if someone types 'x'
   413  		// we want that to be a syntax error and not a literal x in literal quotation marks.
   414  		return "", fmt.Errorf("unquoted string cannot contain quote")
   415  	}
   416  	*s = AutoQuote(t)
   417  	return t, nil
   418  }
   419  
   420  func parseVersion(path string, s *string, fix VersionFixer) (string, error) {
   421  	t, err := parseString(s)
   422  	if err != nil {
   423  		return "", err
   424  	}
   425  	if fix != nil {
   426  		var err error
   427  		t, err = fix(path, t)
   428  		if err != nil {
   429  			return "", err
   430  		}
   431  	}
   432  	if v := module.CanonicalVersion(t); v != "" {
   433  		*s = v
   434  		return *s, nil
   435  	}
   436  	return "", fmt.Errorf("version must be of the form v1.2.3")
   437  }
   438  
   439  func modulePathMajor(path string) (string, error) {
   440  	_, major, ok := module.SplitPathVersion(path)
   441  	if !ok {
   442  		return "", fmt.Errorf("invalid module path")
   443  	}
   444  	return major, nil
   445  }
   446  
   447  func (f *File) Format() ([]byte, error) {
   448  	return Format(f.Syntax), nil
   449  }
   450  
   451  // Cleanup cleans up the file f after any edit operations.
   452  // To avoid quadratic behavior, modifications like DropRequire
   453  // clear the entry but do not remove it from the slice.
   454  // Cleanup cleans out all the cleared entries.
   455  func (f *File) Cleanup() {
   456  	w := 0
   457  	for _, r := range f.Require {
   458  		if r.Mod.Path != "" {
   459  			f.Require[w] = r
   460  			w++
   461  		}
   462  	}
   463  	f.Require = f.Require[:w]
   464  
   465  	w = 0
   466  	for _, x := range f.Exclude {
   467  		if x.Mod.Path != "" {
   468  			f.Exclude[w] = x
   469  			w++
   470  		}
   471  	}
   472  	f.Exclude = f.Exclude[:w]
   473  
   474  	w = 0
   475  	for _, r := range f.Replace {
   476  		if r.Old.Path != "" {
   477  			f.Replace[w] = r
   478  			w++
   479  		}
   480  	}
   481  	f.Replace = f.Replace[:w]
   482  
   483  	f.Syntax.Cleanup()
   484  }
   485  
   486  func IsValidExport(name string) bool {
   487  	return name != ""
   488  }
   489  
   490  func (f *File) AddExportsStmt(name string) error {
   491  	if !IsValidExport(name) {
   492  		return fmt.Errorf("invalid module  exports string %q", name)
   493  	}
   494  	if f.Exports == nil {
   495  		f.Exports = &Exports{
   496  			Name:   name,
   497  			Syntax: f.Syntax.addLine(nil, "exports", name),
   498  		}
   499  	} else {
   500  		f.Exports.Name = name
   501  		f.Syntax.updateLine(f.Exports.Syntax, "exports", name)
   502  	}
   503  	return nil
   504  }
   505  
   506  func (f *File) AddRequire(path, vers string) error {
   507  	need := true
   508  	for _, r := range f.Require {
   509  		if r.Mod.Path == path {
   510  			if need {
   511  				r.Mod.Version = vers
   512  				f.Syntax.updateLine(r.Syntax, "require", AutoQuote(path), vers)
   513  				need = false
   514  			} else {
   515  				f.Syntax.removeLine(r.Syntax)
   516  				*r = Require{}
   517  			}
   518  		}
   519  	}
   520  
   521  	if need {
   522  		f.AddNewRequire(path, vers, false)
   523  	}
   524  	return nil
   525  }
   526  
   527  func (f *File) AddNewRequire(path, vers string, indirect bool) {
   528  	line := f.Syntax.addLine(nil, "require", AutoQuote(path), vers)
   529  	setIndirect(line, indirect)
   530  	f.Require = append(f.Require, &Require{module.Version{Path: path, Version: vers}, indirect, line})
   531  }
   532  
   533  func (f *File) SetRequire(req []*Require) {
   534  	need := make(map[string]string)
   535  	indirect := make(map[string]bool)
   536  	for _, r := range req {
   537  		need[r.Mod.Path] = r.Mod.Version
   538  		indirect[r.Mod.Path] = r.Indirect
   539  	}
   540  
   541  	for _, r := range f.Require {
   542  		if v, ok := need[r.Mod.Path]; ok {
   543  			r.Mod.Version = v
   544  			r.Indirect = indirect[r.Mod.Path]
   545  		}
   546  	}
   547  
   548  	var newStmts []Expr
   549  	for _, stmt := range f.Syntax.Stmt {
   550  		switch stmt := stmt.(type) {
   551  		case *LineBlock:
   552  			if len(stmt.Token) > 0 && stmt.Token[0] == "require" {
   553  				var newLines []*Line
   554  				for _, line := range stmt.Line {
   555  					if p, err := parseString(&line.Token[0]); err == nil && need[p] != "" {
   556  						line.Token[1] = need[p]
   557  						delete(need, p)
   558  						setIndirect(line, indirect[p])
   559  						newLines = append(newLines, line)
   560  					}
   561  				}
   562  				if len(newLines) == 0 {
   563  					continue // drop stmt
   564  				}
   565  				stmt.Line = newLines
   566  			}
   567  
   568  		case *Line:
   569  			if len(stmt.Token) > 0 && stmt.Token[0] == "require" {
   570  				if p, err := parseString(&stmt.Token[1]); err == nil && need[p] != "" {
   571  					stmt.Token[2] = need[p]
   572  					delete(need, p)
   573  					setIndirect(stmt, indirect[p])
   574  				} else {
   575  					continue // drop stmt
   576  				}
   577  			}
   578  		}
   579  		newStmts = append(newStmts, stmt)
   580  	}
   581  	f.Syntax.Stmt = newStmts
   582  
   583  	for path, vers := range need {
   584  		f.AddNewRequire(path, vers, indirect[path])
   585  	}
   586  	f.SortBlocks()
   587  }
   588  
   589  func (f *File) DropRequire(path string) error {
   590  	for _, r := range f.Require {
   591  		if r.Mod.Path == path {
   592  			f.Syntax.removeLine(r.Syntax)
   593  			*r = Require{}
   594  		}
   595  	}
   596  	return nil
   597  }
   598  
   599  func (f *File) AddExclude(path, vers string) error {
   600  	var hint *Line
   601  	for _, x := range f.Exclude {
   602  		if x.Mod.Path == path && x.Mod.Version == vers {
   603  			return nil
   604  		}
   605  		if x.Mod.Path == path {
   606  			hint = x.Syntax
   607  		}
   608  	}
   609  
   610  	f.Exclude = append(f.Exclude, &Exclude{Mod: module.Version{Path: path, Version: vers}, Syntax: f.Syntax.addLine(hint, "exclude", AutoQuote(path), vers)})
   611  	return nil
   612  }
   613  
   614  func (f *File) DropExclude(path, vers string) error {
   615  	for _, x := range f.Exclude {
   616  		if x.Mod.Path == path && x.Mod.Version == vers {
   617  			f.Syntax.removeLine(x.Syntax)
   618  			*x = Exclude{}
   619  		}
   620  	}
   621  	return nil
   622  }
   623  
   624  func (f *File) AddReplace(oldPath, oldVers, newPath, newVers string) error {
   625  	need := true
   626  	old := module.Version{Path: oldPath, Version: oldVers}
   627  	new := module.Version{Path: newPath, Version: newVers}
   628  	tokens := []string{"replace", AutoQuote(oldPath)}
   629  	if oldVers != "" {
   630  		tokens = append(tokens, oldVers)
   631  	}
   632  	tokens = append(tokens, "=>", AutoQuote(newPath))
   633  	if newVers != "" {
   634  		tokens = append(tokens, newVers)
   635  	}
   636  
   637  	var hint *Line
   638  	for _, r := range f.Replace {
   639  		if r.Old.Path == oldPath && (oldVers == "" || r.Old.Version == oldVers) {
   640  			if need {
   641  				// Found replacement for old; update to use new.
   642  				r.New = new
   643  				f.Syntax.updateLine(r.Syntax, tokens...)
   644  				need = false
   645  				continue
   646  			}
   647  			// Already added; delete other replacements for same.
   648  			f.Syntax.removeLine(r.Syntax)
   649  			*r = Replace{}
   650  		}
   651  		if r.Old.Path == oldPath {
   652  			hint = r.Syntax
   653  		}
   654  	}
   655  	if need {
   656  		f.Replace = append(f.Replace, &Replace{Old: old, New: new, Syntax: f.Syntax.addLine(hint, tokens...)})
   657  	}
   658  	return nil
   659  }
   660  
   661  func (f *File) DropReplace(oldPath, oldVers string) error {
   662  	for _, r := range f.Replace {
   663  		if r.Old.Path == oldPath && r.Old.Version == oldVers {
   664  			f.Syntax.removeLine(r.Syntax)
   665  			*r = Replace{}
   666  		}
   667  	}
   668  	return nil
   669  }
   670  
   671  func (f *File) SortBlocks() {
   672  	f.removeDups() // otherwise sorting is unsafe
   673  
   674  	for _, stmt := range f.Syntax.Stmt {
   675  		block, ok := stmt.(*LineBlock)
   676  		if !ok {
   677  			continue
   678  		}
   679  		sort.Slice(block.Line, func(i, j int) bool {
   680  			li := block.Line[i]
   681  			lj := block.Line[j]
   682  			for k := 0; k < len(li.Token) && k < len(lj.Token); k++ {
   683  				if li.Token[k] != lj.Token[k] {
   684  					return li.Token[k] < lj.Token[k]
   685  				}
   686  			}
   687  			return len(li.Token) < len(lj.Token)
   688  		})
   689  	}
   690  }
   691  
   692  func (f *File) removeDups() {
   693  	have := make(map[module.Version]bool)
   694  	kill := make(map[*Line]bool)
   695  	for _, x := range f.Exclude {
   696  		if have[x.Mod] {
   697  			kill[x.Syntax] = true
   698  			continue
   699  		}
   700  		have[x.Mod] = true
   701  	}
   702  	var excl []*Exclude
   703  	for _, x := range f.Exclude {
   704  		if !kill[x.Syntax] {
   705  			excl = append(excl, x)
   706  		}
   707  	}
   708  	f.Exclude = excl
   709  
   710  	have = make(map[module.Version]bool)
   711  	// Later replacements take priority over earlier ones.
   712  	for i := len(f.Replace) - 1; i >= 0; i-- {
   713  		x := f.Replace[i]
   714  		if have[x.Old] {
   715  			kill[x.Syntax] = true
   716  			continue
   717  		}
   718  		have[x.Old] = true
   719  	}
   720  	var repl []*Replace
   721  	for _, x := range f.Replace {
   722  		if !kill[x.Syntax] {
   723  			repl = append(repl, x)
   724  		}
   725  	}
   726  	f.Replace = repl
   727  
   728  	var stmts []Expr
   729  	for _, stmt := range f.Syntax.Stmt {
   730  		switch stmt := stmt.(type) {
   731  		case *Line:
   732  			if kill[stmt] {
   733  				continue
   734  			}
   735  		case *LineBlock:
   736  			var lines []*Line
   737  			for _, line := range stmt.Line {
   738  				if !kill[line] {
   739  					lines = append(lines, line)
   740  				}
   741  			}
   742  			stmt.Line = lines
   743  			if len(lines) == 0 {
   744  				continue
   745  			}
   746  		}
   747  		stmts = append(stmts, stmt)
   748  	}
   749  	f.Syntax.Stmt = stmts
   750  }