diff --git a/Documentation/Configuration.md b/Documentation/Configuration.md index dbdb171a..724960a2 100644 --- a/Documentation/Configuration.md +++ b/Documentation/Configuration.md @@ -194,6 +194,22 @@ switch someValue { --- +### `trailingCommasInMultilineLists` +**type:** `string` + +**description:** Determines how trailing commas in comma-separated lists should be handled during formatting. + +- If set to `"always"`, a trailing comma is always added in multi-line lists. +- If set to `"never"`, trailing commas are removed even in multi-line contexts. +- If set to `"ignore"` (the default), existing commas are preserved as-is, and for collections, the behavior falls back to the `multiElementCollectionTrailingCommas`. + +This option takes precedence over `multiElementCollectionTrailingCommas`, unless it is set to `"ignore"`. + + +**default:** `"ignore"` + +--- + ### `multiElementCollectionTrailingCommas` **type:** boolean diff --git a/Sources/SwiftFormat/API/Configuration+Default.swift b/Sources/SwiftFormat/API/Configuration+Default.swift index 1af06a12..16c7f597 100644 --- a/Sources/SwiftFormat/API/Configuration+Default.swift +++ b/Sources/SwiftFormat/API/Configuration+Default.swift @@ -39,6 +39,7 @@ extension Configuration { self.indentSwitchCaseLabels = false self.spacesAroundRangeFormationOperators = false self.noAssignmentInExpressions = NoAssignmentInExpressionsConfiguration() + self.trailingCommasInMultilineLists = .ignore self.multiElementCollectionTrailingCommas = true self.reflowMultilineStringLiterals = .never self.indentBlankLines = false diff --git a/Sources/SwiftFormat/API/Configuration.swift b/Sources/SwiftFormat/API/Configuration.swift index 98a1b9b7..80b83a9f 100644 --- a/Sources/SwiftFormat/API/Configuration.swift +++ b/Sources/SwiftFormat/API/Configuration.swift @@ -44,6 +44,7 @@ public struct Configuration: Codable, Equatable { case rules case spacesAroundRangeFormationOperators case noAssignmentInExpressions + case trailingCommasInMultilineLists case multiElementCollectionTrailingCommas case reflowMultilineStringLiterals case indentBlankLines @@ -173,6 +174,22 @@ public struct Configuration: Codable, Equatable { /// Contains exceptions for the `NoAssignmentInExpressions` rule. public var noAssignmentInExpressions: NoAssignmentInExpressionsConfiguration + /// Determines how trailing commas in comma-separated lists should be handled during formatting. + public enum TrailingCommasInMultilineLists: String, Codable { + case always + case never + case ignore + } + + /// Determines how trailing commas in comma-separated lists are handled during formatting. + /// + /// This setting takes precedence over `multiElementCollectionTrailingCommas`. + /// If set to `.ignore` (the default), the formatter defers to `multiElementCollectionTrailingCommas` + /// for collections only. In all other cases, existing trailing commas are preserved as-is and not modified. + /// If set to `.always` or `.never`, that behavior is applied uniformly across all list types, + /// regardless of `multiElementCollectionTrailingCommas`. + public var trailingCommasInMultilineLists: TrailingCommasInMultilineLists + /// Determines if multi-element collection literals should have trailing commas. /// /// When `true` (default), the correct form is: @@ -384,6 +401,9 @@ public struct Configuration: Codable, Equatable { forKey: .noAssignmentInExpressions ) ?? defaults.noAssignmentInExpressions + self.trailingCommasInMultilineLists = + try container.decodeIfPresent(TrailingCommasInMultilineLists.self, forKey: .trailingCommasInMultilineLists) + ?? defaults.trailingCommasInMultilineLists self.multiElementCollectionTrailingCommas = try container.decodeIfPresent( Bool.self, diff --git a/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift b/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift index b8700014..3b40fe6b 100644 --- a/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift +++ b/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift @@ -501,7 +501,7 @@ public class PrettyPrinter { case .commaDelimitedRegionStart: commaDelimitedRegionStack.append(openCloseBreakCompensatingLineNumber) - case .commaDelimitedRegionEnd(let hasTrailingComma, let isSingleElement): + case .commaDelimitedRegionEnd(let isCollection, let hasTrailingComma, let isSingleElement): guard let startLineNumber = commaDelimitedRegionStack.popLast() else { fatalError("Found trailing comma end with no corresponding start.") } @@ -511,17 +511,30 @@ public class PrettyPrinter { // types) from a literal (where the elements are the contents of a collection instance). // We never want to add a trailing comma in an initializer so we disable trailing commas on // single element collections. - let shouldHaveTrailingComma = - startLineNumber != openCloseBreakCompensatingLineNumber && !isSingleElement - && configuration.multiElementCollectionTrailingCommas - if shouldHaveTrailingComma && !hasTrailingComma { - diagnose(.addTrailingComma, category: .trailingComma) - } else if !shouldHaveTrailingComma && hasTrailingComma { - diagnose(.removeTrailingComma, category: .trailingComma) - } + let shouldHandleCommaDelimitedRegion: Bool? = + switch configuration.trailingCommasInMultilineLists { + case .always: + true + case .never: + false + case .ignore: + isCollection ? configuration.multiElementCollectionTrailingCommas : nil + } + if let shouldHandleCommaDelimitedRegion { + let shouldHaveTrailingComma = + startLineNumber != openCloseBreakCompensatingLineNumber && !isSingleElement + && shouldHandleCommaDelimitedRegion + if shouldHaveTrailingComma && !hasTrailingComma { + diagnose(.addTrailingComma, category: .trailingComma) + } else if !shouldHaveTrailingComma && hasTrailingComma { + diagnose(.removeTrailingComma, category: .trailingComma) + } - let shouldWriteComma = whitespaceOnly ? hasTrailingComma : shouldHaveTrailingComma - if shouldWriteComma { + let shouldWriteComma = whitespaceOnly ? hasTrailingComma : shouldHaveTrailingComma + if shouldWriteComma { + outputBuffer.write(",") + } + } else if hasTrailingComma { outputBuffer.write(",") } @@ -686,15 +699,28 @@ public class PrettyPrinter { case .commaDelimitedRegionStart: lengths.append(0) - case .commaDelimitedRegionEnd(_, let isSingleElement): + case .commaDelimitedRegionEnd(let isCollection, _, let isSingleElement): // The token's length is only necessary when a comma will be printed, but it's impossible to // know at this point whether the region-start token will be on the same line as this token. // Without adding this length to the total, it would be possible for this comma to be // printed in column `maxLineLength`. Unfortunately, this can cause breaks to fire // unnecessarily when the enclosed tokens comma would fit within `maxLineLength`. - let length = isSingleElement ? 0 : 1 - total += length - lengths.append(length) + let shouldHandleCommaDelimitedRegion: Bool? = + switch configuration.trailingCommasInMultilineLists { + case .always: + true + case .never: + false + case .ignore: + isCollection ? configuration.multiElementCollectionTrailingCommas : nil + } + if let shouldHandleCommaDelimitedRegion, shouldHandleCommaDelimitedRegion { + let length = isSingleElement ? 0 : 1 + total += length + lengths.append(length) + } else { + lengths.append(0) + } case .enableFormatting, .disableFormatting: // no effect on length calculations diff --git a/Sources/SwiftFormat/PrettyPrint/Token.swift b/Sources/SwiftFormat/PrettyPrint/Token.swift index 654f09c5..547ed056 100644 --- a/Sources/SwiftFormat/PrettyPrint/Token.swift +++ b/Sources/SwiftFormat/PrettyPrint/Token.swift @@ -194,7 +194,7 @@ enum Token { /// Marks the end of a comma delimited collection, where a trailing comma should be inserted /// if and only if the collection spans multiple lines and has multiple elements. - case commaDelimitedRegionEnd(hasTrailingComma: Bool, isSingleElement: Bool) + case commaDelimitedRegionEnd(isCollection: Bool, hasTrailingComma: Bool, isSingleElement: Bool) /// Starts a scope where `contextual` breaks have consistent behavior. case contextualBreakingStart diff --git a/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift b/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift index d5be2264..2eb03bf3 100644 --- a/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift +++ b/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift @@ -922,9 +922,19 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { } override func visit(_ node: LabeledExprListSyntax) -> SyntaxVisitorContinueKind { - // Intentionally do nothing here. Since `TupleExprElement`s are used both in tuple expressions - // and function argument lists, which need to be formatted, differently, those nodes manually - // loop over the nodes and arrange them in those contexts. + if let lastElement = node.last { + if let trailingComma = lastElement.trailingComma { + ignoredTokens.insert(trailingComma) + } + before(node.first?.firstToken(viewMode: .sourceAccurate), tokens: .commaDelimitedRegionStart) + let endToken = + Token.commaDelimitedRegionEnd( + isCollection: false, + hasTrailingComma: lastElement.trailingComma != nil, + isSingleElement: node.first == lastElement + ) + after(lastElement.expression.lastToken(viewMode: .sourceAccurate), tokens: [endToken]) + } return .visitChildren } @@ -974,6 +984,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { before(node.first?.firstToken(viewMode: .sourceAccurate), tokens: .commaDelimitedRegionStart) let endToken = Token.commaDelimitedRegionEnd( + isCollection: true, hasTrailingComma: lastElement.trailingComma != nil, isSingleElement: node.first == lastElement ) @@ -1018,6 +1029,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { before(node.first?.firstToken(viewMode: .sourceAccurate), tokens: .commaDelimitedRegionStart) let endToken = Token.commaDelimitedRegionEnd( + isCollection: true, hasTrailingComma: lastElement.trailingComma != nil, isSingleElement: node.first == node.last ) @@ -1291,6 +1303,27 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { return .visitChildren } + override func visit(_ node: ClosureCaptureListSyntax) -> SyntaxVisitorContinueKind { + if let lastElement = node.last { + if let trailingComma = lastElement.trailingComma { + ignoredTokens.insert(trailingComma) + } + before(node.first?.firstToken(viewMode: .sourceAccurate), tokens: .commaDelimitedRegionStart) + let endToken = + Token.commaDelimitedRegionEnd( + isCollection: false, + hasTrailingComma: lastElement.trailingComma != nil, + isSingleElement: node.first == lastElement + ) + if lastElement.initializer != nil { + after(lastElement.initializer?.lastToken(viewMode: .sourceAccurate), tokens: [endToken]) + } else { + after(lastElement.name.lastToken(viewMode: .sourceAccurate), tokens: [endToken]) + } + } + return .visitChildren + } + override func visit(_ node: ClosureCaptureSyntax) -> SyntaxVisitorContinueKind { before(node.firstToken(viewMode: .sourceAccurate), tokens: .open) after(node.specifier?.lastToken(viewMode: .sourceAccurate), tokens: .break) @@ -1405,6 +1438,27 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { return .visitChildren } + override func visit(_ node: EnumCaseParameterListSyntax) -> SyntaxVisitorContinueKind { + if let lastElement = node.last { + if let trailingComma = lastElement.trailingComma { + ignoredTokens.insert(trailingComma) + } + before(node.first?.firstToken(viewMode: .sourceAccurate), tokens: .commaDelimitedRegionStart) + let endToken = + Token.commaDelimitedRegionEnd( + isCollection: false, + hasTrailingComma: lastElement.trailingComma != nil, + isSingleElement: node.first == lastElement + ) + if lastElement.defaultValue != nil { + after(lastElement.defaultValue?.lastToken(viewMode: .sourceAccurate), tokens: [endToken]) + } else { + after(lastElement.type.lastToken(viewMode: .sourceAccurate), tokens: [endToken]) + } + } + return .visitChildren + } + override func visit(_ node: FunctionParameterClauseSyntax) -> SyntaxVisitorContinueKind { // Prioritize keeping ") throws -> " together. We can only do this if the function // has arguments. @@ -1417,6 +1471,29 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { return .visitChildren } + override func visit(_ node: FunctionParameterListSyntax) -> SyntaxVisitorContinueKind { + if let lastElement = node.last { + if let trailingComma = lastElement.trailingComma { + ignoredTokens.insert(trailingComma) + } + before(node.first?.firstToken(viewMode: .sourceAccurate), tokens: .commaDelimitedRegionStart) + let endToken = + Token.commaDelimitedRegionEnd( + isCollection: false, + hasTrailingComma: lastElement.trailingComma != nil, + isSingleElement: node.first == lastElement + ) + if lastElement.defaultValue != nil { + after(lastElement.defaultValue?.lastToken(viewMode: .sourceAccurate), tokens: [endToken]) + } else if lastElement.ellipsis != nil { + after(lastElement.ellipsis?.lastToken(viewMode: .sourceAccurate), tokens: [endToken]) + } else { + after(lastElement.type.lastToken(viewMode: .sourceAccurate), tokens: [endToken]) + } + } + return .visitChildren + } + override func visit(_ node: ClosureParameterSyntax) -> SyntaxVisitorContinueKind { before(node.firstToken(viewMode: .sourceAccurate), tokens: .open) arrangeAttributeList(node.attributes) @@ -1722,6 +1799,28 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { return .visitChildren } + override func visit(_ node: GenericParameterListSyntax) -> SyntaxVisitorContinueKind { + if let lastElement = node.last { + if let trailingComma = lastElement.trailingComma { + ignoredTokens.insert(trailingComma) + } + before(node.first?.firstToken(viewMode: .sourceAccurate), tokens: .commaDelimitedRegionStart) + let endToken = + Token.commaDelimitedRegionEnd( + isCollection: false, + hasTrailingComma: lastElement.trailingComma != nil, + isSingleElement: node.first == lastElement + ) + + if lastElement.inheritedType != nil { + after(lastElement.inheritedType?.lastToken(viewMode: .sourceAccurate), tokens: [endToken]) + } else { + after(lastElement.name.lastToken(viewMode: .sourceAccurate), tokens: [endToken]) + } + } + return .visitChildren + } + override func visit(_ node: PrimaryAssociatedTypeClauseSyntax) -> SyntaxVisitorContinueKind { after(node.leftAngle, tokens: .break(.open, size: 0), .open(argumentListConsistency())) before(node.rightAngle, tokens: .break(.close, size: 0), .close) @@ -1772,6 +1871,24 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { return .visitChildren } + override func visit(_ node: TuplePatternElementListSyntax) -> SyntaxVisitorContinueKind { + if let lastElement = node.last { + if let trailingComma = lastElement.trailingComma { + ignoredTokens.insert(trailingComma) + } + before(node.first?.firstToken(viewMode: .sourceAccurate), tokens: .commaDelimitedRegionStart) + let endToken = + Token.commaDelimitedRegionEnd( + isCollection: false, + hasTrailingComma: lastElement.trailingComma != nil, + isSingleElement: node.first == lastElement + ) + + after(lastElement.pattern.lastToken(viewMode: .sourceAccurate), tokens: [endToken]) + } + return .visitChildren + } + override func visit(_ node: TryExprSyntax) -> SyntaxVisitorContinueKind { before( node.expression.firstToken(viewMode: .sourceAccurate), diff --git a/Tests/SwiftFormatTests/PrettyPrint/CommaTests.swift b/Tests/SwiftFormatTests/PrettyPrint/CommaTests.swift index fb817884..65d02696 100644 --- a/Tests/SwiftFormatTests/PrettyPrint/CommaTests.swift +++ b/Tests/SwiftFormatTests/PrettyPrint/CommaTests.swift @@ -394,4 +394,748 @@ final class CommaTests: PrettyPrintTestCase { configuration.multiElementCollectionTrailingCommas = false assertPrettyPrintEqual(input: input, expected: expected, linelength: 40, configuration: configuration) } + + func testAlwaysTrailingCommasInGenericParameterList() { + let input = + """ + struct S< + T1, + T2, + T3 + > {} + + struct S< + T1, + T2, + T3: Foo + > {} + + """ + + let expected = + """ + struct S< + T1, + T2, + T3, + > {} + + struct S< + T1, + T2, + T3: Foo, + > {} + + """ + + var configuration = Configuration.forTesting + configuration.trailingCommasInMultilineLists = .always + assertPrettyPrintEqual(input: input, expected: expected, linelength: 40, configuration: configuration) + } + + func testAlwaysTrailingCommasInTuple() { + let input = + """ + let velocity = ( + 1.66007664274403694e-03, + 7.69901118419740425e-03, + 6.90460016972063023e-05 + ) + + let ( + velocityX, + velocityY, + velocityZ + ) = velocity + + """ + + let expected = + """ + let velocity = ( + 1.66007664274403694e-03, + 7.69901118419740425e-03, + 6.90460016972063023e-05, + ) + + let ( + velocityX, + velocityY, + velocityZ, + ) = velocity + + """ + + var configuration = Configuration.forTesting + configuration.trailingCommasInMultilineLists = .always + assertPrettyPrintEqual(input: input, expected: expected, linelength: 80, configuration: configuration) + } + + func testAlwaysTrailingCommasInFunction() { + let input = + """ + func foo( + input1: Int = 0, + input2: Int = 0 + ) {} + + func foo( + input1: Int = 0, + input2: Int + ) {} + + func foo( + input1: Int = 0, + input2: Int... + ) {} + + foo( + input1: 1, + input2: 1 + ) + """ + + let expected = + """ + func foo( + input1: Int = 0, + input2: Int = 0, + ) {} + + func foo( + input1: Int = 0, + input2: Int, + ) {} + + func foo( + input1: Int = 0, + input2: Int..., + ) {} + + foo( + input1: 1, + input2: 1, + ) + + """ + + var configuration = Configuration.forTesting + configuration.trailingCommasInMultilineLists = .always + assertPrettyPrintEqual(input: input, expected: expected, linelength: 40, configuration: configuration) + } + + func testAlwaysTrailingCommasInInitializer() { + let input = + """ + struct S { + init( + input1: Int = 0, + input2: Int = 0 + ) {} + } + + """ + + let expected = + """ + struct S { + init( + input1: Int = 0, + input2: Int = 0, + ) {} + } + + """ + + var configuration = Configuration.forTesting + configuration.trailingCommasInMultilineLists = .always + assertPrettyPrintEqual(input: input, expected: expected, linelength: 40, configuration: configuration) + } + + func testAlwaysTrailingCommasInEnumeration() { + let input = + """ + enum E { + case foo( + input1: Int = 0, + input2: Int = 0 + ) + } + + enum E { + case foo( + input1: Int = 0, + input2: Int + ) + } + + """ + + let expected = + """ + enum E { + case foo( + input1: Int = 0, + input2: Int = 0, + ) + } + + enum E { + case foo( + input1: Int = 0, + input2: Int, + ) + } + + """ + + var configuration = Configuration.forTesting + configuration.trailingCommasInMultilineLists = .always + assertPrettyPrintEqual(input: input, expected: expected, linelength: 40, configuration: configuration) + } + + func testAlwaysTrailingCommasInAttribute() { + let input = + """ + @Foo( + "input 1", + "input 2", + "input 3" + ) + struct S {} + + """ + + let expected = + """ + @Foo( + "input 1", + "input 2", + "input 3", + ) + struct S {} + + """ + + var configuration = Configuration.forTesting + configuration.trailingCommasInMultilineLists = .always + assertPrettyPrintEqual(input: input, expected: expected, linelength: 40, configuration: configuration) + } + + func testAlwaysTrailingCommasInMacro() { + let input = + """ + struct S { + #foo( + "input 1", + "input 2", + "input 3" + ) + } + + """ + + let expected = + """ + struct S { + #foo( + "input 1", + "input 2", + "input 3", + ) + } + + """ + + var configuration = Configuration.forTesting + configuration.trailingCommasInMultilineLists = .always + assertPrettyPrintEqual(input: input, expected: expected, linelength: 40, configuration: configuration) + } + + func testAlwaysTrailingCommasInKeyPath() { + let input = + #""" + let value = m[ + x, + y + ] + + let keyPath = \Foo.bar[ + x, + y + ] + + f(\.[ + x, + y + ]) + + """# + + let expected = + #""" + let value = m[ + x, + y, + ] + + let keyPath = + \Foo.bar[ + x, + y, + ] + + f( + \.[ + x, + y, + ]) + + """# + + var configuration = Configuration.forTesting + configuration.trailingCommasInMultilineLists = .always + assertPrettyPrintEqual(input: input, expected: expected, linelength: 40, configuration: configuration) + } + + func testAlwaysTrailingCommasInClosureCapture() { + let input = + """ + { + [ + capturedValue1, + capturedValue2 + ] in + } + + { + [ + capturedValue1, + capturedValue2 = foo + ] in + } + + """ + + let expected = + """ + { + [ + capturedValue1, + capturedValue2, + ] in + } + + { + [ + capturedValue1, + capturedValue2 = foo, + ] in + } + + """ + + var configuration = Configuration.forTesting + configuration.trailingCommasInMultilineLists = .always + assertPrettyPrintEqual(input: input, expected: expected, linelength: 40, configuration: configuration) + } + + func testNeverTrailingCommasInGenericParameterList() { + let input = + """ + struct S< + T1, + T2, + T3, + > {} + + struct S< + T1, + T2, + T3: Foo, + > {} + + """ + + let expected = + """ + struct S< + T1, + T2, + T3 + > {} + + struct S< + T1, + T2, + T3: Foo + > {} + + """ + + var configuration = Configuration.forTesting + configuration.trailingCommasInMultilineLists = .never + assertPrettyPrintEqual(input: input, expected: expected, linelength: 40, configuration: configuration) + } + + func testNeverTrailingCommasInTuple() { + let input = + """ + let velocity = ( + 1.66007664274403694e-03, + 7.69901118419740425e-03, + 6.90460016972063023e-05, + ) + + let ( + velocityX, + velocityY, + velocityZ, + ) = velocity + + """ + + let expected = + """ + let velocity = ( + 1.66007664274403694e-03, + 7.69901118419740425e-03, + 6.90460016972063023e-05 + ) + + let ( + velocityX, + velocityY, + velocityZ + ) = velocity + + """ + + var configuration = Configuration.forTesting + configuration.trailingCommasInMultilineLists = .never + assertPrettyPrintEqual(input: input, expected: expected, linelength: 80, configuration: configuration) + } + + func testNeverTrailingCommasInFunction() { + let input = + """ + func foo( + input1: Int = 0, + input2: Int = 0, + ) {} + + func foo( + input1: Int = 0, + input2: Int, + ) {} + + func foo( + input1: Int = 0, + input2: Int..., + ) {} + + foo( + input1: 1, + input2: 1, + ) + """ + + let expected = + """ + func foo( + input1: Int = 0, + input2: Int = 0 + ) {} + + func foo( + input1: Int = 0, + input2: Int + ) {} + + func foo( + input1: Int = 0, + input2: Int... + ) {} + + foo( + input1: 1, + input2: 1 + ) + + """ + + var configuration = Configuration.forTesting + configuration.trailingCommasInMultilineLists = .never + assertPrettyPrintEqual(input: input, expected: expected, linelength: 40, configuration: configuration) + } + + func testNeverTrailingCommasInInitializer() { + let input = + """ + struct S { + init( + input1: Int = 0, + input2: Int = 0, + ) {} + } + + """ + + let expected = + """ + struct S { + init( + input1: Int = 0, + input2: Int = 0 + ) {} + } + + """ + + var configuration = Configuration.forTesting + configuration.trailingCommasInMultilineLists = .never + assertPrettyPrintEqual(input: input, expected: expected, linelength: 40, configuration: configuration) + } + + func testNeverTrailingCommasInEnumeration() { + let input = + """ + enum E { + case foo( + input1: Int = 0, + input2: Int = 0, + ) + } + + enum E { + case foo( + input1: Int = 0, + input2: Int, + ) + } + + """ + + let expected = + """ + enum E { + case foo( + input1: Int = 0, + input2: Int = 0 + ) + } + + enum E { + case foo( + input1: Int = 0, + input2: Int + ) + } + + """ + + var configuration = Configuration.forTesting + configuration.trailingCommasInMultilineLists = .never + assertPrettyPrintEqual(input: input, expected: expected, linelength: 40, configuration: configuration) + } + + func testNeverTrailingCommasInAttribute() { + let input = + """ + @Foo( + "input 1", + "input 2", + "input 3", + ) + struct S {} + + """ + + let expected = + """ + @Foo( + "input 1", + "input 2", + "input 3" + ) + struct S {} + + """ + + var configuration = Configuration.forTesting + configuration.trailingCommasInMultilineLists = .never + assertPrettyPrintEqual(input: input, expected: expected, linelength: 40, configuration: configuration) + } + + func testNeverTrailingCommasInMacro() { + let input = + """ + struct S { + #foo( + "input 1", + "input 2", + "input 3", + ) + } + + """ + + let expected = + """ + struct S { + #foo( + "input 1", + "input 2", + "input 3" + ) + } + + """ + + var configuration = Configuration.forTesting + configuration.trailingCommasInMultilineLists = .never + assertPrettyPrintEqual(input: input, expected: expected, linelength: 40, configuration: configuration) + } + + func testNeverTrailingCommasInKeyPath() { + let input = + #""" + let value = m[ + x, + y, + ] + + let keyPath = \Foo.bar[ + x, + y, + ] + + f(\.[ + x, + y, + ]) + + """# + + let expected = + #""" + let value = m[ + x, + y + ] + + let keyPath = + \Foo.bar[ + x, + y + ] + + f( + \.[ + x, + y + ]) + + """# + + var configuration = Configuration.forTesting + configuration.trailingCommasInMultilineLists = .never + assertPrettyPrintEqual(input: input, expected: expected, linelength: 40, configuration: configuration) + } + + func testNeverTrailingCommasInClosureCapture() { + let input = + """ + { + [ + capturedValue1, + capturedValue2, + ] in + } + + { + [ + capturedValue1, + capturedValue2 = foo, + ] in + } + + """ + + let expected = + """ + { + [ + capturedValue1, + capturedValue2 + ] in + } + + { + [ + capturedValue1, + capturedValue2 = foo + ] in + } + + """ + + var configuration = Configuration.forTesting + configuration.trailingCommasInMultilineLists = .never + assertPrettyPrintEqual(input: input, expected: expected, linelength: 40, configuration: configuration) + } + + func testAlwaysTrailingCommasInMultilineListsOverridesMultiElementCollectionTrailingCommas() { + let input = + """ + let MyCollection = [ + "a": 1, + "b": 2, + "c": 3, + ] + + """ + + let expected = + """ + let MyCollection = [ + "a": 1, + "b": 2, + "c": 3, + ] + + """ + + var configuration = Configuration.forTesting + configuration.trailingCommasInMultilineLists = .always + configuration.multiElementCollectionTrailingCommas = false + assertPrettyPrintEqual(input: input, expected: expected, linelength: 20, configuration: configuration) + } + + func testNeverTrailingCommasInMultilineListsOverridesMultiElementCollectionTrailingCommas() { + let input = + """ + let MyCollection = [ + "a": 1, + "b": 2, + "c": 3, + ] + + """ + + let expected = + """ + let MyCollection = [ + "a": 1, + "b": 2, + "c": 3 + ] + + """ + + var configuration = Configuration.forTesting + configuration.trailingCommasInMultilineLists = .never + configuration.multiElementCollectionTrailingCommas = true + assertPrettyPrintEqual(input: input, expected: expected, linelength: 20, configuration: configuration) + } }