 9bdcbe0447
			
		
	
	9bdcbe0447
	
	
	
		
			
			Major integrations and fixes: - Added BACKBEAT SDK integration for P2P operation timing - Implemented beat-aware status tracking for distributed operations - Added Docker secrets support for secure license management - Resolved KACHING license validation via HTTPS/TLS - Updated docker-compose configuration for clean stack deployment - Disabled rollback policies to prevent deployment failures - Added license credential storage (CHORUS-DEV-MULTI-001) Technical improvements: - BACKBEAT P2P operation tracking with phase management - Enhanced configuration system with file-based secrets - Improved error handling for license validation - Clean separation of KACHING and CHORUS deployment stacks 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
		
			
				
	
	
		
			1767 lines
		
	
	
		
			46 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			1767 lines
		
	
	
		
			46 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright 2018 The Go Authors. All rights reserved.
 | |
| // Use of this source code is governed by a BSD-style
 | |
| // license that can be found in the LICENSE file.
 | |
| 
 | |
| // Package modfile implements a parser and formatter for go.mod files.
 | |
| //
 | |
| // The go.mod syntax is described in
 | |
| // https://pkg.go.dev/cmd/go/#hdr-The_go_mod_file.
 | |
| //
 | |
| // The [Parse] and [ParseLax] functions both parse a go.mod file and return an
 | |
| // abstract syntax tree. ParseLax ignores unknown statements and may be used to
 | |
| // parse go.mod files that may have been developed with newer versions of Go.
 | |
| //
 | |
| // The [File] struct returned by Parse and ParseLax represent an abstract
 | |
| // go.mod file. File has several methods like [File.AddNewRequire] and
 | |
| // [File.DropReplace] that can be used to programmatically edit a file.
 | |
| //
 | |
| // The [Format] function formats a File back to a byte slice which can be
 | |
| // written to a file.
 | |
| package modfile
 | |
| 
 | |
| import (
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"path/filepath"
 | |
| 	"sort"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 	"unicode"
 | |
| 
 | |
| 	"golang.org/x/mod/internal/lazyregexp"
 | |
| 	"golang.org/x/mod/module"
 | |
| 	"golang.org/x/mod/semver"
 | |
| )
 | |
| 
 | |
| // A File is the parsed, interpreted form of a go.mod file.
 | |
| type File struct {
 | |
| 	Module    *Module
 | |
| 	Go        *Go
 | |
| 	Toolchain *Toolchain
 | |
| 	Godebug   []*Godebug
 | |
| 	Require   []*Require
 | |
| 	Exclude   []*Exclude
 | |
| 	Replace   []*Replace
 | |
| 	Retract   []*Retract
 | |
| 
 | |
| 	Syntax *FileSyntax
 | |
| }
 | |
| 
 | |
| // A Module is the module statement.
 | |
| type Module struct {
 | |
| 	Mod        module.Version
 | |
| 	Deprecated string
 | |
| 	Syntax     *Line
 | |
| }
 | |
| 
 | |
| // A Go is the go statement.
 | |
| type Go struct {
 | |
| 	Version string // "1.23"
 | |
| 	Syntax  *Line
 | |
| }
 | |
| 
 | |
| // A Toolchain is the toolchain statement.
 | |
| type Toolchain struct {
 | |
| 	Name   string // "go1.21rc1"
 | |
| 	Syntax *Line
 | |
| }
 | |
| 
 | |
| // A Godebug is a single godebug key=value statement.
 | |
| type Godebug struct {
 | |
| 	Key    string
 | |
| 	Value  string
 | |
| 	Syntax *Line
 | |
| }
 | |
| 
 | |
| // An Exclude is a single exclude statement.
 | |
| type Exclude struct {
 | |
| 	Mod    module.Version
 | |
| 	Syntax *Line
 | |
| }
 | |
| 
 | |
| // A Replace is a single replace statement.
 | |
| type Replace struct {
 | |
| 	Old    module.Version
 | |
| 	New    module.Version
 | |
| 	Syntax *Line
 | |
| }
 | |
| 
 | |
| // A Retract is a single retract statement.
 | |
| type Retract struct {
 | |
| 	VersionInterval
 | |
| 	Rationale string
 | |
| 	Syntax    *Line
 | |
| }
 | |
| 
 | |
| // A VersionInterval represents a range of versions with upper and lower bounds.
 | |
| // Intervals are closed: both bounds are included. When Low is equal to High,
 | |
| // the interval may refer to a single version ('v1.2.3') or an interval
 | |
| // ('[v1.2.3, v1.2.3]'); both have the same representation.
 | |
| type VersionInterval struct {
 | |
| 	Low, High string
 | |
| }
 | |
| 
 | |
| // A Require is a single require statement.
 | |
| type Require struct {
 | |
| 	Mod      module.Version
 | |
| 	Indirect bool // has "// indirect" comment
 | |
| 	Syntax   *Line
 | |
| }
 | |
| 
 | |
| func (r *Require) markRemoved() {
 | |
| 	r.Syntax.markRemoved()
 | |
| 	*r = Require{}
 | |
| }
 | |
| 
 | |
| func (r *Require) setVersion(v string) {
 | |
| 	r.Mod.Version = v
 | |
| 
 | |
| 	if line := r.Syntax; len(line.Token) > 0 {
 | |
| 		if line.InBlock {
 | |
| 			// If the line is preceded by an empty line, remove it; see
 | |
| 			// https://golang.org/issue/33779.
 | |
| 			if len(line.Comments.Before) == 1 && len(line.Comments.Before[0].Token) == 0 {
 | |
| 				line.Comments.Before = line.Comments.Before[:0]
 | |
| 			}
 | |
| 			if len(line.Token) >= 2 { // example.com v1.2.3
 | |
| 				line.Token[1] = v
 | |
| 			}
 | |
| 		} else {
 | |
| 			if len(line.Token) >= 3 { // require example.com v1.2.3
 | |
| 				line.Token[2] = v
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // setIndirect sets line to have (or not have) a "// indirect" comment.
 | |
| func (r *Require) setIndirect(indirect bool) {
 | |
| 	r.Indirect = indirect
 | |
| 	line := r.Syntax
 | |
| 	if isIndirect(line) == indirect {
 | |
| 		return
 | |
| 	}
 | |
| 	if indirect {
 | |
| 		// Adding comment.
 | |
| 		if len(line.Suffix) == 0 {
 | |
| 			// New comment.
 | |
| 			line.Suffix = []Comment{{Token: "// indirect", Suffix: true}}
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		com := &line.Suffix[0]
 | |
| 		text := strings.TrimSpace(strings.TrimPrefix(com.Token, string(slashSlash)))
 | |
| 		if text == "" {
 | |
| 			// Empty comment.
 | |
| 			com.Token = "// indirect"
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		// Insert at beginning of existing comment.
 | |
| 		com.Token = "// indirect; " + text
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Removing comment.
 | |
| 	f := strings.TrimSpace(strings.TrimPrefix(line.Suffix[0].Token, string(slashSlash)))
 | |
| 	if f == "indirect" {
 | |
| 		// Remove whole comment.
 | |
| 		line.Suffix = nil
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Remove comment prefix.
 | |
| 	com := &line.Suffix[0]
 | |
| 	i := strings.Index(com.Token, "indirect;")
 | |
| 	com.Token = "//" + com.Token[i+len("indirect;"):]
 | |
| }
 | |
| 
 | |
| // isIndirect reports whether line has a "// indirect" comment,
 | |
| // meaning it is in go.mod only for its effect on indirect dependencies,
 | |
| // so that it can be dropped entirely once the effective version of the
 | |
| // indirect dependency reaches the given minimum version.
 | |
| func isIndirect(line *Line) bool {
 | |
| 	if len(line.Suffix) == 0 {
 | |
| 		return false
 | |
| 	}
 | |
| 	f := strings.Fields(strings.TrimPrefix(line.Suffix[0].Token, string(slashSlash)))
 | |
| 	return (len(f) == 1 && f[0] == "indirect" || len(f) > 1 && f[0] == "indirect;")
 | |
| }
 | |
| 
 | |
| func (f *File) AddModuleStmt(path string) error {
 | |
| 	if f.Syntax == nil {
 | |
| 		f.Syntax = new(FileSyntax)
 | |
| 	}
 | |
| 	if f.Module == nil {
 | |
| 		f.Module = &Module{
 | |
| 			Mod:    module.Version{Path: path},
 | |
| 			Syntax: f.Syntax.addLine(nil, "module", AutoQuote(path)),
 | |
| 		}
 | |
| 	} else {
 | |
| 		f.Module.Mod.Path = path
 | |
| 		f.Syntax.updateLine(f.Module.Syntax, "module", AutoQuote(path))
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (f *File) AddComment(text string) {
 | |
| 	if f.Syntax == nil {
 | |
| 		f.Syntax = new(FileSyntax)
 | |
| 	}
 | |
| 	f.Syntax.Stmt = append(f.Syntax.Stmt, &CommentBlock{
 | |
| 		Comments: Comments{
 | |
| 			Before: []Comment{
 | |
| 				{
 | |
| 					Token: text,
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 	})
 | |
| }
 | |
| 
 | |
| type VersionFixer func(path, version string) (string, error)
 | |
| 
 | |
| // errDontFix is returned by a VersionFixer to indicate the version should be
 | |
| // left alone, even if it's not canonical.
 | |
| var dontFixRetract VersionFixer = func(_, vers string) (string, error) {
 | |
| 	return vers, nil
 | |
| }
 | |
| 
 | |
| // Parse parses and returns a go.mod file.
 | |
| //
 | |
| // file is the name of the file, used in positions and errors.
 | |
| //
 | |
| // data is the content of the file.
 | |
| //
 | |
| // fix is an optional function that canonicalizes module versions.
 | |
| // If fix is nil, all module versions must be canonical ([module.CanonicalVersion]
 | |
| // must return the same string).
 | |
| func Parse(file string, data []byte, fix VersionFixer) (*File, error) {
 | |
| 	return parseToFile(file, data, fix, true)
 | |
| }
 | |
| 
 | |
| // ParseLax is like Parse but ignores unknown statements.
 | |
| // It is used when parsing go.mod files other than the main module,
 | |
| // under the theory that most statement types we add in the future will
 | |
| // only apply in the main module, like exclude and replace,
 | |
| // and so we get better gradual deployments if old go commands
 | |
| // simply ignore those statements when found in go.mod files
 | |
| // in dependencies.
 | |
| func ParseLax(file string, data []byte, fix VersionFixer) (*File, error) {
 | |
| 	return parseToFile(file, data, fix, false)
 | |
| }
 | |
| 
 | |
| func parseToFile(file string, data []byte, fix VersionFixer, strict bool) (parsed *File, err error) {
 | |
| 	fs, err := parse(file, data)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	f := &File{
 | |
| 		Syntax: fs,
 | |
| 	}
 | |
| 	var errs ErrorList
 | |
| 
 | |
| 	// fix versions in retract directives after the file is parsed.
 | |
| 	// We need the module path to fix versions, and it might be at the end.
 | |
| 	defer func() {
 | |
| 		oldLen := len(errs)
 | |
| 		f.fixRetract(fix, &errs)
 | |
| 		if len(errs) > oldLen {
 | |
| 			parsed, err = nil, errs
 | |
| 		}
 | |
| 	}()
 | |
| 
 | |
| 	for _, x := range fs.Stmt {
 | |
| 		switch x := x.(type) {
 | |
| 		case *Line:
 | |
| 			f.add(&errs, nil, x, x.Token[0], x.Token[1:], fix, strict)
 | |
| 
 | |
| 		case *LineBlock:
 | |
| 			if len(x.Token) > 1 {
 | |
| 				if strict {
 | |
| 					errs = append(errs, Error{
 | |
| 						Filename: file,
 | |
| 						Pos:      x.Start,
 | |
| 						Err:      fmt.Errorf("unknown block type: %s", strings.Join(x.Token, " ")),
 | |
| 					})
 | |
| 				}
 | |
| 				continue
 | |
| 			}
 | |
| 			switch x.Token[0] {
 | |
| 			default:
 | |
| 				if strict {
 | |
| 					errs = append(errs, Error{
 | |
| 						Filename: file,
 | |
| 						Pos:      x.Start,
 | |
| 						Err:      fmt.Errorf("unknown block type: %s", strings.Join(x.Token, " ")),
 | |
| 					})
 | |
| 				}
 | |
| 				continue
 | |
| 			case "module", "godebug", "require", "exclude", "replace", "retract":
 | |
| 				for _, l := range x.Line {
 | |
| 					f.add(&errs, x, l, x.Token[0], l.Token, fix, strict)
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if len(errs) > 0 {
 | |
| 		return nil, errs
 | |
| 	}
 | |
| 	return f, nil
 | |
| }
 | |
| 
 | |
| var GoVersionRE = lazyregexp.New(`^([1-9][0-9]*)\.(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))?([a-z]+[0-9]+)?$`)
 | |
| var laxGoVersionRE = lazyregexp.New(`^v?(([1-9][0-9]*)\.(0|[1-9][0-9]*))([^0-9].*)$`)
 | |
| 
 | |
| // Toolchains must be named beginning with `go1`,
 | |
| // like "go1.20.3" or "go1.20.3-gccgo". As a special case, "default" is also permitted.
 | |
| // Note that this regexp is a much looser condition than go/version.IsValid,
 | |
| // for forward compatibility.
 | |
| // (This code has to be work to identify new toolchains even if we tweak the syntax in the future.)
 | |
| var ToolchainRE = lazyregexp.New(`^default$|^go1($|\.)`)
 | |
| 
 | |
| func (f *File) add(errs *ErrorList, block *LineBlock, line *Line, verb string, args []string, fix VersionFixer, strict bool) {
 | |
| 	// If strict is false, this module is a dependency.
 | |
| 	// We ignore all unknown directives as well as main-module-only
 | |
| 	// directives like replace and exclude. It will work better for
 | |
| 	// forward compatibility if we can depend on modules that have unknown
 | |
| 	// statements (presumed relevant only when acting as the main module)
 | |
| 	// and simply ignore those statements.
 | |
| 	if !strict {
 | |
| 		switch verb {
 | |
| 		case "go", "module", "retract", "require":
 | |
| 			// want these even for dependency go.mods
 | |
| 		default:
 | |
| 			return
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	wrapModPathError := func(modPath string, err error) {
 | |
| 		*errs = append(*errs, Error{
 | |
| 			Filename: f.Syntax.Name,
 | |
| 			Pos:      line.Start,
 | |
| 			ModPath:  modPath,
 | |
| 			Verb:     verb,
 | |
| 			Err:      err,
 | |
| 		})
 | |
| 	}
 | |
| 	wrapError := func(err error) {
 | |
| 		*errs = append(*errs, Error{
 | |
| 			Filename: f.Syntax.Name,
 | |
| 			Pos:      line.Start,
 | |
| 			Err:      err,
 | |
| 		})
 | |
| 	}
 | |
| 	errorf := func(format string, args ...interface{}) {
 | |
| 		wrapError(fmt.Errorf(format, args...))
 | |
| 	}
 | |
| 
 | |
| 	switch verb {
 | |
| 	default:
 | |
| 		errorf("unknown directive: %s", verb)
 | |
| 
 | |
| 	case "go":
 | |
| 		if f.Go != nil {
 | |
| 			errorf("repeated go statement")
 | |
| 			return
 | |
| 		}
 | |
| 		if len(args) != 1 {
 | |
| 			errorf("go directive expects exactly one argument")
 | |
| 			return
 | |
| 		} else if !GoVersionRE.MatchString(args[0]) {
 | |
| 			fixed := false
 | |
| 			if !strict {
 | |
| 				if m := laxGoVersionRE.FindStringSubmatch(args[0]); m != nil {
 | |
| 					args[0] = m[1]
 | |
| 					fixed = true
 | |
| 				}
 | |
| 			}
 | |
| 			if !fixed {
 | |
| 				errorf("invalid go version '%s': must match format 1.23.0", args[0])
 | |
| 				return
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		f.Go = &Go{Syntax: line}
 | |
| 		f.Go.Version = args[0]
 | |
| 
 | |
| 	case "toolchain":
 | |
| 		if f.Toolchain != nil {
 | |
| 			errorf("repeated toolchain statement")
 | |
| 			return
 | |
| 		}
 | |
| 		if len(args) != 1 {
 | |
| 			errorf("toolchain directive expects exactly one argument")
 | |
| 			return
 | |
| 		} else if !ToolchainRE.MatchString(args[0]) {
 | |
| 			errorf("invalid toolchain version '%s': must match format go1.23.0 or default", args[0])
 | |
| 			return
 | |
| 		}
 | |
| 		f.Toolchain = &Toolchain{Syntax: line}
 | |
| 		f.Toolchain.Name = args[0]
 | |
| 
 | |
| 	case "module":
 | |
| 		if f.Module != nil {
 | |
| 			errorf("repeated module statement")
 | |
| 			return
 | |
| 		}
 | |
| 		deprecated := parseDeprecation(block, line)
 | |
| 		f.Module = &Module{
 | |
| 			Syntax:     line,
 | |
| 			Deprecated: deprecated,
 | |
| 		}
 | |
| 		if len(args) != 1 {
 | |
| 			errorf("usage: module module/path")
 | |
| 			return
 | |
| 		}
 | |
| 		s, err := parseString(&args[0])
 | |
| 		if err != nil {
 | |
| 			errorf("invalid quoted string: %v", err)
 | |
| 			return
 | |
| 		}
 | |
| 		f.Module.Mod = module.Version{Path: s}
 | |
| 
 | |
| 	case "godebug":
 | |
| 		if len(args) != 1 || strings.ContainsAny(args[0], "\"`',") {
 | |
| 			errorf("usage: godebug key=value")
 | |
| 			return
 | |
| 		}
 | |
| 		key, value, ok := strings.Cut(args[0], "=")
 | |
| 		if !ok {
 | |
| 			errorf("usage: godebug key=value")
 | |
| 			return
 | |
| 		}
 | |
| 		f.Godebug = append(f.Godebug, &Godebug{
 | |
| 			Key:    key,
 | |
| 			Value:  value,
 | |
| 			Syntax: line,
 | |
| 		})
 | |
| 
 | |
| 	case "require", "exclude":
 | |
| 		if len(args) != 2 {
 | |
| 			errorf("usage: %s module/path v1.2.3", verb)
 | |
| 			return
 | |
| 		}
 | |
| 		s, err := parseString(&args[0])
 | |
| 		if err != nil {
 | |
| 			errorf("invalid quoted string: %v", err)
 | |
| 			return
 | |
| 		}
 | |
| 		v, err := parseVersion(verb, s, &args[1], fix)
 | |
| 		if err != nil {
 | |
| 			wrapError(err)
 | |
| 			return
 | |
| 		}
 | |
| 		pathMajor, err := modulePathMajor(s)
 | |
| 		if err != nil {
 | |
| 			wrapError(err)
 | |
| 			return
 | |
| 		}
 | |
| 		if err := module.CheckPathMajor(v, pathMajor); err != nil {
 | |
| 			wrapModPathError(s, err)
 | |
| 			return
 | |
| 		}
 | |
| 		if verb == "require" {
 | |
| 			f.Require = append(f.Require, &Require{
 | |
| 				Mod:      module.Version{Path: s, Version: v},
 | |
| 				Syntax:   line,
 | |
| 				Indirect: isIndirect(line),
 | |
| 			})
 | |
| 		} else {
 | |
| 			f.Exclude = append(f.Exclude, &Exclude{
 | |
| 				Mod:    module.Version{Path: s, Version: v},
 | |
| 				Syntax: line,
 | |
| 			})
 | |
| 		}
 | |
| 
 | |
| 	case "replace":
 | |
| 		replace, wrappederr := parseReplace(f.Syntax.Name, line, verb, args, fix)
 | |
| 		if wrappederr != nil {
 | |
| 			*errs = append(*errs, *wrappederr)
 | |
| 			return
 | |
| 		}
 | |
| 		f.Replace = append(f.Replace, replace)
 | |
| 
 | |
| 	case "retract":
 | |
| 		rationale := parseDirectiveComment(block, line)
 | |
| 		vi, err := parseVersionInterval(verb, "", &args, dontFixRetract)
 | |
| 		if err != nil {
 | |
| 			if strict {
 | |
| 				wrapError(err)
 | |
| 				return
 | |
| 			} else {
 | |
| 				// Only report errors parsing intervals in the main module. We may
 | |
| 				// support additional syntax in the future, such as open and half-open
 | |
| 				// intervals. Those can't be supported now, because they break the
 | |
| 				// go.mod parser, even in lax mode.
 | |
| 				return
 | |
| 			}
 | |
| 		}
 | |
| 		if len(args) > 0 && strict {
 | |
| 			// In the future, there may be additional information after the version.
 | |
| 			errorf("unexpected token after version: %q", args[0])
 | |
| 			return
 | |
| 		}
 | |
| 		retract := &Retract{
 | |
| 			VersionInterval: vi,
 | |
| 			Rationale:       rationale,
 | |
| 			Syntax:          line,
 | |
| 		}
 | |
| 		f.Retract = append(f.Retract, retract)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func parseReplace(filename string, line *Line, verb string, args []string, fix VersionFixer) (*Replace, *Error) {
 | |
| 	wrapModPathError := func(modPath string, err error) *Error {
 | |
| 		return &Error{
 | |
| 			Filename: filename,
 | |
| 			Pos:      line.Start,
 | |
| 			ModPath:  modPath,
 | |
| 			Verb:     verb,
 | |
| 			Err:      err,
 | |
| 		}
 | |
| 	}
 | |
| 	wrapError := func(err error) *Error {
 | |
| 		return &Error{
 | |
| 			Filename: filename,
 | |
| 			Pos:      line.Start,
 | |
| 			Err:      err,
 | |
| 		}
 | |
| 	}
 | |
| 	errorf := func(format string, args ...interface{}) *Error {
 | |
| 		return wrapError(fmt.Errorf(format, args...))
 | |
| 	}
 | |
| 
 | |
| 	arrow := 2
 | |
| 	if len(args) >= 2 && args[1] == "=>" {
 | |
| 		arrow = 1
 | |
| 	}
 | |
| 	if len(args) < arrow+2 || len(args) > arrow+3 || args[arrow] != "=>" {
 | |
| 		return nil, errorf("usage: %s module/path [v1.2.3] => other/module v1.4\n\t or %s module/path [v1.2.3] => ../local/directory", verb, verb)
 | |
| 	}
 | |
| 	s, err := parseString(&args[0])
 | |
| 	if err != nil {
 | |
| 		return nil, errorf("invalid quoted string: %v", err)
 | |
| 	}
 | |
| 	pathMajor, err := modulePathMajor(s)
 | |
| 	if err != nil {
 | |
| 		return nil, wrapModPathError(s, err)
 | |
| 
 | |
| 	}
 | |
| 	var v string
 | |
| 	if arrow == 2 {
 | |
| 		v, err = parseVersion(verb, s, &args[1], fix)
 | |
| 		if err != nil {
 | |
| 			return nil, wrapError(err)
 | |
| 		}
 | |
| 		if err := module.CheckPathMajor(v, pathMajor); err != nil {
 | |
| 			return nil, wrapModPathError(s, err)
 | |
| 		}
 | |
| 	}
 | |
| 	ns, err := parseString(&args[arrow+1])
 | |
| 	if err != nil {
 | |
| 		return nil, errorf("invalid quoted string: %v", err)
 | |
| 	}
 | |
| 	nv := ""
 | |
| 	if len(args) == arrow+2 {
 | |
| 		if !IsDirectoryPath(ns) {
 | |
| 			if strings.Contains(ns, "@") {
 | |
| 				return nil, errorf("replacement module must match format 'path version', not 'path@version'")
 | |
| 			}
 | |
| 			return nil, errorf("replacement module without version must be directory path (rooted or starting with . or ..)")
 | |
| 		}
 | |
| 		if filepath.Separator == '/' && strings.Contains(ns, `\`) {
 | |
| 			return nil, errorf("replacement directory appears to be Windows path (on a non-windows system)")
 | |
| 		}
 | |
| 	}
 | |
| 	if len(args) == arrow+3 {
 | |
| 		nv, err = parseVersion(verb, ns, &args[arrow+2], fix)
 | |
| 		if err != nil {
 | |
| 			return nil, wrapError(err)
 | |
| 		}
 | |
| 		if IsDirectoryPath(ns) {
 | |
| 			return nil, errorf("replacement module directory path %q cannot have version", ns)
 | |
| 		}
 | |
| 	}
 | |
| 	return &Replace{
 | |
| 		Old:    module.Version{Path: s, Version: v},
 | |
| 		New:    module.Version{Path: ns, Version: nv},
 | |
| 		Syntax: line,
 | |
| 	}, nil
 | |
| }
 | |
| 
 | |
| // fixRetract applies fix to each retract directive in f, appending any errors
 | |
| // to errs.
 | |
| //
 | |
| // Most versions are fixed as we parse the file, but for retract directives,
 | |
| // the relevant module path is the one specified with the module directive,
 | |
| // and that might appear at the end of the file (or not at all).
 | |
| func (f *File) fixRetract(fix VersionFixer, errs *ErrorList) {
 | |
| 	if fix == nil {
 | |
| 		return
 | |
| 	}
 | |
| 	path := ""
 | |
| 	if f.Module != nil {
 | |
| 		path = f.Module.Mod.Path
 | |
| 	}
 | |
| 	var r *Retract
 | |
| 	wrapError := func(err error) {
 | |
| 		*errs = append(*errs, Error{
 | |
| 			Filename: f.Syntax.Name,
 | |
| 			Pos:      r.Syntax.Start,
 | |
| 			Err:      err,
 | |
| 		})
 | |
| 	}
 | |
| 
 | |
| 	for _, r = range f.Retract {
 | |
| 		if path == "" {
 | |
| 			wrapError(errors.New("no module directive found, so retract cannot be used"))
 | |
| 			return // only print the first one of these
 | |
| 		}
 | |
| 
 | |
| 		args := r.Syntax.Token
 | |
| 		if args[0] == "retract" {
 | |
| 			args = args[1:]
 | |
| 		}
 | |
| 		vi, err := parseVersionInterval("retract", path, &args, fix)
 | |
| 		if err != nil {
 | |
| 			wrapError(err)
 | |
| 		}
 | |
| 		r.VersionInterval = vi
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (f *WorkFile) add(errs *ErrorList, line *Line, verb string, args []string, fix VersionFixer) {
 | |
| 	wrapError := func(err error) {
 | |
| 		*errs = append(*errs, Error{
 | |
| 			Filename: f.Syntax.Name,
 | |
| 			Pos:      line.Start,
 | |
| 			Err:      err,
 | |
| 		})
 | |
| 	}
 | |
| 	errorf := func(format string, args ...interface{}) {
 | |
| 		wrapError(fmt.Errorf(format, args...))
 | |
| 	}
 | |
| 
 | |
| 	switch verb {
 | |
| 	default:
 | |
| 		errorf("unknown directive: %s", verb)
 | |
| 
 | |
| 	case "go":
 | |
| 		if f.Go != nil {
 | |
| 			errorf("repeated go statement")
 | |
| 			return
 | |
| 		}
 | |
| 		if len(args) != 1 {
 | |
| 			errorf("go directive expects exactly one argument")
 | |
| 			return
 | |
| 		} else if !GoVersionRE.MatchString(args[0]) {
 | |
| 			errorf("invalid go version '%s': must match format 1.23.0", args[0])
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		f.Go = &Go{Syntax: line}
 | |
| 		f.Go.Version = args[0]
 | |
| 
 | |
| 	case "toolchain":
 | |
| 		if f.Toolchain != nil {
 | |
| 			errorf("repeated toolchain statement")
 | |
| 			return
 | |
| 		}
 | |
| 		if len(args) != 1 {
 | |
| 			errorf("toolchain directive expects exactly one argument")
 | |
| 			return
 | |
| 		} else if !ToolchainRE.MatchString(args[0]) {
 | |
| 			errorf("invalid toolchain version '%s': must match format go1.23.0 or default", args[0])
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		f.Toolchain = &Toolchain{Syntax: line}
 | |
| 		f.Toolchain.Name = args[0]
 | |
| 
 | |
| 	case "godebug":
 | |
| 		if len(args) != 1 || strings.ContainsAny(args[0], "\"`',") {
 | |
| 			errorf("usage: godebug key=value")
 | |
| 			return
 | |
| 		}
 | |
| 		key, value, ok := strings.Cut(args[0], "=")
 | |
| 		if !ok {
 | |
| 			errorf("usage: godebug key=value")
 | |
| 			return
 | |
| 		}
 | |
| 		f.Godebug = append(f.Godebug, &Godebug{
 | |
| 			Key:    key,
 | |
| 			Value:  value,
 | |
| 			Syntax: line,
 | |
| 		})
 | |
| 
 | |
| 	case "use":
 | |
| 		if len(args) != 1 {
 | |
| 			errorf("usage: %s local/dir", verb)
 | |
| 			return
 | |
| 		}
 | |
| 		s, err := parseString(&args[0])
 | |
| 		if err != nil {
 | |
| 			errorf("invalid quoted string: %v", err)
 | |
| 			return
 | |
| 		}
 | |
| 		f.Use = append(f.Use, &Use{
 | |
| 			Path:   s,
 | |
| 			Syntax: line,
 | |
| 		})
 | |
| 
 | |
| 	case "replace":
 | |
| 		replace, wrappederr := parseReplace(f.Syntax.Name, line, verb, args, fix)
 | |
| 		if wrappederr != nil {
 | |
| 			*errs = append(*errs, *wrappederr)
 | |
| 			return
 | |
| 		}
 | |
| 		f.Replace = append(f.Replace, replace)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // IsDirectoryPath reports whether the given path should be interpreted as a directory path.
 | |
| // Just like on the go command line, relative paths starting with a '.' or '..' path component
 | |
| // and rooted paths are directory paths; the rest are module paths.
 | |
| func IsDirectoryPath(ns string) bool {
 | |
| 	// Because go.mod files can move from one system to another,
 | |
| 	// we check all known path syntaxes, both Unix and Windows.
 | |
| 	return ns == "." || strings.HasPrefix(ns, "./") || strings.HasPrefix(ns, `.\`) ||
 | |
| 		ns == ".." || strings.HasPrefix(ns, "../") || strings.HasPrefix(ns, `..\`) ||
 | |
| 		strings.HasPrefix(ns, "/") || strings.HasPrefix(ns, `\`) ||
 | |
| 		len(ns) >= 2 && ('A' <= ns[0] && ns[0] <= 'Z' || 'a' <= ns[0] && ns[0] <= 'z') && ns[1] == ':'
 | |
| }
 | |
| 
 | |
| // MustQuote reports whether s must be quoted in order to appear as
 | |
| // a single token in a go.mod line.
 | |
| func MustQuote(s string) bool {
 | |
| 	for _, r := range s {
 | |
| 		switch r {
 | |
| 		case ' ', '"', '\'', '`':
 | |
| 			return true
 | |
| 
 | |
| 		case '(', ')', '[', ']', '{', '}', ',':
 | |
| 			if len(s) > 1 {
 | |
| 				return true
 | |
| 			}
 | |
| 
 | |
| 		default:
 | |
| 			if !unicode.IsPrint(r) {
 | |
| 				return true
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	return s == "" || strings.Contains(s, "//") || strings.Contains(s, "/*")
 | |
| }
 | |
| 
 | |
| // AutoQuote returns s or, if quoting is required for s to appear in a go.mod,
 | |
| // the quotation of s.
 | |
| func AutoQuote(s string) string {
 | |
| 	if MustQuote(s) {
 | |
| 		return strconv.Quote(s)
 | |
| 	}
 | |
| 	return s
 | |
| }
 | |
| 
 | |
| func parseVersionInterval(verb string, path string, args *[]string, fix VersionFixer) (VersionInterval, error) {
 | |
| 	toks := *args
 | |
| 	if len(toks) == 0 || toks[0] == "(" {
 | |
| 		return VersionInterval{}, fmt.Errorf("expected '[' or version")
 | |
| 	}
 | |
| 	if toks[0] != "[" {
 | |
| 		v, err := parseVersion(verb, path, &toks[0], fix)
 | |
| 		if err != nil {
 | |
| 			return VersionInterval{}, err
 | |
| 		}
 | |
| 		*args = toks[1:]
 | |
| 		return VersionInterval{Low: v, High: v}, nil
 | |
| 	}
 | |
| 	toks = toks[1:]
 | |
| 
 | |
| 	if len(toks) == 0 {
 | |
| 		return VersionInterval{}, fmt.Errorf("expected version after '['")
 | |
| 	}
 | |
| 	low, err := parseVersion(verb, path, &toks[0], fix)
 | |
| 	if err != nil {
 | |
| 		return VersionInterval{}, err
 | |
| 	}
 | |
| 	toks = toks[1:]
 | |
| 
 | |
| 	if len(toks) == 0 || toks[0] != "," {
 | |
| 		return VersionInterval{}, fmt.Errorf("expected ',' after version")
 | |
| 	}
 | |
| 	toks = toks[1:]
 | |
| 
 | |
| 	if len(toks) == 0 {
 | |
| 		return VersionInterval{}, fmt.Errorf("expected version after ','")
 | |
| 	}
 | |
| 	high, err := parseVersion(verb, path, &toks[0], fix)
 | |
| 	if err != nil {
 | |
| 		return VersionInterval{}, err
 | |
| 	}
 | |
| 	toks = toks[1:]
 | |
| 
 | |
| 	if len(toks) == 0 || toks[0] != "]" {
 | |
| 		return VersionInterval{}, fmt.Errorf("expected ']' after version")
 | |
| 	}
 | |
| 	toks = toks[1:]
 | |
| 
 | |
| 	*args = toks
 | |
| 	return VersionInterval{Low: low, High: high}, nil
 | |
| }
 | |
| 
 | |
| func parseString(s *string) (string, error) {
 | |
| 	t := *s
 | |
| 	if strings.HasPrefix(t, `"`) {
 | |
| 		var err error
 | |
| 		if t, err = strconv.Unquote(t); err != nil {
 | |
| 			return "", err
 | |
| 		}
 | |
| 	} else if strings.ContainsAny(t, "\"'`") {
 | |
| 		// Other quotes are reserved both for possible future expansion
 | |
| 		// and to avoid confusion. For example if someone types 'x'
 | |
| 		// we want that to be a syntax error and not a literal x in literal quotation marks.
 | |
| 		return "", fmt.Errorf("unquoted string cannot contain quote")
 | |
| 	}
 | |
| 	*s = AutoQuote(t)
 | |
| 	return t, nil
 | |
| }
 | |
| 
 | |
| var deprecatedRE = lazyregexp.New(`(?s)(?:^|\n\n)Deprecated: *(.*?)(?:$|\n\n)`)
 | |
| 
 | |
| // parseDeprecation extracts the text of comments on a "module" directive and
 | |
| // extracts a deprecation message from that.
 | |
| //
 | |
| // A deprecation message is contained in a paragraph within a block of comments
 | |
| // that starts with "Deprecated:" (case sensitive). The message runs until the
 | |
| // end of the paragraph and does not include the "Deprecated:" prefix. If the
 | |
| // comment block has multiple paragraphs that start with "Deprecated:",
 | |
| // parseDeprecation returns the message from the first.
 | |
| func parseDeprecation(block *LineBlock, line *Line) string {
 | |
| 	text := parseDirectiveComment(block, line)
 | |
| 	m := deprecatedRE.FindStringSubmatch(text)
 | |
| 	if m == nil {
 | |
| 		return ""
 | |
| 	}
 | |
| 	return m[1]
 | |
| }
 | |
| 
 | |
| // parseDirectiveComment extracts the text of comments on a directive.
 | |
| // If the directive's line does not have comments and is part of a block that
 | |
| // does have comments, the block's comments are used.
 | |
| func parseDirectiveComment(block *LineBlock, line *Line) string {
 | |
| 	comments := line.Comment()
 | |
| 	if block != nil && len(comments.Before) == 0 && len(comments.Suffix) == 0 {
 | |
| 		comments = block.Comment()
 | |
| 	}
 | |
| 	groups := [][]Comment{comments.Before, comments.Suffix}
 | |
| 	var lines []string
 | |
| 	for _, g := range groups {
 | |
| 		for _, c := range g {
 | |
| 			if !strings.HasPrefix(c.Token, "//") {
 | |
| 				continue // blank line
 | |
| 			}
 | |
| 			lines = append(lines, strings.TrimSpace(strings.TrimPrefix(c.Token, "//")))
 | |
| 		}
 | |
| 	}
 | |
| 	return strings.Join(lines, "\n")
 | |
| }
 | |
| 
 | |
| type ErrorList []Error
 | |
| 
 | |
| func (e ErrorList) Error() string {
 | |
| 	errStrs := make([]string, len(e))
 | |
| 	for i, err := range e {
 | |
| 		errStrs[i] = err.Error()
 | |
| 	}
 | |
| 	return strings.Join(errStrs, "\n")
 | |
| }
 | |
| 
 | |
| type Error struct {
 | |
| 	Filename string
 | |
| 	Pos      Position
 | |
| 	Verb     string
 | |
| 	ModPath  string
 | |
| 	Err      error
 | |
| }
 | |
| 
 | |
| func (e *Error) Error() string {
 | |
| 	var pos string
 | |
| 	if e.Pos.LineRune > 1 {
 | |
| 		// Don't print LineRune if it's 1 (beginning of line).
 | |
| 		// It's always 1 except in scanner errors, which are rare.
 | |
| 		pos = fmt.Sprintf("%s:%d:%d: ", e.Filename, e.Pos.Line, e.Pos.LineRune)
 | |
| 	} else if e.Pos.Line > 0 {
 | |
| 		pos = fmt.Sprintf("%s:%d: ", e.Filename, e.Pos.Line)
 | |
| 	} else if e.Filename != "" {
 | |
| 		pos = fmt.Sprintf("%s: ", e.Filename)
 | |
| 	}
 | |
| 
 | |
| 	var directive string
 | |
| 	if e.ModPath != "" {
 | |
| 		directive = fmt.Sprintf("%s %s: ", e.Verb, e.ModPath)
 | |
| 	} else if e.Verb != "" {
 | |
| 		directive = fmt.Sprintf("%s: ", e.Verb)
 | |
| 	}
 | |
| 
 | |
| 	return pos + directive + e.Err.Error()
 | |
| }
 | |
| 
 | |
| func (e *Error) Unwrap() error { return e.Err }
 | |
| 
 | |
| func parseVersion(verb string, path string, s *string, fix VersionFixer) (string, error) {
 | |
| 	t, err := parseString(s)
 | |
| 	if err != nil {
 | |
| 		return "", &Error{
 | |
| 			Verb:    verb,
 | |
| 			ModPath: path,
 | |
| 			Err: &module.InvalidVersionError{
 | |
| 				Version: *s,
 | |
| 				Err:     err,
 | |
| 			},
 | |
| 		}
 | |
| 	}
 | |
| 	if fix != nil {
 | |
| 		fixed, err := fix(path, t)
 | |
| 		if err != nil {
 | |
| 			if err, ok := err.(*module.ModuleError); ok {
 | |
| 				return "", &Error{
 | |
| 					Verb:    verb,
 | |
| 					ModPath: path,
 | |
| 					Err:     err.Err,
 | |
| 				}
 | |
| 			}
 | |
| 			return "", err
 | |
| 		}
 | |
| 		t = fixed
 | |
| 	} else {
 | |
| 		cv := module.CanonicalVersion(t)
 | |
| 		if cv == "" {
 | |
| 			return "", &Error{
 | |
| 				Verb:    verb,
 | |
| 				ModPath: path,
 | |
| 				Err: &module.InvalidVersionError{
 | |
| 					Version: t,
 | |
| 					Err:     errors.New("must be of the form v1.2.3"),
 | |
| 				},
 | |
| 			}
 | |
| 		}
 | |
| 		t = cv
 | |
| 	}
 | |
| 	*s = t
 | |
| 	return *s, nil
 | |
| }
 | |
| 
 | |
| func modulePathMajor(path string) (string, error) {
 | |
| 	_, major, ok := module.SplitPathVersion(path)
 | |
| 	if !ok {
 | |
| 		return "", fmt.Errorf("invalid module path")
 | |
| 	}
 | |
| 	return major, nil
 | |
| }
 | |
| 
 | |
| func (f *File) Format() ([]byte, error) {
 | |
| 	return Format(f.Syntax), nil
 | |
| }
 | |
| 
 | |
| // Cleanup cleans up the file f after any edit operations.
 | |
| // To avoid quadratic behavior, modifications like [File.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 _, g := range f.Godebug {
 | |
| 		if g.Key != "" {
 | |
| 			f.Godebug[w] = g
 | |
| 			w++
 | |
| 		}
 | |
| 	}
 | |
| 	f.Godebug = f.Godebug[:w]
 | |
| 
 | |
| 	w = 0
 | |
| 	for _, r := range f.Require {
 | |
| 		if r.Mod.Path != "" {
 | |
| 			f.Require[w] = r
 | |
| 			w++
 | |
| 		}
 | |
| 	}
 | |
| 	f.Require = f.Require[:w]
 | |
| 
 | |
| 	w = 0
 | |
| 	for _, x := range f.Exclude {
 | |
| 		if x.Mod.Path != "" {
 | |
| 			f.Exclude[w] = x
 | |
| 			w++
 | |
| 		}
 | |
| 	}
 | |
| 	f.Exclude = f.Exclude[:w]
 | |
| 
 | |
| 	w = 0
 | |
| 	for _, r := range f.Replace {
 | |
| 		if r.Old.Path != "" {
 | |
| 			f.Replace[w] = r
 | |
| 			w++
 | |
| 		}
 | |
| 	}
 | |
| 	f.Replace = f.Replace[:w]
 | |
| 
 | |
| 	w = 0
 | |
| 	for _, r := range f.Retract {
 | |
| 		if r.Low != "" || r.High != "" {
 | |
| 			f.Retract[w] = r
 | |
| 			w++
 | |
| 		}
 | |
| 	}
 | |
| 	f.Retract = f.Retract[:w]
 | |
| 
 | |
| 	f.Syntax.Cleanup()
 | |
| }
 | |
| 
 | |
| func (f *File) AddGoStmt(version string) error {
 | |
| 	if !GoVersionRE.MatchString(version) {
 | |
| 		return fmt.Errorf("invalid language version %q", version)
 | |
| 	}
 | |
| 	if f.Go == nil {
 | |
| 		var hint Expr
 | |
| 		if f.Module != nil && f.Module.Syntax != nil {
 | |
| 			hint = f.Module.Syntax
 | |
| 		} else if f.Syntax == nil {
 | |
| 			f.Syntax = new(FileSyntax)
 | |
| 		}
 | |
| 		f.Go = &Go{
 | |
| 			Version: version,
 | |
| 			Syntax:  f.Syntax.addLine(hint, "go", version),
 | |
| 		}
 | |
| 	} else {
 | |
| 		f.Go.Version = version
 | |
| 		f.Syntax.updateLine(f.Go.Syntax, "go", version)
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // DropGoStmt deletes the go statement from the file.
 | |
| func (f *File) DropGoStmt() {
 | |
| 	if f.Go != nil {
 | |
| 		f.Go.Syntax.markRemoved()
 | |
| 		f.Go = nil
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // DropToolchainStmt deletes the toolchain statement from the file.
 | |
| func (f *File) DropToolchainStmt() {
 | |
| 	if f.Toolchain != nil {
 | |
| 		f.Toolchain.Syntax.markRemoved()
 | |
| 		f.Toolchain = nil
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (f *File) AddToolchainStmt(name string) error {
 | |
| 	if !ToolchainRE.MatchString(name) {
 | |
| 		return fmt.Errorf("invalid toolchain name %q", name)
 | |
| 	}
 | |
| 	if f.Toolchain == nil {
 | |
| 		var hint Expr
 | |
| 		if f.Go != nil && f.Go.Syntax != nil {
 | |
| 			hint = f.Go.Syntax
 | |
| 		} else if f.Module != nil && f.Module.Syntax != nil {
 | |
| 			hint = f.Module.Syntax
 | |
| 		}
 | |
| 		f.Toolchain = &Toolchain{
 | |
| 			Name:   name,
 | |
| 			Syntax: f.Syntax.addLine(hint, "toolchain", name),
 | |
| 		}
 | |
| 	} else {
 | |
| 		f.Toolchain.Name = name
 | |
| 		f.Syntax.updateLine(f.Toolchain.Syntax, "toolchain", name)
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // AddGodebug sets the first godebug line for key to value,
 | |
| // preserving any existing comments for that line and removing all
 | |
| // other godebug lines for key.
 | |
| //
 | |
| // If no line currently exists for key, AddGodebug adds a new line
 | |
| // at the end of the last godebug block.
 | |
| func (f *File) AddGodebug(key, value string) error {
 | |
| 	need := true
 | |
| 	for _, g := range f.Godebug {
 | |
| 		if g.Key == key {
 | |
| 			if need {
 | |
| 				g.Value = value
 | |
| 				f.Syntax.updateLine(g.Syntax, "godebug", key+"="+value)
 | |
| 				need = false
 | |
| 			} else {
 | |
| 				g.Syntax.markRemoved()
 | |
| 				*g = Godebug{}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if need {
 | |
| 		f.addNewGodebug(key, value)
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // addNewGodebug adds a new godebug key=value line at the end
 | |
| // of the last godebug block, regardless of any existing godebug lines for key.
 | |
| func (f *File) addNewGodebug(key, value string) {
 | |
| 	line := f.Syntax.addLine(nil, "godebug", key+"="+value)
 | |
| 	g := &Godebug{
 | |
| 		Key:    key,
 | |
| 		Value:  value,
 | |
| 		Syntax: line,
 | |
| 	}
 | |
| 	f.Godebug = append(f.Godebug, g)
 | |
| }
 | |
| 
 | |
| // 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
 | |
| // [File.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) DropGodebug(key string) error {
 | |
| 	for _, g := range f.Godebug {
 | |
| 		if g.Key == key {
 | |
| 			g.Syntax.markRemoved()
 | |
| 			*g = Godebug{}
 | |
| 		}
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (f *File) DropRequire(path string) error {
 | |
| 	for _, r := range f.Require {
 | |
| 		if r.Mod.Path == path {
 | |
| 			r.Syntax.markRemoved()
 | |
| 			*r = Require{}
 | |
| 		}
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // AddExclude adds a exclude statement to the mod file. Errors if the provided
 | |
| // version is not a canonical version string
 | |
| func (f *File) AddExclude(path, vers string) error {
 | |
| 	if err := checkCanonicalVersion(path, vers); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	var hint *Line
 | |
| 	for _, x := range f.Exclude {
 | |
| 		if x.Mod.Path == path && x.Mod.Version == vers {
 | |
| 			return nil
 | |
| 		}
 | |
| 		if x.Mod.Path == path {
 | |
| 			hint = x.Syntax
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	f.Exclude = append(f.Exclude, &Exclude{Mod: module.Version{Path: path, Version: vers}, Syntax: f.Syntax.addLine(hint, "exclude", AutoQuote(path), vers)})
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (f *File) DropExclude(path, vers string) error {
 | |
| 	for _, x := range f.Exclude {
 | |
| 		if x.Mod.Path == path && x.Mod.Version == vers {
 | |
| 			x.Syntax.markRemoved()
 | |
| 			*x = Exclude{}
 | |
| 		}
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (f *File) AddReplace(oldPath, oldVers, newPath, newVers string) error {
 | |
| 	return addReplace(f.Syntax, &f.Replace, oldPath, oldVers, newPath, newVers)
 | |
| }
 | |
| 
 | |
| func addReplace(syntax *FileSyntax, replace *[]*Replace, oldPath, oldVers, newPath, newVers string) error {
 | |
| 	need := true
 | |
| 	old := module.Version{Path: oldPath, Version: oldVers}
 | |
| 	new := module.Version{Path: newPath, Version: newVers}
 | |
| 	tokens := []string{"replace", AutoQuote(oldPath)}
 | |
| 	if oldVers != "" {
 | |
| 		tokens = append(tokens, oldVers)
 | |
| 	}
 | |
| 	tokens = append(tokens, "=>", AutoQuote(newPath))
 | |
| 	if newVers != "" {
 | |
| 		tokens = append(tokens, newVers)
 | |
| 	}
 | |
| 
 | |
| 	var hint *Line
 | |
| 	for _, r := range *replace {
 | |
| 		if r.Old.Path == oldPath && (oldVers == "" || r.Old.Version == oldVers) {
 | |
| 			if need {
 | |
| 				// Found replacement for old; update to use new.
 | |
| 				r.New = new
 | |
| 				syntax.updateLine(r.Syntax, tokens...)
 | |
| 				need = false
 | |
| 				continue
 | |
| 			}
 | |
| 			// Already added; delete other replacements for same.
 | |
| 			r.Syntax.markRemoved()
 | |
| 			*r = Replace{}
 | |
| 		}
 | |
| 		if r.Old.Path == oldPath {
 | |
| 			hint = r.Syntax
 | |
| 		}
 | |
| 	}
 | |
| 	if need {
 | |
| 		*replace = append(*replace, &Replace{Old: old, New: new, Syntax: syntax.addLine(hint, tokens...)})
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (f *File) DropReplace(oldPath, oldVers string) error {
 | |
| 	for _, r := range f.Replace {
 | |
| 		if r.Old.Path == oldPath && r.Old.Version == oldVers {
 | |
| 			r.Syntax.markRemoved()
 | |
| 			*r = Replace{}
 | |
| 		}
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // AddRetract adds a retract statement to the mod file. Errors if the provided
 | |
| // version interval does not consist of canonical version strings
 | |
| func (f *File) AddRetract(vi VersionInterval, rationale string) error {
 | |
| 	var path string
 | |
| 	if f.Module != nil {
 | |
| 		path = f.Module.Mod.Path
 | |
| 	}
 | |
| 	if err := checkCanonicalVersion(path, vi.High); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	if err := checkCanonicalVersion(path, vi.Low); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	r := &Retract{
 | |
| 		VersionInterval: vi,
 | |
| 	}
 | |
| 	if vi.Low == vi.High {
 | |
| 		r.Syntax = f.Syntax.addLine(nil, "retract", AutoQuote(vi.Low))
 | |
| 	} else {
 | |
| 		r.Syntax = f.Syntax.addLine(nil, "retract", "[", AutoQuote(vi.Low), ",", AutoQuote(vi.High), "]")
 | |
| 	}
 | |
| 	if rationale != "" {
 | |
| 		for _, line := range strings.Split(rationale, "\n") {
 | |
| 			com := Comment{Token: "// " + line}
 | |
| 			r.Syntax.Comment().Before = append(r.Syntax.Comment().Before, com)
 | |
| 		}
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (f *File) DropRetract(vi VersionInterval) error {
 | |
| 	for _, r := range f.Retract {
 | |
| 		if r.VersionInterval == vi {
 | |
| 			r.Syntax.markRemoved()
 | |
| 			*r = Retract{}
 | |
| 		}
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (f *File) SortBlocks() {
 | |
| 	f.removeDups() // otherwise sorting is unsafe
 | |
| 
 | |
| 	// semanticSortForExcludeVersionV is the Go version (plus leading "v") at which
 | |
| 	// lines in exclude blocks start to use semantic sort instead of lexicographic sort.
 | |
| 	// See go.dev/issue/60028.
 | |
| 	const semanticSortForExcludeVersionV = "v1.21"
 | |
| 	useSemanticSortForExclude := f.Go != nil && semver.Compare("v"+f.Go.Version, semanticSortForExcludeVersionV) >= 0
 | |
| 
 | |
| 	for _, stmt := range f.Syntax.Stmt {
 | |
| 		block, ok := stmt.(*LineBlock)
 | |
| 		if !ok {
 | |
| 			continue
 | |
| 		}
 | |
| 		less := lineLess
 | |
| 		if block.Token[0] == "exclude" && useSemanticSortForExclude {
 | |
| 			less = lineExcludeLess
 | |
| 		} else if block.Token[0] == "retract" {
 | |
| 			less = lineRetractLess
 | |
| 		}
 | |
| 		sort.SliceStable(block.Line, func(i, j int) bool {
 | |
| 			return less(block.Line[i], block.Line[j])
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // removeDups removes duplicate exclude and replace directives.
 | |
| //
 | |
| // Earlier exclude directives take priority.
 | |
| //
 | |
| // Later replace directives take priority.
 | |
| //
 | |
| // require directives are not de-duplicated. That's left up to higher-level
 | |
| // logic (MVS).
 | |
| //
 | |
| // retract directives are not de-duplicated since comments are
 | |
| // meaningful, and versions may be retracted multiple times.
 | |
| func (f *File) removeDups() {
 | |
| 	removeDups(f.Syntax, &f.Exclude, &f.Replace)
 | |
| }
 | |
| 
 | |
| func removeDups(syntax *FileSyntax, exclude *[]*Exclude, replace *[]*Replace) {
 | |
| 	kill := make(map[*Line]bool)
 | |
| 
 | |
| 	// Remove duplicate excludes.
 | |
| 	if exclude != nil {
 | |
| 		haveExclude := make(map[module.Version]bool)
 | |
| 		for _, x := range *exclude {
 | |
| 			if haveExclude[x.Mod] {
 | |
| 				kill[x.Syntax] = true
 | |
| 				continue
 | |
| 			}
 | |
| 			haveExclude[x.Mod] = true
 | |
| 		}
 | |
| 		var excl []*Exclude
 | |
| 		for _, x := range *exclude {
 | |
| 			if !kill[x.Syntax] {
 | |
| 				excl = append(excl, x)
 | |
| 			}
 | |
| 		}
 | |
| 		*exclude = excl
 | |
| 	}
 | |
| 
 | |
| 	// Remove duplicate replacements.
 | |
| 	// Later replacements take priority over earlier ones.
 | |
| 	haveReplace := make(map[module.Version]bool)
 | |
| 	for i := len(*replace) - 1; i >= 0; i-- {
 | |
| 		x := (*replace)[i]
 | |
| 		if haveReplace[x.Old] {
 | |
| 			kill[x.Syntax] = true
 | |
| 			continue
 | |
| 		}
 | |
| 		haveReplace[x.Old] = true
 | |
| 	}
 | |
| 	var repl []*Replace
 | |
| 	for _, x := range *replace {
 | |
| 		if !kill[x.Syntax] {
 | |
| 			repl = append(repl, x)
 | |
| 		}
 | |
| 	}
 | |
| 	*replace = repl
 | |
| 
 | |
| 	// Duplicate require and retract directives are not removed.
 | |
| 
 | |
| 	// Drop killed statements from the syntax tree.
 | |
| 	var stmts []Expr
 | |
| 	for _, stmt := range syntax.Stmt {
 | |
| 		switch stmt := stmt.(type) {
 | |
| 		case *Line:
 | |
| 			if kill[stmt] {
 | |
| 				continue
 | |
| 			}
 | |
| 		case *LineBlock:
 | |
| 			var lines []*Line
 | |
| 			for _, line := range stmt.Line {
 | |
| 				if !kill[line] {
 | |
| 					lines = append(lines, line)
 | |
| 				}
 | |
| 			}
 | |
| 			stmt.Line = lines
 | |
| 			if len(lines) == 0 {
 | |
| 				continue
 | |
| 			}
 | |
| 		}
 | |
| 		stmts = append(stmts, stmt)
 | |
| 	}
 | |
| 	syntax.Stmt = stmts
 | |
| }
 | |
| 
 | |
| // lineLess returns whether li should be sorted before lj. It sorts
 | |
| // lexicographically without assigning any special meaning to tokens.
 | |
| func lineLess(li, lj *Line) bool {
 | |
| 	for k := 0; k < len(li.Token) && k < len(lj.Token); k++ {
 | |
| 		if li.Token[k] != lj.Token[k] {
 | |
| 			return li.Token[k] < lj.Token[k]
 | |
| 		}
 | |
| 	}
 | |
| 	return len(li.Token) < len(lj.Token)
 | |
| }
 | |
| 
 | |
| // lineExcludeLess reports whether li should be sorted before lj for lines in
 | |
| // an "exclude" block.
 | |
| func lineExcludeLess(li, lj *Line) bool {
 | |
| 	if len(li.Token) != 2 || len(lj.Token) != 2 {
 | |
| 		// Not a known exclude specification.
 | |
| 		// Fall back to sorting lexicographically.
 | |
| 		return lineLess(li, lj)
 | |
| 	}
 | |
| 	// An exclude specification has two tokens: ModulePath and Version.
 | |
| 	// Compare module path by string order and version by semver rules.
 | |
| 	if pi, pj := li.Token[0], lj.Token[0]; pi != pj {
 | |
| 		return pi < pj
 | |
| 	}
 | |
| 	return semver.Compare(li.Token[1], lj.Token[1]) < 0
 | |
| }
 | |
| 
 | |
| // lineRetractLess returns whether li should be sorted before lj for lines in
 | |
| // a "retract" block. It treats each line as a version interval. Single versions
 | |
| // are compared as if they were intervals with the same low and high version.
 | |
| // Intervals are sorted in descending order, first by low version, then by
 | |
| // high version, using semver.Compare.
 | |
| func lineRetractLess(li, lj *Line) bool {
 | |
| 	interval := func(l *Line) VersionInterval {
 | |
| 		if len(l.Token) == 1 {
 | |
| 			return VersionInterval{Low: l.Token[0], High: l.Token[0]}
 | |
| 		} else if len(l.Token) == 5 && l.Token[0] == "[" && l.Token[2] == "," && l.Token[4] == "]" {
 | |
| 			return VersionInterval{Low: l.Token[1], High: l.Token[3]}
 | |
| 		} else {
 | |
| 			// Line in unknown format. Treat as an invalid version.
 | |
| 			return VersionInterval{}
 | |
| 		}
 | |
| 	}
 | |
| 	vii := interval(li)
 | |
| 	vij := interval(lj)
 | |
| 	if cmp := semver.Compare(vii.Low, vij.Low); cmp != 0 {
 | |
| 		return cmp > 0
 | |
| 	}
 | |
| 	return semver.Compare(vii.High, vij.High) > 0
 | |
| }
 | |
| 
 | |
| // checkCanonicalVersion returns a non-nil error if vers is not a canonical
 | |
| // version string or does not match the major version of path.
 | |
| //
 | |
| // If path is non-empty, the error text suggests a format with a major version
 | |
| // corresponding to the path.
 | |
| func checkCanonicalVersion(path, vers string) error {
 | |
| 	_, pathMajor, pathMajorOk := module.SplitPathVersion(path)
 | |
| 
 | |
| 	if vers == "" || vers != module.CanonicalVersion(vers) {
 | |
| 		if pathMajor == "" {
 | |
| 			return &module.InvalidVersionError{
 | |
| 				Version: vers,
 | |
| 				Err:     fmt.Errorf("must be of the form v1.2.3"),
 | |
| 			}
 | |
| 		}
 | |
| 		return &module.InvalidVersionError{
 | |
| 			Version: vers,
 | |
| 			Err:     fmt.Errorf("must be of the form %s.2.3", module.PathMajorPrefix(pathMajor)),
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if pathMajorOk {
 | |
| 		if err := module.CheckPathMajor(vers, pathMajor); err != nil {
 | |
| 			if pathMajor == "" {
 | |
| 				// In this context, the user probably wrote "v2.3.4" when they meant
 | |
| 				// "v2.3.4+incompatible". Suggest that instead of "v0 or v1".
 | |
| 				return &module.InvalidVersionError{
 | |
| 					Version: vers,
 | |
| 					Err:     fmt.Errorf("should be %s+incompatible (or module %s/%v)", vers, path, semver.Major(vers)),
 | |
| 				}
 | |
| 			}
 | |
| 			return err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 |