feat: Allow default options to be customzied with a flag. (#35)
diff --git a/cmd/cmd.go b/cmd/cmd.go
index 949bf52..75fa451 100644
--- a/cmd/cmd.go
+++ b/cmd/cmd.go
@@ -30,9 +30,10 @@
)
type Config struct {
- id string
- operation operation
- modifiedLines []keepsorted.LineRange
+ id string
+ defaultOptions keepsorted.BlockOptions
+ operation operation
+ modifiedLines []keepsorted.LineRange
}
func (c *Config) FromFlags(fs *flag.FlagSet) {
@@ -45,13 +46,37 @@
panic(err)
}
+ c.defaultOptions = keepsorted.DefaultBlockOptions()
+ fs.Var(&blockOptionsFlag{&c.defaultOptions}, "default-options", "The options keep-sorted will use to sort. Per-block overrides apply on top of these options. Note: list options like prefix_order are not merged with per-block overrides. They are completely overridden.")
+
of := &operationFlag{op: &c.operation}
if err := of.Set("fix"); err != nil {
panic(err)
}
fs.Var(of, "mode", fmt.Sprintf("Determines what mode to run this tool in. One of %q", knownModes()))
- fs.Var(&lineRangeFlag{lineRanges: &c.modifiedLines}, "lines", "Line ranges of the form \"start:end\". Only processes keep-sorted blocks that overlap with the given line ranges. Can only be used when fixing a single file.")
+ fs.Var(&lineRangeFlag{lineRanges: &c.modifiedLines}, "lines", "Line ranges of the form \"start:end\". Only processes keep-sorted blocks that overlap with the given line ranges. Can only be used when fixing a single file. This flag can either be a comma-separated list of line ranges, or it can be specified multiple times on the command line to specify multiple line ranges.")
+}
+
+type blockOptionsFlag struct {
+ opts *keepsorted.BlockOptions
+}
+
+func (f *blockOptionsFlag) String() string {
+ return f.opts.String()
+}
+
+func (f *blockOptionsFlag) Set(val string) error {
+ opts, err := keepsorted.ParseBlockOptions(val)
+ if err != nil {
+ return err
+ }
+ *f.opts = opts
+ return nil
+}
+
+func (f *blockOptionsFlag) Type() string {
+ return "options"
}
var (
@@ -67,7 +92,7 @@
return ms
}
-type operation func(id string, filenames []string, modifiedLines []keepsorted.LineRange) (ok bool, err error)
+type operation func(fixer *keepsorted.Fixer, filenames []string, modifiedLines []keepsorted.LineRange) (ok bool, err error)
type operationFlag struct {
op *operation
@@ -83,6 +108,7 @@
if op == nil {
return fmt.Errorf("unknown mode %q. Valid modes: %q", val, knownModes())
}
+ f.s = val
*f.op = op
return nil
}
@@ -116,7 +142,7 @@
}
func (f *lineRangeFlag) Type() string {
- return "line ranges"
+ return "line_ranges"
}
func (f *lineRangeFlag) Append(val string) error {
@@ -185,16 +211,16 @@
return false, errors.New("cannot specify modifiedLines with more than one file")
}
- return c.operation(c.id, files, c.modifiedLines)
+ return c.operation(keepsorted.New(c.id, c.defaultOptions), files, c.modifiedLines)
}
-func fix(id string, filenames []string, modifiedLines []keepsorted.LineRange) (ok bool, err error) {
+func fix(fixer *keepsorted.Fixer, filenames []string, modifiedLines []keepsorted.LineRange) (ok bool, err error) {
for _, fn := range filenames {
contents, err := read(fn)
if err != nil {
return false, err
}
- if want, alreadyFixed := keepsorted.New(id).Fix(contents, modifiedLines); fn == stdin || !alreadyFixed {
+ if want, alreadyFixed := fixer.Fix(contents, modifiedLines); fn == stdin || !alreadyFixed {
if err := write(fn, want); err != nil {
return false, err
}
@@ -203,14 +229,14 @@
return true, nil
}
-func lint(id string, filenames []string, modifiedLines []keepsorted.LineRange) (ok bool, err error) {
+func lint(fixer *keepsorted.Fixer, filenames []string, modifiedLines []keepsorted.LineRange) (ok bool, err error) {
var fs []*keepsorted.Finding
for _, fn := range filenames {
contents, err := read(fn)
if err != nil {
return false, err
}
- fs = append(fs, keepsorted.New(id).Findings(fn, contents, modifiedLines)...)
+ fs = append(fs, fixer.Findings(fn, contents, modifiedLines)...)
}
if len(fs) == 0 {
diff --git a/keepsorted/block.go b/keepsorted/block.go
index c292c85..b094241 100644
--- a/keepsorted/block.go
+++ b/keepsorted/block.go
@@ -109,7 +109,7 @@
continue
}
- opts, err := f.parseBlockOptions(start.line, defaultOptions)
+ opts, err := parseBlockOptions(start.line, f.defaultOptions)
if err != nil {
// TODO(b/250608236): Is there a better way to surface this error?
log.Err(fmt.Errorf("keep-sorted block at index %d had bad start directive: %w", start.index+offset, err)).Msg("")
@@ -238,7 +238,7 @@
}
groups := groupLines(lines, b.metadata)
- log.Printf("%d groups for block at index %d are (options %#v)", len(groups), b.start, b.metadata.opts)
+ log.Printf("%d groups for block at index %d are (options %v)", len(groups), b.start, b.metadata.opts)
for _, lg := range groups {
log.Printf("%#v", lg)
}
diff --git a/keepsorted/keep_sorted.go b/keepsorted/keep_sorted.go
index 4196d58..45b9a03 100644
--- a/keepsorted/keep_sorted.go
+++ b/keepsorted/keep_sorted.go
@@ -31,15 +31,17 @@
type Fixer struct {
ID string
+ defaultOptions blockOptions
startDirective string
endDirective string
}
// New creates a new fixer with the given string as its identifier.
// By default, id is "keep-sorted"
-func New(id string) *Fixer {
+func New(id string, defaultOptions BlockOptions) *Fixer {
return &Fixer{
ID: id,
+ defaultOptions: defaultOptions.opts,
startDirective: id + " start",
endDirective: id + " end",
}
diff --git a/keepsorted/keep_sorted_test.go b/keepsorted/keep_sorted_test.go
index 307030f..0abcd04 100644
--- a/keepsorted/keep_sorted_test.go
+++ b/keepsorted/keep_sorted_test.go
@@ -15,10 +15,13 @@
package keepsorted
import (
+ "fmt"
+ "reflect"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
+ "github.com/google/go-cmp/cmp/cmpopts"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
@@ -31,20 +34,6 @@
t.Cleanup(func() { log.Logger = oldLogger })
}
-func defaultOptionsWithCommentMarker(marker string) blockOptions {
- opts := defaultOptions
- opts.StickyComments = true
- opts.setCommentMarker(marker)
- return opts
-}
-
-func optionsWithCommentMarker(marker string) blockOptions {
- var opts blockOptions
- opts.StickyComments = true
- opts.setCommentMarker(marker)
- return opts
-}
-
func defaultMetadataWith(opts blockOptions) blockMetadata {
return blockMetadata{
startDirective: "keep-sorted-test start",
@@ -53,6 +42,12 @@
}
}
+func defaultMetadataWithCommentMarker(marker string) blockMetadata {
+ var opts blockOptions
+ opts.setCommentMarker(marker)
+ return defaultMetadataWith(opts)
+}
+
func TestFix(t *testing.T) {
for _, tc := range []struct {
name string
@@ -179,7 +174,7 @@
} {
t.Run(tc.name, func(t *testing.T) {
initZerolog(t)
- got, gotAlreadyFixed := New("keep-sorted-test").Fix(tc.in, nil)
+ got, gotAlreadyFixed := New("keep-sorted-test", BlockOptions{}).Fix(tc.in, nil)
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("Fix diff (-want +got):\n%s", diff)
}
@@ -320,7 +315,7 @@
mod = append(mod, LineRange{l, l})
}
}
- got := New("keep-sorted-test").findings(filename, strings.Split(tc.in, "\n"), mod, tc.considerLintOption)
+ got := New("keep-sorted-test", BlockOptions{}).findings(filename, strings.Split(tc.in, "\n"), mod, tc.considerLintOption)
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("Findings diff (-want +got):\n%s", diff)
}
@@ -360,7 +355,7 @@
wantBlocks: []block{
{
- metadata: defaultMetadataWith(defaultOptionsWithCommentMarker("//")),
+ metadata: defaultMetadataWithCommentMarker("//"),
start: 3,
end: 7,
lines: []string{
@@ -370,7 +365,7 @@
},
},
{
- metadata: defaultMetadataWith(defaultOptionsWithCommentMarker("//")),
+ metadata: defaultMetadataWithCommentMarker("//"),
start: 9,
end: 13,
lines: []string{
@@ -397,7 +392,7 @@
wantBlocks: []block{
{
- metadata: defaultMetadataWith(defaultOptionsWithCommentMarker("//")),
+ metadata: defaultMetadataWithCommentMarker("//"),
start: 5,
end: 7,
lines: []string{
@@ -435,7 +430,7 @@
wantBlocks: []block{
{
- metadata: defaultMetadataWith(defaultOptionsWithCommentMarker("//")),
+ metadata: defaultMetadataWithCommentMarker("//"),
start: 3,
end: 7,
lines: []string{
@@ -463,7 +458,7 @@
wantBlocks: []block{
{
- metadata: defaultMetadataWith(defaultOptionsWithCommentMarker("//")),
+ metadata: defaultMetadataWithCommentMarker("//"),
start: 1,
end: 6,
lines: []string{
@@ -496,7 +491,7 @@
wantBlocks: []block{
{
- metadata: defaultMetadataWith(defaultOptionsWithCommentMarker("//")),
+ metadata: defaultMetadataWithCommentMarker("//"),
start: 1,
end: 13,
lines: []string{
@@ -514,7 +509,7 @@
},
nestedBlocks: []block{
{
- metadata: defaultMetadataWith(defaultOptionsWithCommentMarker("//")),
+ metadata: defaultMetadataWithCommentMarker("//"),
start: 5,
end: 9,
lines: []string{
@@ -574,7 +569,7 @@
wantBlocks: []block{
{
- metadata: defaultMetadataWith(defaultOptionsWithCommentMarker("//")),
+ metadata: defaultMetadataWithCommentMarker("//"),
start: 1,
end: 34,
lines: []string{
@@ -613,7 +608,7 @@
},
nestedBlocks: []block{
{
- metadata: defaultMetadataWith(defaultOptionsWithCommentMarker("//")),
+ metadata: defaultMetadataWithCommentMarker("//"),
start: 5,
end: 30,
lines: []string{
@@ -644,7 +639,7 @@
},
nestedBlocks: []block{
{
- metadata: defaultMetadataWith(defaultOptionsWithCommentMarker("//")),
+ metadata: defaultMetadataWithCommentMarker("//"),
start: 9,
end: 21,
lines: []string{
@@ -662,7 +657,7 @@
},
nestedBlocks: []block{
{
- metadata: defaultMetadataWith(defaultOptionsWithCommentMarker("//")),
+ metadata: defaultMetadataWithCommentMarker("//"),
start: 13,
end: 17,
lines: []string{
@@ -674,7 +669,7 @@
},
},
{
- metadata: defaultMetadataWith(defaultOptionsWithCommentMarker("//")),
+ metadata: defaultMetadataWithCommentMarker("//"),
start: 22,
end: 26,
lines: []string{
@@ -688,7 +683,7 @@
},
},
{
- metadata: defaultMetadataWith(defaultOptionsWithCommentMarker("//")),
+ metadata: defaultMetadataWithCommentMarker("//"),
start: 35,
end: 39,
lines: []string{
@@ -714,7 +709,7 @@
wantBlocks: []block{
{
- metadata: defaultMetadataWith(defaultOptionsWithCommentMarker("//")),
+ metadata: defaultMetadataWithCommentMarker("//"),
start: 5,
end: 7,
lines: []string{"2"},
@@ -732,7 +727,7 @@
tc.include = func(start, end int) bool { return true }
}
- gotBlocks, gotIncompleteBlocks := New("keep-sorted-test").newBlocks(strings.Split(tc.in, "\n"), 0, tc.include)
+ gotBlocks, gotIncompleteBlocks := New("keep-sorted-test", BlockOptions{}).newBlocks(strings.Split(tc.in, "\n"), 0, tc.include)
if diff := cmp.Diff(tc.wantBlocks, gotBlocks, cmp.AllowUnexported(block{}, blockMetadata{}, blockOptions{})); diff != "" {
t.Errorf("blocks diff (-want +got):\n%s", diff)
}
@@ -861,7 +856,13 @@
{
name: "CommentOnlyBlock",
- opts: optionsWithCommentMarker("//"),
+ opts: func() blockOptions {
+ opts := blockOptions{
+ StickyComments: true,
+ }
+ opts.setCommentMarker("//")
+ return opts
+ }(),
in: []string{
"2",
"1",
@@ -919,8 +920,11 @@
name: "RemoveDuplicates_ConsidersComments",
opts: func() blockOptions {
- opts := optionsWithCommentMarker("//")
- opts.RemoveDuplicates = true
+ opts := blockOptions{
+ RemoveDuplicates: true,
+ StickyComments: true,
+ }
+ opts.setCommentMarker("//")
return opts
}(),
in: []string{
@@ -1180,7 +1184,13 @@
},
{
name: "StickyComments",
- opts: optionsWithCommentMarker("//"),
+ opts: func() blockOptions {
+ opts := blockOptions{
+ StickyComments: true,
+ }
+ opts.setCommentMarker("//")
+ return opts
+ }(),
want: []lineGroup{
{
@@ -1203,7 +1213,13 @@
},
{
name: "CommentOnlyGroup",
- opts: optionsWithCommentMarker("//"),
+ opts: func() blockOptions {
+ opts := blockOptions{
+ StickyComments: true,
+ }
+ opts.setCommentMarker("//")
+ return opts
+ }(),
want: []lineGroup{
{
@@ -1291,8 +1307,11 @@
{
name: "Group_NestedKeepSortedBlocksWithoutAnyIndentation",
opts: func() blockOptions {
- opts := optionsWithCommentMarker("//")
- opts.Group = true
+ opts := blockOptions{
+ Group: true,
+ StickyComments: true,
+ }
+ opts.setCommentMarker("//")
return opts
}(),
@@ -1425,8 +1444,10 @@
{
name: "Block_IgnoresSpecialCharactersWithinFullLineComments",
opts: func() blockOptions {
- opts := optionsWithCommentMarker("//")
- opts.Block = true
+ opts := blockOptions{
+ Block: true,
+ }
+ opts.setCommentMarker("//")
return opts
}(),
@@ -1450,8 +1471,10 @@
{
name: "Block_IgnoresSpecialCharactersWithinTrailingComments",
opts: func() blockOptions {
- opts := optionsWithCommentMarker("//")
- opts.Block = true
+ opts := blockOptions{
+ Block: true,
+ }
+ opts.setCommentMarker("//")
return opts
}(),
@@ -1628,7 +1651,7 @@
} {
t.Run(tc.name, func(t *testing.T) {
initZerolog(t)
- got, err := New("keep-sorted-test").parseBlockOptions(tc.in, tc.defaultOptions)
+ got, err := parseBlockOptions(tc.in, tc.defaultOptions)
if err != nil {
if tc.wantErr == "" {
t.Errorf("parseBlockOptions(%q) = %v", tc.in, err)
@@ -1639,6 +1662,47 @@
if diff := cmp.Diff(tc.want, got, cmp.AllowUnexported(blockOptions{})); diff != "" {
t.Errorf("parseBlockOptions(%q) mismatch (-want +got):\n%s", tc.in, diff)
}
+
+ _ = got.String() // Make sure this doesn't panic.
})
}
}
+
+func TestBlockOptions_ClonesDefaultOptions(t *testing.T) {
+ defaults := blockOptions{
+ StickyPrefixes: map[string]bool{},
+ }
+ _, err := parseBlockOptions("sticky_prefixes=//", defaults)
+ if err != nil {
+ t.Errorf("parseBlockOptions() = _, %v", err)
+ }
+ if diff := cmp.Diff(blockOptions{}, defaults, cmp.AllowUnexported(blockOptions{}), cmpopts.EquateEmpty()); diff != "" {
+ t.Errorf("defaults appear to have been modified (-want +got):\n%s", diff)
+ }
+}
+
+func TestBlockOptions_ClonesDefaultOptions_Reflection(t *testing.T) {
+ defaults := blockOptions{}
+ defaultOpts := reflect.ValueOf(&defaults).Elem()
+ var s []string
+ for i := 0; i < defaultOpts.NumField(); i++ {
+ val := defaultOpts.Field(i)
+ switch val.Kind() {
+ case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128, reflect.String:
+ continue
+ case reflect.Slice:
+ val.Set(reflect.MakeSlice(val.Type(), 0, 0))
+ s = append(s, fmt.Sprintf("%s=a,b,c", key(defaultOpts.Type().Field(i))))
+ case reflect.Map:
+ val.Set(reflect.MakeMap(val.Type()))
+ s = append(s, fmt.Sprintf("%s=a,b,c", key(defaultOpts.Type().Field(i))))
+ default:
+ t.Errorf("Option %q has unhandled type: %v", key(defaultOpts.Type().Field(i)), val.Type())
+ }
+
+ }
+ _, _ = parseBlockOptions(strings.Join(s, " "), defaults)
+ if diff := cmp.Diff(blockOptions{}, defaults, cmp.AllowUnexported(blockOptions{}), cmpopts.EquateEmpty()); diff != "" {
+ t.Errorf("defaults appear to have been modified (-want +got):\n%s", diff)
+ }
+}
diff --git a/keepsorted/options.go b/keepsorted/options.go
index 1e5a72c..a2ce6f0 100644
--- a/keepsorted/options.go
+++ b/keepsorted/options.go
@@ -25,6 +25,8 @@
"strconv"
"strings"
"unicode"
+
+ "golang.org/x/exp/maps"
)
var (
@@ -34,8 +36,32 @@
"no": false,
"false": false,
}
+ boolString = map[bool]string{
+ true: "yes",
+ false: "no",
+ }
)
+type BlockOptions struct {
+ opts blockOptions
+}
+
+func DefaultBlockOptions() BlockOptions {
+ return BlockOptions{defaultOptions}
+}
+
+func ParseBlockOptions(startLine string) (BlockOptions, error) {
+ opts, err := parseBlockOptions(startLine, blockOptions{})
+ if err != nil {
+ return BlockOptions{}, err
+ }
+ return BlockOptions{opts}, nil
+}
+
+func (opts BlockOptions) String() string {
+ return opts.opts.String()
+}
+
// blockOptions enable/disable extra features that control how a block of lines is sorted.
//
// Currently, only four types are supported:
@@ -97,12 +123,9 @@
StickyPrefixes: nil, // Will be populated with the comment marker of the start directive.
CaseSensitive: true,
RemoveDuplicates: true,
- // If we ever add a default map or slice or other reference type to this
- // struct, we'll want to make sure we're doing a deep copy in
- // parseBlockOptions.
}
-func (f *Fixer) parseBlockOptions(startLine string, defaults blockOptions) (blockOptions, error) {
+func parseBlockOptions(startLine string, defaults blockOptions) (blockOptions, error) {
ret := defaults
opts := reflect.ValueOf(&ret)
var errs error
@@ -133,7 +156,7 @@
ret.GroupPrefixes = nil
}
- if cm := f.guessCommentMarker(startLine); cm != "" {
+ if cm := guessCommentMarker(startLine); cm != "" {
ret.setCommentMarker(cm)
}
if len(ret.IgnorePrefixes) > 1 {
@@ -147,11 +170,7 @@
var errOptionNotSet = errors.New("not set in start directive")
func parseBlockOption(f reflect.StructField, startLine string) (reflect.Value, error) {
- key := strings.ToLower(f.Name)
- if k, ok := f.Tag.Lookup("key"); ok {
- key = k
- }
-
+ key := key(f)
regex := regexp.MustCompile(fmt.Sprintf(`(^|\s)%s=(?P<value>[^ ]+?)($|\s)`, regexp.QuoteMeta(key)))
if m := regex.FindStringSubmatchIndex(startLine); m != nil {
val := string(regex.ExpandString(nil, "${value}", startLine, m))
@@ -160,9 +179,17 @@
return reflect.Zero(f.Type), fmt.Errorf("option %q: %w", key, errOptionNotSet)
}
+func key(f reflect.StructField) string {
+ key := strings.ToLower(f.Name)
+ if k, ok := f.Tag.Lookup("key"); ok {
+ key = k
+ }
+ return key
+}
+
func parseValue(f reflect.StructField, key, val string) (reflect.Value, error) {
switch f.Type {
- case reflect.TypeOf(true):
+ case reflect.TypeOf(bool(false)):
b, ok := boolValues[val]
if !ok {
return reflect.Zero(f.Type), fmt.Errorf("option %q has unknown value %q", key, val)
@@ -186,7 +213,7 @@
m[s] = true
}
return reflect.ValueOf(m), nil
- case reflect.TypeOf(0):
+ case reflect.TypeOf(int(0)):
if val == "" {
return reflect.Zero(f.Type), nil
}
@@ -201,7 +228,24 @@
panic(fmt.Errorf("unsupported blockOptions type: %v", f.Type))
}
-func (f *Fixer) guessCommentMarker(startLine string) string {
+func formatValue(val reflect.Value) string {
+ switch val.Type() {
+ case reflect.TypeOf(bool(false)):
+ return boolString[val.Bool()]
+ case reflect.TypeOf([]string{}):
+ return strings.Join(val.Interface().([]string), ",")
+ case reflect.TypeOf(map[string]bool{}):
+ keys := maps.Keys(val.Interface().(map[string]bool))
+ slices.Sort(keys)
+ return strings.Join(keys, ",")
+ case reflect.TypeOf(int(0)):
+ return strconv.Itoa(int(val.Int()))
+ }
+
+ panic(fmt.Errorf("unsupported blockOptions type: %v", val.Type()))
+}
+
+func guessCommentMarker(startLine string) string {
startLine = strings.TrimSpace(startLine)
if strings.HasPrefix(startLine, "//") {
return "//"
@@ -229,6 +273,25 @@
}
}
+func (opts blockOptions) String() string {
+ var s []string
+ val := reflect.ValueOf(opts)
+ for i := 0; i < val.NumField(); i++ {
+ field := val.Type().Field(i)
+ if !field.IsExported() {
+ continue
+ }
+
+ fieldVal := val.FieldByIndex(field.Index)
+ if fieldVal.IsZero() {
+ continue
+ }
+ s = append(s, fmt.Sprintf("%s=%s", key(field), formatValue(fieldVal)))
+ }
+
+ return strings.Join(s, " ")
+}
+
// hasPrefix determines if s has one of the prefixes.
func hasPrefix(s string, prefixes map[string]bool) bool {
if len(prefixes) == 0 {