From 5eae4e3649617e53dc3038aad9b4f0fde7ff9605 Mon Sep 17 00:00:00 2001 From: Jacob Drury <35348468+jacobdrury@users.noreply.github.com> Date: Mon, 24 Feb 2025 16:30:28 -0600 Subject: [PATCH] Add support to cuddle assignments for a whole block (#163) * Support to cuddle assignments for a whole block * Implement PR suggestions * Update doc/configuration.md Co-authored-by: Simon Sawert --------- Co-authored-by: Simon Sawert --- analyzer.go | 2 + doc/configuration.md | 103 ++++++++++++ doc/rules.md | 4 + .../cuddle_used_in_first_line.go | 142 ++++++++++++++++ .../cuddle_used_in_first_line.go.golden | 154 ++++++++++++++++++ .../cuddle_used_in_block.go | 147 +++++++++++++++++ .../cuddle_used_in_block.go.golden | 149 +++++++++++++++++ wsl.go | 72 ++++++-- wsl_test.go | 7 + 9 files changed, 768 insertions(+), 12 deletions(-) create mode 100644 testdata/src/default_config/cuddle_used_in_first_line/cuddle_used_in_first_line.go create mode 100644 testdata/src/default_config/cuddle_used_in_first_line/cuddle_used_in_first_line.go.golden create mode 100644 testdata/src/with_config/cuddle_used_in_block/cuddle_used_in_block.go create mode 100644 testdata/src/with_config/cuddle_used_in_block/cuddle_used_in_block.go.golden diff --git a/analyzer.go b/analyzer.go index e51df89..9be43d9 100644 --- a/analyzer.go +++ b/analyzer.go @@ -37,6 +37,7 @@ func defaultConfig() *Configuration { AllowCuddleWithRHS: []string{"Unlock", "RUnlock"}, ErrorVariableNames: []string{"err"}, ForceCaseTrailingWhitespaceLimit: 0, + AllowCuddleUsedInBlock: false, } } @@ -68,6 +69,7 @@ func (wa *wslAnalyzer) flags() flag.FlagSet { flags.BoolVar(&wa.config.ForceExclusiveShortDeclarations, "force-short-decl-cuddling", false, "Force short declarations to cuddle by themselves") flags.BoolVar(&wa.config.StrictAppend, "strict-append", true, "Strict rules for append") flags.BoolVar(&wa.config.IncludeGenerated, "include-generated", false, "Include generated files") + flags.BoolVar(&wa.config.AllowCuddleUsedInBlock, "allow-cuddle-used-in-block", false, "Allow cuddling of variables used in block statements") flags.IntVar(&wa.config.ForceCaseTrailingWhitespaceLimit, "force-case-trailing-whitespace", 0, "Force newlines for case blocks > this number.") flags.Var(&multiStringValue{slicePtr: &wa.config.AllowCuddleWithCalls}, "allow-cuddle-with-calls", "Comma separated list of idents that can have cuddles after") diff --git a/doc/configuration.md b/doc/configuration.md index a13cb25..149c9ce 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -233,6 +233,109 @@ var ( ) ``` +### allow-cuddle-used-in-block + +Controls if you may cuddle variables used anywhere in the following block. + +> Default value: false + +Supported when true: + +```go +counter := 0 +if somethingTrue { + checker := getAChecker() + if !checker { + return + } + + counter++ +} + +var numbers []int +for i := 0; i < 10; i++ { + if 1 == 1 { + numbers = append(numbers, i) + } +} + +var numbers2 []int +for { + if 1 == 1 { + numbers2 = append(numbers2, 1) + } +} + +var id string +switch { +case int: + if true { + id = strconv.Itoa(i) + } +case uint32: + if true { + id = strconv.Itoa(int(i)) + } +case string: + if true { + id = i + } +} +``` + +Required when false: + +```go +counter := 0 + +if somethingTrue { + checker := getAChecker() + if !checker { + return + } + + counter++ +} + +var numbers []int + +for i := 0; i < 10; i++ { + if 1 == 1 { + numbers = append(numbers, i) + } +} + +var numbers2 []int + +for { + if 1 == 1 { + numbers2 = append(numbers2, 1) + } +} + +var id string + +switch { +case int: + if true { + id = strconv.Itoa(i) + } +case uint32: + if true { + id = strconv.Itoa(int(i)) + } +case string: + if true { + id = i + } +} +``` + +**Note**: this means the option _overrides_ the following rules: +- [Anonymous switch statements should never be cuddled](rules.md#anonymous-switch-statements-should-never-be-cuddled) +- [For statement without condition should never be cuddled](rules.md#for-statement-without-condition-should-never-be-cuddled) + + ### [allow-trailing-comment](rules.md#block-should-not-end-with-a-whitespace-or-comment) Controls if blocks can end with comments. This is not encouraged sine it's diff --git a/doc/rules.md b/doc/rules.md index 27498f4..1e2357b 100644 --- a/doc/rules.md +++ b/doc/rules.md @@ -6,6 +6,8 @@ linter and how they should be resolved or configured to handle. ## Checklist ### Anonymous switch statements should never be cuddled +> Can be configured, see [configuration +documentation](configuration.md#allow-cuddle-used-in-block) Anonymous `switch` statements (mindless `switch`) should deserve its needed attention that it does not need any assigned variables. Hence, it should not @@ -398,6 +400,8 @@ run() --- ### For statement without condition should never be cuddled +> Can be configured, see [configuration +documentation](configuration.md#allow-cuddle-used-in-block) `for` loop without conditions (infinity loop) should deserves its own attention. Hence, it should not be cuddled with anyone. diff --git a/testdata/src/default_config/cuddle_used_in_first_line/cuddle_used_in_first_line.go b/testdata/src/default_config/cuddle_used_in_first_line/cuddle_used_in_first_line.go new file mode 100644 index 0000000..2bc0043 --- /dev/null +++ b/testdata/src/default_config/cuddle_used_in_first_line/cuddle_used_in_first_line.go @@ -0,0 +1,142 @@ +package testpkg + +import ( + "fmt" + "strconv" +) + +func ForCuddleAssignmentWholeBlock() { + x := 1 + y := 2 + + var numbers []int + for i := 0; i < 10; i++ { // want "for statements should only be cuddled with assignments used in the iteration" + if x == y { + numbers = append(numbers, i) + } + } + + var numbers2 []int + for i := range 10 { // want "ranges should only be cuddled with assignments used in the iteration" + if x == y { + numbers2 = append(numbers2, i) + } + } + + var numbers3 []int + for { // want "for statement without condition should never be cuddled" + if x == y { + numbers3 = append(numbers3, i) + } + } + + environment = make(map[string]string) + for _, env := range req.GetConfig().GetEnvs() { // want "ranges should only be cuddled with assignments used in the iteration" + switch env.GetKey() { + case "user-data": + cloudInitUserData = env.GetValue() + default: + environment[env.GetKey()] = env.GetValue() + } + } +} + +func IfCuddleAssignmentWholeBlock() { + x := 1 + y := 2 + + counter := 0 + if somethingTrue { // want "if statements should only be cuddled with assignments used in the if statement itself" + checker := getAChecker() + if !checker { + return + } + + counter++ // Cuddled variable used in block, but not as first statement + } + + var number2 []int + if x == y { // want "if statements should only be cuddled with assignments used in the if statement itself" + fmt.Println("a") + } else { + if x > y { + number2 = append(number2, i) + } + } + + var number3 []int + if x == y { // want "if statements should only be cuddled with assignments used in the if statement itself" + fmt.Println("a") + } else if x > y { + if x == y { + number3 = append(number3, i) + } + } + + var number4 []int + if x == y { // want "if statements should only be cuddled with assignments used in the if statement itself" + if x == y { + number4 = append(number4, i) + } + } else if x > y { + if x == y { + number4 = append(number4, i) + } + } else { + if x > y { + number4 = append(number4, i) + } + } +} + +func SwitchCuddleAssignmentWholeBlock() { + var id string + var b bool // want "declarations should never be cuddled" + switch b { // want "only one cuddle assignment allowed before switch statement" + case true: + id = "a" + case false: + id = "b" + } + + var b bool + var id string // want "declarations should never be cuddled" + switch b { // want "only one cuddle assignment allowed before switch statement" + case true: + id = "a" + case false: + id = "b" + } + + var id2 string + switch i := objectID.(type) { // want "type switch statements should only be cuddled with variables switched" + case int: + if true { + id2 = strconv.Itoa(i) + } + case uint32: + if true { + id2 = strconv.Itoa(int(i)) + } + case string: + if true { + id2 = i + } + } + + var id3 string + switch { // want "anonymous switch statements should never be cuddled" + case int: + if true { + id3 = strconv.Itoa(i) + } + case uint32: + if true { + id3 = strconv.Itoa(int(i)) + } + case string: + if true { + id3 = i + } + } +} diff --git a/testdata/src/default_config/cuddle_used_in_first_line/cuddle_used_in_first_line.go.golden b/testdata/src/default_config/cuddle_used_in_first_line/cuddle_used_in_first_line.go.golden new file mode 100644 index 0000000..8998afa --- /dev/null +++ b/testdata/src/default_config/cuddle_used_in_first_line/cuddle_used_in_first_line.go.golden @@ -0,0 +1,154 @@ +package testpkg + +import ( + "fmt" + "strconv" +) + +func ForCuddleAssignmentWholeBlock() { + x := 1 + y := 2 + + var numbers []int + + for i := 0; i < 10; i++ { // want "for statements should only be cuddled with assignments used in the iteration" + if x == y { + numbers = append(numbers, i) + } + } + + var numbers2 []int + + for i := range 10 { // want "ranges should only be cuddled with assignments used in the iteration" + if x == y { + numbers2 = append(numbers2, i) + } + } + + var numbers3 []int + + for { // want "for statement without condition should never be cuddled" + if x == y { + numbers3 = append(numbers3, i) + } + } + + environment = make(map[string]string) + + for _, env := range req.GetConfig().GetEnvs() { // want "ranges should only be cuddled with assignments used in the iteration" + switch env.GetKey() { + case "user-data": + cloudInitUserData = env.GetValue() + default: + environment[env.GetKey()] = env.GetValue() + } + } +} + +func IfCuddleAssignmentWholeBlock() { + x := 1 + y := 2 + + counter := 0 + + if somethingTrue { // want "if statements should only be cuddled with assignments used in the if statement itself" + checker := getAChecker() + if !checker { + return + } + + counter++ // Cuddled variable used in block, but not as first statement + } + + var number2 []int + + if x == y { // want "if statements should only be cuddled with assignments used in the if statement itself" + fmt.Println("a") + } else { + if x > y { + number2 = append(number2, i) + } + } + + var number3 []int + + if x == y { // want "if statements should only be cuddled with assignments used in the if statement itself" + fmt.Println("a") + } else if x > y { + if x == y { + number3 = append(number3, i) + } + } + + var number4 []int + + if x == y { // want "if statements should only be cuddled with assignments used in the if statement itself" + if x == y { + number4 = append(number4, i) + } + } else if x > y { + if x == y { + number4 = append(number4, i) + } + } else { + if x > y { + number4 = append(number4, i) + } + } +} + +func SwitchCuddleAssignmentWholeBlock() { + var id string + + var b bool // want "declarations should never be cuddled" + switch b { // want "only one cuddle assignment allowed before switch statement" + case true: + id = "a" + case false: + id = "b" + } + + var b bool + + var id string // want "declarations should never be cuddled" + switch b { // want "only one cuddle assignment allowed before switch statement" + case true: + id = "a" + case false: + id = "b" + } + + var id2 string + + switch i := objectID.(type) { // want "type switch statements should only be cuddled with variables switched" + case int: + if true { + id2 = strconv.Itoa(i) + } + case uint32: + if true { + id2 = strconv.Itoa(int(i)) + } + case string: + if true { + id2 = i + } + } + + var id3 string + + switch { // want "anonymous switch statements should never be cuddled" + case int: + if true { + id3 = strconv.Itoa(i) + } + case uint32: + if true { + id3 = strconv.Itoa(int(i)) + } + case string: + if true { + id3 = i + } + } +} diff --git a/testdata/src/with_config/cuddle_used_in_block/cuddle_used_in_block.go b/testdata/src/with_config/cuddle_used_in_block/cuddle_used_in_block.go new file mode 100644 index 0000000..b4a316c --- /dev/null +++ b/testdata/src/with_config/cuddle_used_in_block/cuddle_used_in_block.go @@ -0,0 +1,147 @@ +package testpkg + +import ( + "fmt" + "strconv" +) + +func ForCuddleAssignmentWholeBlock() { + x := 1 + y := 2 + + var numbers []int + for i := 0; i < 10; i++ { + if x == y { + numbers = append(numbers, i) + } + } + + var numbers2 []int + for i := range 10 { + if x == y { + numbers2 = append(numbers2, i) + } + } + + var numbers3 []int + for { + if x == y { + numbers3 = append(numbers3, 1) + } + } + + var numbers4 []int + for { + numbers4 = append(numbers4, 1) + } + + environment = make(map[string]string) + for _, env := range req.GetConfig().GetEnvs() { + switch env.GetKey() { + case "user-data": + cloudInitUserData = env.GetValue() + default: + environment[env.GetKey()] = env.GetValue() + } + } +} + +func IfCuddleAssignmentWholeBlock() { + x := 1 + y := 2 + + counter := 0 + if somethingTrue { + checker := getAChecker() + if !checker { + return + } + + counter++ // Cuddled variable used in block, but not as first statement + } + + var number2 []int + if x == y { + fmt.Println("a") + } else { + if x > y { + number2 = append(number2, i) + } + } + + var number3 []int + if x == y { + fmt.Println("a") + } else if x > y { + if x == y { + number3 = append(number3, i) + } + } + + var number4 []int + if x == y { + if x == y { + number4 = append(number4, i) + } + } else if x > y { + if x == y { + number4 = append(number4, i) + } + } else { + if x > y { + number4 = append(number4, i) + } + } +} + +func SwitchCuddleAssignmentWholeBlock() { + var id string + var b bool // want "declarations should never be cuddled" + switch b { // want "only one cuddle assignment allowed before switch statement" + case true: + id = "a" + case false: + id = "b" + } + + var b bool + var id string // want "declarations should never be cuddled" + switch b { // want "only one cuddle assignment allowed before switch statement" + case true: + id = "a" + case false: + id = "b" + } + + var id2 string + switch i := objectID.(type) { + case int: + if true { + id2 = strconv.Itoa(i) + } + case uint32: + if true { + id2 = strconv.Itoa(int(i)) + } + case string: + if true { + id2 = i + } + } + + var id3 string + switch { + case int: + if true { + id3 = strconv.Itoa(i) + } + case uint32: + if true { + id3 = strconv.Itoa(int(i)) + } + case string: + if true { + id3 = i + } + } +} diff --git a/testdata/src/with_config/cuddle_used_in_block/cuddle_used_in_block.go.golden b/testdata/src/with_config/cuddle_used_in_block/cuddle_used_in_block.go.golden new file mode 100644 index 0000000..03f1213 --- /dev/null +++ b/testdata/src/with_config/cuddle_used_in_block/cuddle_used_in_block.go.golden @@ -0,0 +1,149 @@ +package testpkg + +import ( + "fmt" + "strconv" +) + +func ForCuddleAssignmentWholeBlock() { + x := 1 + y := 2 + + var numbers []int + for i := 0; i < 10; i++ { + if x == y { + numbers = append(numbers, i) + } + } + + var numbers2 []int + for i := range 10 { + if x == y { + numbers2 = append(numbers2, i) + } + } + + var numbers3 []int + for { + if x == y { + numbers3 = append(numbers3, 1) + } + } + + var numbers4 []int + for { + numbers4 = append(numbers4, 1) + } + + environment = make(map[string]string) + for _, env := range req.GetConfig().GetEnvs() { + switch env.GetKey() { + case "user-data": + cloudInitUserData = env.GetValue() + default: + environment[env.GetKey()] = env.GetValue() + } + } +} + +func IfCuddleAssignmentWholeBlock() { + x := 1 + y := 2 + + counter := 0 + if somethingTrue { + checker := getAChecker() + if !checker { + return + } + + counter++ // Cuddled variable used in block, but not as first statement + } + + var number2 []int + if x == y { + fmt.Println("a") + } else { + if x > y { + number2 = append(number2, i) + } + } + + var number3 []int + if x == y { + fmt.Println("a") + } else if x > y { + if x == y { + number3 = append(number3, i) + } + } + + var number4 []int + if x == y { + if x == y { + number4 = append(number4, i) + } + } else if x > y { + if x == y { + number4 = append(number4, i) + } + } else { + if x > y { + number4 = append(number4, i) + } + } +} + +func SwitchCuddleAssignmentWholeBlock() { + var id string + + var b bool // want "declarations should never be cuddled" + switch b { // want "only one cuddle assignment allowed before switch statement" + case true: + id = "a" + case false: + id = "b" + } + + var b bool + + var id string // want "declarations should never be cuddled" + switch b { // want "only one cuddle assignment allowed before switch statement" + case true: + id = "a" + case false: + id = "b" + } + + var id2 string + switch i := objectID.(type) { + case int: + if true { + id2 = strconv.Itoa(i) + } + case uint32: + if true { + id2 = strconv.Itoa(int(i)) + } + case string: + if true { + id2 = i + } + } + + var id3 string + switch { + case int: + if true { + id3 = strconv.Itoa(i) + } + case uint32: + if true { + id3 = strconv.Itoa(int(i)) + } + case string: + if true { + id3 = i + } + } +} diff --git a/wsl.go b/wsl.go index 44c7abe..abfad93 100644 --- a/wsl.go +++ b/wsl.go @@ -179,6 +179,18 @@ type Configuration struct { // errors even for generated files. Can be useful when developing // generators. IncludeGenerated bool + + // AllowCuddleUsedInBlock will allowing cuddling of variables with block statements + // if they are used anywhere in the block. This defaults to false but setting + // it to true will allow the following example: + // + // var numbers []int + // for i := 0; i < 10; i++ { + // if 1 == 1 { + // numbers = append(numbers, i) + // } + // } + AllowCuddleUsedInBlock bool } // fix is a range to fixup. @@ -304,12 +316,18 @@ func (p *processor) parseBlockStatements(statements []ast.Stmt) { } } - // We could potentially have a block which require us to check the first - // argument before ruling out an allowed cuddle. - var calledOrAssignedFirstInBlock []string + // Contains the union of all variable names used anywhere + // within the block body (if applicable) and is used to check + // if a preceding statement's variables are actually used within + // the block. This helps enforce rules about allowed cuddling. + var identifiersUsedInBlock []string if firstBodyStatement != nil { - calledOrAssignedFirstInBlock = append(p.findLHS(firstBodyStatement), p.findRHS(firstBodyStatement)...) + if p.config.AllowCuddleUsedInBlock { + identifiersUsedInBlock = p.findUsedVariablesInStatement(stmt) + } else { + identifiersUsedInBlock = append(p.findLHS(firstBodyStatement), p.findRHS(firstBodyStatement)...) + } } var ( @@ -426,7 +444,7 @@ func (p *processor) parseBlockStatements(statements []ast.Stmt) { reportNewlineTwoLinesAbove := func(n1, n2 ast.Node, reason string) { if atLeastOneInListsMatch(rightAndLeftHandSide, assignedOnLineAbove) || - atLeastOneInListsMatch(assignedOnLineAbove, calledOrAssignedFirstInBlock) { + atLeastOneInListsMatch(assignedOnLineAbove, identifiersUsedInBlock) { // If both the assignment on the line above _and_ the assignment // two lines above is part of line or first in block, add the // newline as if non were. @@ -435,7 +453,7 @@ func (p *processor) parseBlockStatements(statements []ast.Stmt) { if isAssignmentTwoLinesAbove && (atLeastOneInListsMatch(rightAndLeftHandSide, assignedTwoLinesAbove) || - atLeastOneInListsMatch(assignedTwoLinesAbove, calledOrAssignedFirstInBlock)) { + atLeastOneInListsMatch(assignedTwoLinesAbove, identifiersUsedInBlock)) { p.addWhitespaceBeforeError(n1, reason) } else { // If the variable on the line above is allowed to be @@ -507,7 +525,7 @@ func (p *processor) parseBlockStatements(statements []ast.Stmt) { continue } - if atLeastOneInListsMatch(assignedOnLineAbove, calledOrAssignedFirstInBlock) { + if atLeastOneInListsMatch(assignedOnLineAbove, identifiersUsedInBlock) { continue } @@ -611,7 +629,7 @@ func (p *processor) parseBlockStatements(statements []ast.Stmt) { } if !atLeastOneInListsMatch(rightAndLeftHandSide, assignedOnLineAbove) { - if !atLeastOneInListsMatch(assignedOnLineAbove, calledOrAssignedFirstInBlock) { + if !atLeastOneInListsMatch(assignedOnLineAbove, identifiersUsedInBlock) { p.addWhitespaceBeforeError(t, reasonRangeCuddledWithoutUse) } } @@ -679,7 +697,7 @@ func (p *processor) parseBlockStatements(statements []ast.Stmt) { } } - if atLeastOneInListsMatch(assignedOnLineAbove, calledOrAssignedFirstInBlock) { + if atLeastOneInListsMatch(assignedOnLineAbove, identifiersUsedInBlock) { continue } @@ -687,7 +705,7 @@ func (p *processor) parseBlockStatements(statements []ast.Stmt) { p.addWhitespaceBeforeError(t, reasonDeferCuddledWithOtherVar) } case *ast.ForStmt: - if len(rightAndLeftHandSide) == 0 { + if len(rightAndLeftHandSide) == 0 && !p.config.AllowCuddleUsedInBlock { p.addWhitespaceBeforeError(t, reasonForWithoutCondition) continue } @@ -701,7 +719,7 @@ func (p *processor) parseBlockStatements(statements []ast.Stmt) { // comments regarding variable usages on the line before or as the // first line in the block for details. if !atLeastOneInListsMatch(rightAndLeftHandSide, assignedOnLineAbove) { - if !atLeastOneInListsMatch(assignedOnLineAbove, calledOrAssignedFirstInBlock) { + if !atLeastOneInListsMatch(assignedOnLineAbove, identifiersUsedInBlock) { p.addWhitespaceBeforeError(t, reasonForCuddledAssignWithoutUse) } } @@ -742,6 +760,10 @@ func (p *processor) parseBlockStatements(statements []ast.Stmt) { if !atLeastOneInListsMatch(rightAndLeftHandSide, assignedOnLineAbove) { if len(rightAndLeftHandSide) == 0 { + if p.config.AllowCuddleUsedInBlock { + continue + } + p.addWhitespaceBeforeError(t, reasonAnonSwitchCuddled) } else { p.addWhitespaceBeforeError(t, reasonSwitchCuddledWithoutUse) @@ -757,7 +779,7 @@ func (p *processor) parseBlockStatements(statements []ast.Stmt) { if !atLeastOneInListsMatch(rightHandSide, assignedOnLineAbove) { // Allow type assertion on variables used in the first case // immediately. - if !atLeastOneInListsMatch(assignedOnLineAbove, calledOrAssignedFirstInBlock) { + if !atLeastOneInListsMatch(assignedOnLineAbove, identifiersUsedInBlock) { p.addWhitespaceBeforeError(t, reasonTypeSwitchCuddledWithoutUse) } } @@ -839,6 +861,32 @@ func (p *processor) firstBodyStatement(i int, allStmt []ast.Stmt) ast.Node { return firstBodyStatement } +// findUsedVariablesInStatement processes a statement, +// returning a union of all variables used within it. +func (p *processor) findUsedVariablesInStatement(stmt ast.Stmt) []string { + var ( + used []string + seen = map[string]struct{}{} + ) + + // ast.Inspect walks the AST of the statement. + ast.Inspect(stmt, func(n ast.Node) bool { + // We're only interested in identifiers. + if ident, ok := n.(*ast.Ident); ok { + if _, exists := seen[ident.Name]; !exists { + seen[ident.Name] = struct{}{} + + used = append(used, ident.Name) + } + } + + // Continue walking the AST. + return true + }) + + return used +} + func (p *processor) findLHS(node ast.Node) []string { var lhs []string diff --git a/wsl_test.go b/wsl_test.go index 0070d6f..2ea5e5e 100644 --- a/wsl_test.go +++ b/wsl_test.go @@ -31,6 +31,7 @@ func TestDefaultConfig(t *testing.T) { {dir: "multiline_case"}, {dir: "remove_whitespace"}, {dir: "line_directive"}, + {dir: "cuddle_used_in_first_line_block"}, } for _, test := range testCases { @@ -123,6 +124,12 @@ func TestWithConfig(t *testing.T) { config.IncludeGenerated = true }, }, + { + subdir: "cuddle_used_in_block", + configFn: func(config *Configuration) { + config.AllowCuddleUsedInBlock = true + }, + }, } { t.Run(tc.subdir, func(t *testing.T) { config := defaultConfig()