diff --git a/Documentation/Configuration.md b/Documentation/Configuration.md index 6f109210..774eea21 100644 --- a/Documentation/Configuration.md +++ b/Documentation/Configuration.md @@ -82,6 +82,9 @@ top-level keys and values: * `spacesAroundRangeFormationOperators` _(boolean)_: Determines whether whitespace should be forced before and after the range formation operators `...` and `..<`. +* `multiElementCollectionTrailingCommas` _(boolean)_: Determines whether multi-element collection literals should have trailing commas. + Defaults to `true`. + > TODO: Add support for enabling/disabling specific syntax transformations in > the pipeline. diff --git a/Sources/SwiftFormat/API/Configuration+Default.swift b/Sources/SwiftFormat/API/Configuration+Default.swift index 61ba05c3..3f0123fb 100644 --- a/Sources/SwiftFormat/API/Configuration+Default.swift +++ b/Sources/SwiftFormat/API/Configuration+Default.swift @@ -37,5 +37,6 @@ extension Configuration { self.indentSwitchCaseLabels = false self.spacesAroundRangeFormationOperators = false self.noAssignmentInExpressions = NoAssignmentInExpressionsConfiguration() + self.multiElementCollectionTrailingCommas = true } } diff --git a/Sources/SwiftFormat/API/Configuration.swift b/Sources/SwiftFormat/API/Configuration.swift index 13064c6e..6ef95969 100644 --- a/Sources/SwiftFormat/API/Configuration.swift +++ b/Sources/SwiftFormat/API/Configuration.swift @@ -42,6 +42,7 @@ public struct Configuration: Codable, Equatable { case rules case spacesAroundRangeFormationOperators case noAssignmentInExpressions + case multiElementCollectionTrailingCommas } /// A dictionary containing the default enabled/disabled states of rules, keyed by the rules' @@ -162,6 +163,29 @@ public struct Configuration: Codable, Equatable { /// Contains exceptions for the `NoAssignmentInExpressions` rule. public var noAssignmentInExpressions: NoAssignmentInExpressionsConfiguration + /// Determines if multi-element collection literals should have trailing commas. + /// + /// When `true` (default), the correct form is: + /// ```swift + /// let MyCollection = [1, 2,] + /// ... + /// let MyCollection = [ + /// "a": 1, + /// "b": 2, + /// ] + /// ``` + /// + /// When `false`, the correct form is: + /// ```swift + /// let MyCollection = [1, 2] + /// ... + /// let MyCollection = [ + /// "a": 1, + /// "b": 2 + /// ] + /// ``` + public var multiElementCollectionTrailingCommas: Bool + /// Constructs a Configuration by loading it from a configuration file. public init(contentsOf url: URL) throws { let data = try Data(contentsOf: url) @@ -239,6 +263,10 @@ public struct Configuration: Codable, Equatable { try container.decodeIfPresent( NoAssignmentInExpressionsConfiguration.self, forKey: .noAssignmentInExpressions) ?? defaults.noAssignmentInExpressions + self.multiElementCollectionTrailingCommas = + try container.decodeIfPresent( + Bool.self, forKey: .multiElementCollectionTrailingCommas) + ?? defaults.multiElementCollectionTrailingCommas // If the `rules` key is not present at all, default it to the built-in set // so that the behavior is the same as if the configuration had been @@ -271,6 +299,7 @@ public struct Configuration: Codable, Equatable { try container.encode(fileScopedDeclarationPrivacy, forKey: .fileScopedDeclarationPrivacy) try container.encode(indentSwitchCaseLabels, forKey: .indentSwitchCaseLabels) try container.encode(noAssignmentInExpressions, forKey: .noAssignmentInExpressions) + try container.encode(multiElementCollectionTrailingCommas, forKey: .multiElementCollectionTrailingCommas) try container.encode(rules, forKey: .rules) } diff --git a/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift b/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift index 97a1c5fc..8447ff8d 100644 --- a/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift +++ b/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift @@ -557,7 +557,7 @@ public class PrettyPrinter { // 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 + startLineNumber != openCloseBreakCompensatingLineNumber && !isSingleElement && configuration.multiElementCollectionTrailingCommas if shouldHaveTrailingComma && !hasTrailingComma { diagnose(.addTrailingComma, category: .trailingComma) } else if !shouldHaveTrailingComma && hasTrailingComma { diff --git a/Sources/_SwiftFormatTestSupport/Configuration+Testing.swift b/Sources/_SwiftFormatTestSupport/Configuration+Testing.swift index 36cd2971..e9ab39c3 100644 --- a/Sources/_SwiftFormatTestSupport/Configuration+Testing.swift +++ b/Sources/_SwiftFormatTestSupport/Configuration+Testing.swift @@ -40,6 +40,7 @@ extension Configuration { config.indentSwitchCaseLabels = false config.spacesAroundRangeFormationOperators = false config.noAssignmentInExpressions = NoAssignmentInExpressionsConfiguration() + config.multiElementCollectionTrailingCommas = true return config } } diff --git a/Tests/SwiftFormatTests/PrettyPrint/CommaTests.swift b/Tests/SwiftFormatTests/PrettyPrint/CommaTests.swift new file mode 100644 index 00000000..24bd0238 --- /dev/null +++ b/Tests/SwiftFormatTests/PrettyPrint/CommaTests.swift @@ -0,0 +1,289 @@ +import SwiftFormat + +final class CommaTests: PrettyPrintTestCase { + func testArrayCommasAbsentEnabled() { + let input = + """ + let MyCollection = [ + 1, + 2, + 3 + ] + + """ + + let expected = + """ + let MyCollection = [ + 1, + 2, + 3, + ] + + """ + + var configuration = Configuration.forTesting + configuration.multiElementCollectionTrailingCommas = true + assertPrettyPrintEqual(input: input, expected: expected, linelength: 20, configuration: configuration) + } + + func testArrayCommasAbsentDisabled() { + let input = + """ + let MyCollection = [ + 1, + 2, + 3 + ] + + """ + + let expected = + """ + let MyCollection = [ + 1, + 2, + 3 + ] + + """ + + var configuration = Configuration.forTesting + configuration.multiElementCollectionTrailingCommas = false + assertPrettyPrintEqual(input: input, expected: expected, linelength: 20, configuration: configuration) + } + + func testArrayCommasPresentEnabled() { + let input = + """ + let MyCollection = [ + 1, + 2, + 3, + ] + + """ + + let expected = + """ + let MyCollection = [ + 1, + 2, + 3, + ] + + """ + + var configuration = Configuration.forTesting + configuration.multiElementCollectionTrailingCommas = true + assertPrettyPrintEqual(input: input, expected: expected, linelength: 20, configuration: configuration) + } + + func testArrayCommasPresentDisabled() { + let input = + """ + let MyCollection = [ + 1, + 2, + 3, + ] + + """ + + let expected = + """ + let MyCollection = [ + 1, + 2, + 3 + ] + + """ + + var configuration = Configuration.forTesting + configuration.multiElementCollectionTrailingCommas = false + assertPrettyPrintEqual(input: input, expected: expected, linelength: 20, configuration: configuration) + } + + func testArraySingleLineCommasPresentDisabled() { + let input = + """ + let MyCollection = [1, 2, 3,] + + """ + + // no effect expected + let expected = + """ + let MyCollection = [1, 2, 3] + + """ + + var configuration = Configuration.forTesting + configuration.multiElementCollectionTrailingCommas = true + assertPrettyPrintEqual(input: input, expected: expected, linelength: 40, configuration: configuration) + } + + func testArraySingleLineCommasPresentEnabled() { + let input = + """ + let MyCollection = [1, 2, 3,] + + """ + + // no effect expected + let expected = + """ + let MyCollection = [1, 2, 3] + + """ + + var configuration = Configuration.forTesting + configuration.multiElementCollectionTrailingCommas = false + assertPrettyPrintEqual(input: input, expected: expected, linelength: 40, configuration: configuration) + } + + func testDictionaryCommasAbsentEnabled() { + 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.multiElementCollectionTrailingCommas = true + assertPrettyPrintEqual(input: input, expected: expected, linelength: 20, configuration: configuration) + } + + func testDictionaryCommasAbsentDisabled() { + 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.multiElementCollectionTrailingCommas = false + assertPrettyPrintEqual(input: input, expected: expected, linelength: 20, configuration: configuration) + } + + func testDictionaryCommasPresentEnabled() { + 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.multiElementCollectionTrailingCommas = true + assertPrettyPrintEqual(input: input, expected: expected, linelength: 20, configuration: configuration) + } + + func testDictionaryCommasPresentDisabled() { + 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.multiElementCollectionTrailingCommas = false + assertPrettyPrintEqual(input: input, expected: expected, linelength: 20, configuration: configuration) + } + + func testDictionarySingleLineCommasPresentDisabled() { + 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.multiElementCollectionTrailingCommas = true + assertPrettyPrintEqual(input: input, expected: expected, linelength: 40, configuration: configuration) + } + + func testDictionarySingleLineCommasPresentEnabled() { + 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.multiElementCollectionTrailingCommas = false + assertPrettyPrintEqual(input: input, expected: expected, linelength: 40, configuration: configuration) + } +}