diff --git a/modfile/read.go b/modfile/read.go index 5b5bb5e..2205682 100644 --- a/modfile/read.go +++ b/modfile/read.go @@ -225,7 +225,7 @@ func (x *FileSyntax) Cleanup() { if ww == 0 { continue } - if ww == 1 { + if ww == 1 && len(stmt.RParen.Comments.Before) == 0 { // Collapse block into single line. line := &Line{ Comments: Comments{ diff --git a/modfile/rule.go b/modfile/rule.go index 35fd1f5..66dcaf9 100644 --- a/modfile/rule.go +++ b/modfile/rule.go @@ -38,6 +38,7 @@ type File struct { Module *Module Go *Go Toolchain *Toolchain + Godebug []*Godebug Require []*Require Exclude []*Exclude Replace []*Replace @@ -65,6 +66,13 @@ type Toolchain struct { 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 @@ -289,7 +297,7 @@ func parseToFile(file string, data []byte, fix VersionFixer, strict bool) (parse }) } continue - case "module", "require", "exclude", "replace", "retract": + case "module", "godebug", "require", "exclude", "replace", "retract": for _, l := range x.Line { f.add(&errs, x, l, x.Token[0], l.Token, fix, strict) } @@ -308,6 +316,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) { @@ -383,8 +394,8 @@ func (f *File) add(errs *ErrorList, block *LineBlock, line *Line, verb string, a if len(args) != 1 { errorf("toolchain directive expects exactly one argument") return - } else if strict && !ToolchainRE.MatchString(args[0]) { - errorf("invalid toolchain version '%s': must match format go1.23.0 or local", args[0]) + } 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} @@ -411,6 +422,22 @@ func (f *File) add(errs *ErrorList, block *LineBlock, line *Line, verb string, a } 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) @@ -630,7 +657,7 @@ func (f *WorkFile) add(errs *ErrorList, line *Line, verb string, args []string, errorf("go directive expects exactly one argument") return } else if !GoVersionRE.MatchString(args[0]) { - errorf("invalid go version '%s': must match format 1.23", args[0]) + errorf("invalid go version '%s': must match format 1.23.0", args[0]) return } @@ -646,13 +673,29 @@ func (f *WorkFile) add(errs *ErrorList, line *Line, verb string, args []string, errorf("toolchain directive expects exactly one argument") return } else if !ToolchainRE.MatchString(args[0]) { - errorf("invalid toolchain version '%s': must match format go1.23 or local", 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) @@ -928,6 +971,15 @@ func (f *File) Format() ([]byte, error) { // 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 @@ -974,6 +1026,8 @@ func (f *File) AddGoStmt(version string) error { 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, @@ -1024,6 +1078,45 @@ func (f *File) AddToolchainStmt(name string) error { 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. @@ -1331,6 +1424,16 @@ func (f *File) SetRequireSeparateIndirect(req []*Require) { 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 { diff --git a/modfile/rule_test.go b/modfile/rule_test.go index 57c8be6..4d0d12a 100644 --- a/modfile/rule_test.go +++ b/modfile/rule_test.go @@ -90,6 +90,24 @@ var addRequireTests = []struct { ) `, }, + { + `unattached_comments`, + ` + module m + require ( + foo v0.0.0-00010101000000-000000000000 + // bar v0.0.0-00010101000000-000000000000 + ) + `, + "foo", "v0.0.0-00010101000000-000000000000", + ` + module m + require ( + foo v0.0.0-00010101000000-000000000000 + // bar v0.0.0-00010101000000-000000000000 + ) + `, + }, } type require struct { @@ -1549,6 +1567,153 @@ var fixVersionTests = []struct { }, } +var modifyEmptyFilesTests = []struct { + desc string + operations func(f *File) + want string +}{ + { + desc: `addGoStmt`, + operations: func(f *File) { + f.AddGoStmt("1.20") + }, + want: `go 1.20`, + }, +} + +var addGodebugTests = []struct { + desc string + in string + key string + value string + out string +}{ + { + `existing`, + ` + module m + godebug key=old + `, + "key", "new", + ` + module m + godebug key=new + `, + }, + { + `existing2`, + ` + module m + godebug ( + key=first // first + other=first-a // first-a + ) + godebug key=second // second + godebug ( + key=third // third + other=third-a // third-a + ) + `, + "key", "new", + ` + module m + + godebug ( + key=new // first + other=first-a// first-a + ) + + godebug other=third-a // third-a + `, + }, + { + `new`, + ` + module m + godebug other=foo + `, + "key", "new", + ` + module m + godebug ( + other=foo + key=new + ) + `, + }, + { + `new2`, + ` + module m + godebug first=1 + godebug second=2 + `, + "third", "3", + ` + module m + godebug first=1 + godebug ( + second=2 + third=3 + ) + `, + }, +} + +var dropGodebugTests = []struct { + desc string + in string + key string + out string +}{ + { + `existing`, + ` + module m + godebug key=old + `, + "key", + ` + module m + `, + }, + { + `existing2`, + ` + module m + godebug ( + key=first // first + other=first-a // first-a + ) + godebug key=second // second + godebug ( + key=third // third + other=third-a // third-a + ) + `, + "key", + ` + module m + + godebug other=first-a// first-a + + godebug other=third-a // third-a + `, + }, + { + `new`, + ` + module m + godebug other=foo + `, + "key", + ` + module m + godebug other=foo + `, + }, +} + func fixV(path, version string) (string, error) { if path != "example.com/m" { return "", fmt.Errorf("module path must be example.com/m") @@ -1568,6 +1733,18 @@ func TestAddRequire(t *testing.T) { } } +func TestAddGodebug(t *testing.T) { + for _, tt := range addGodebugTests { + t.Run(tt.desc, func(t *testing.T) { + testEdit(t, tt.in, tt.out, true, func(f *File) error { + err := f.AddGodebug(tt.key, tt.value) + f.Cleanup() + return err + }) + }) + } +} + func TestSetRequire(t *testing.T) { for _, tt := range setRequireTests { t.Run(tt.desc, func(t *testing.T) { @@ -1664,6 +1841,18 @@ func TestDropToolchain(t *testing.T) { } } +func TestDropGodebug(t *testing.T) { + for _, tt := range dropGodebugTests { + t.Run(tt.desc, func(t *testing.T) { + testEdit(t, tt.in, tt.out, true, func(f *File) error { + f.DropGodebug(tt.key) + f.Cleanup() + return nil + }) + }) + } +} + func TestAddExclude(t *testing.T) { for _, tt := range addExcludeTests { t.Run(tt.desc, func(t *testing.T) { @@ -1846,3 +2035,29 @@ func TestFixVersion(t *testing.T) { }) } } + +func TestAddOnEmptyFile(t *testing.T) { + for _, tt := range modifyEmptyFilesTests { + t.Run(tt.desc, func(t *testing.T) { + f := &File{} + tt.operations(f) + + expect, err := Parse("out", []byte(tt.want), nil) + if err != nil { + t.Fatal(err) + } + golden, err := expect.Format() + if err != nil { + t.Fatal(err) + } + got, err := f.Format() + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(got, golden) { + t.Fatalf("got:\n%s\nwant:\n%s", got, golden) + } + }) + } +} diff --git a/modfile/work.go b/modfile/work.go index d7b9937..8f54897 100644 --- a/modfile/work.go +++ b/modfile/work.go @@ -14,6 +14,7 @@ import ( type WorkFile struct { Go *Go Toolchain *Toolchain + Godebug []*Godebug Use []*Use Replace []*Replace @@ -68,7 +69,7 @@ func ParseWork(file string, data []byte, fix VersionFixer) (*WorkFile, error) { Err: fmt.Errorf("unknown block type: %s", strings.Join(x.Token, " ")), }) continue - case "use", "replace": + case "godebug", "use", "replace": for _, l := range x.Line { f.add(&errs, l, x.Token[0], l.Token, fix) } @@ -184,6 +185,55 @@ func (f *WorkFile) DropToolchainStmt() { } } +// 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 *WorkFile) 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 *WorkFile) 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) +} + +func (f *WorkFile) DropGodebug(key string) error { + for _, g := range f.Godebug { + if g.Key == key { + g.Syntax.markRemoved() + *g = Godebug{} + } + } + return nil +} + func (f *WorkFile) AddUse(diskPath, modulePath string) error { need := true for _, d := range f.Use { diff --git a/modfile/work_test.go b/modfile/work_test.go index dcc0810..b4b4e7e 100644 --- a/modfile/work_test.go +++ b/modfile/work_test.go @@ -352,6 +352,34 @@ func TestWorkSortBlocks(t *testing.T) { } } +func TestWorkAddGodebug(t *testing.T) { + for _, tt := range addGodebugTests { + t.Run(tt.desc, func(t *testing.T) { + in := strings.ReplaceAll(tt.in, "module m", "use foo") + out := strings.ReplaceAll(tt.out, "module m", "use foo") + testWorkEdit(t, in, out, func(f *WorkFile) error { + err := f.AddGodebug(tt.key, tt.value) + f.Cleanup() + return err + }) + }) + } +} + +func TestWorkDropGodebug(t *testing.T) { + for _, tt := range dropGodebugTests { + t.Run(tt.desc, func(t *testing.T) { + in := strings.ReplaceAll(tt.in, "module m", "use foo") + out := strings.ReplaceAll(tt.out, "module m", "use foo") + testWorkEdit(t, in, out, func(f *WorkFile) error { + f.DropGodebug(tt.key) + f.Cleanup() + return nil + }) + }) + } +} + // Test that when files in the testdata directory are parsed // and printed and parsed again, we get the same parse tree // both times. diff --git a/module/module.go b/module/module.go index 2a364b2..cac1a89 100644 --- a/module/module.go +++ b/module/module.go @@ -506,6 +506,7 @@ var badWindowsNames = []string{ "PRN", "AUX", "NUL", + "COM0", "COM1", "COM2", "COM3", @@ -515,6 +516,7 @@ var badWindowsNames = []string{ "COM7", "COM8", "COM9", + "LPT0", "LPT1", "LPT2", "LPT3", diff --git a/sumdb/client.go b/sumdb/client.go index aecdc68..04c6e24 100644 --- a/sumdb/client.go +++ b/sumdb/client.go @@ -8,7 +8,6 @@ import ( "bytes" "errors" "fmt" - "path" "strings" "sync" "sync/atomic" @@ -193,51 +192,7 @@ func (c *Client) SetGONOSUMDB(list string) { var ErrGONOSUMDB = errors.New("skipped (listed in GONOSUMDB)") func (c *Client) skip(target string) bool { - return globsMatchPath(c.nosumdb, target) -} - -// globsMatchPath reports whether any path prefix of target -// matches one of the glob patterns (as defined by path.Match) -// in the comma-separated globs list. -// It ignores any empty or malformed patterns in the list. -func globsMatchPath(globs, target string) bool { - for globs != "" { - // Extract next non-empty glob in comma-separated list. - var glob string - if i := strings.Index(globs, ","); i >= 0 { - glob, globs = globs[:i], globs[i+1:] - } else { - glob, globs = globs, "" - } - if glob == "" { - continue - } - - // A glob with N+1 path elements (N slashes) needs to be matched - // against the first N+1 path elements of target, - // which end just before the N+1'th slash. - n := strings.Count(glob, "/") - prefix := target - // Walk target, counting slashes, truncating at the N+1'th slash. - for i := 0; i < len(target); i++ { - if target[i] == '/' { - if n == 0 { - prefix = target[:i] - break - } - n-- - } - } - if n > 0 { - // Not enough prefix elements. - continue - } - matched, _ := path.Match(glob, prefix) - if matched { - return true - } - } - return false + return module.MatchPrefixPatterns(c.nosumdb, target) } // Lookup returns the go.sum lines for the given module path and version. diff --git a/sumdb/tlog/tile.go b/sumdb/tlog/tile.go index 857d487..37771c5 100644 --- a/sumdb/tlog/tile.go +++ b/sumdb/tlog/tile.go @@ -115,16 +115,14 @@ func NewTiles(h int, oldTreeSize, newTreeSize int64) []Tile { for level := uint(0); newTreeSize>>(H*level) > 0; level++ { oldN := oldTreeSize >> (H * level) newN := newTreeSize >> (H * level) + if oldN == newN { + continue + } for n := oldN >> H; n < newN>>H; n++ { tiles = append(tiles, Tile{H: h, L: int(level), N: n, W: 1 << H}) } n := newN >> H - maxW := int(newN - n< n< 0 { tiles = append(tiles, Tile{H: h, L: int(level), N: n, W: w}) } } diff --git a/sumdb/tlog/tile_test.go b/sumdb/tlog/tile_test.go index e451a63..62b50b7 100644 --- a/sumdb/tlog/tile_test.go +++ b/sumdb/tlog/tile_test.go @@ -5,6 +5,7 @@ package tlog import ( + "fmt" "testing" ) @@ -22,3 +23,28 @@ func FuzzParseTilePath(f *testing.F) { ParseTilePath(path) }) } + +func TestNewTilesForSize(t *testing.T) { + for _, tt := range []struct { + old, new int64 + want int + }{ + {1, 1, 0}, + {100, 101, 1}, + {1023, 1025, 3}, + {1024, 1030, 1}, + {1030, 2000, 1}, + {1030, 10000, 10}, + {49516517, 49516586, 3}, + } { + t.Run(fmt.Sprintf("%d-%d", tt.old, tt.new), func(t *testing.T) { + tiles := NewTiles(10, tt.old, tt.new) + if got := len(tiles); got != tt.want { + t.Errorf("got %d, want %d", got, tt.want) + for _, tile := range tiles { + t.Logf("%+v", tile) + } + } + }) + } +}