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