diff --git a/Documentation/Configuration.md b/Documentation/Configuration.md index 8decd52e6..dbdb171a3 100644 --- a/Documentation/Configuration.md +++ b/Documentation/Configuration.md @@ -204,7 +204,30 @@ switch someValue { --- ### `reflowMultilineStringLiterals` -**type:** `string` + +> [!NOTE] +> This setting should be specified as a string value (e.g. `"never"`) +> For backward compatibility with swift-format version 601.0.0, the configuration also accepts the legacy object format where the setting is specified as an object with a single key (e.g., ⁠`{ "never": {} }`). + +**type:** `string` or `object` (legacy) + +**example:** + +For all versions above 601.0.0, the configuration should be specified as a string, for example: +```json +{ + "reflowMultilineStringLiterals": "never" +} +``` + +For version 601.0.0, the configuration should be specified as an object, for example: +```json +{ + "reflowMultilineStringLiterals": { + "never": {} + } +} +``` **description:** Determines how multiline string literals should reflow when formatted. diff --git a/Sources/SwiftFormat/API/Configuration.swift b/Sources/SwiftFormat/API/Configuration.swift index ac1c742d8..cb5dfb025 100644 --- a/Sources/SwiftFormat/API/Configuration.swift +++ b/Sources/SwiftFormat/API/Configuration.swift @@ -197,7 +197,7 @@ public struct Configuration: Codable, Equatable { public var multiElementCollectionTrailingCommas: Bool /// Determines how multiline string literals should reflow when formatted. - public enum MultilineStringReflowBehavior: Codable { + public enum MultilineStringReflowBehavior: String, Codable { /// Never reflow multiline string literals. case never /// Reflow lines in string literal that exceed the maximum line length. For example with a line length of 10: @@ -241,20 +241,36 @@ public struct Configuration: Codable, Equatable { case always var isNever: Bool { - switch self { - case .never: - return true - default: - return false - } + self == .never } var isAlways: Bool { + self == .always + } + } + + /// A private enum created to maintain backward compatibility with swift-format version 601.0.0, + /// which had a `MultilineStringReflowBehavior` enum without a String raw type. + /// + /// In version 601.0.0, the `reflowMultilineStringLiterals` configuration was encoded as an object + /// with a single key (e.g., `{ "never": {} }`) rather than as a string (e.g., `"never"`). This + /// enum allows decoding from both formats: + /// - First, we attempt to decode as a String using `MultilineStringReflowBehavior` + /// - If that fails, we fall back to this legacy format + /// - If both attempts fail, an error will be thrown + /// + /// This approach preserves compatibility without requiring a configuration version bump. + private enum LegacyMultilineStringReflowBehavior: Codable { + case never + case onlyLinesOverLength + case always + + /// Converts this legacy enum to the corresponding `MultilineStringReflowBehavior` value. + func toMultilineStringReflowBehavior() -> MultilineStringReflowBehavior { switch self { - case .always: - return true - default: - return false + case .never: .never + case .always: .always + case .onlyLinesOverLength: .onlyLinesOverLength } } } @@ -371,9 +387,31 @@ public struct Configuration: Codable, Equatable { ) ?? defaults.multiElementCollectionTrailingCommas - self.reflowMultilineStringLiterals = - try container.decodeIfPresent(MultilineStringReflowBehavior.self, forKey: .reflowMultilineStringLiterals) - ?? defaults.reflowMultilineStringLiterals + self.reflowMultilineStringLiterals = try { + // Try to decode `reflowMultilineStringLiterals` as a string + // This handles configurations using the String raw value format (e.g. "never"). + // If an error occurs, we'll silently bypass it and fall back to the legacy behavior. + if let behavior = try? container.decodeIfPresent( + MultilineStringReflowBehavior.self, + forKey: .reflowMultilineStringLiterals + ) { + return behavior + } + + // If the modern format fails, try to decode as an object with a single key. + // This handles configurations from swift-format v601.0.0 (e.g. { "never": {} }). + // If an error occurs in this step, we'll propagate it to the caller. + if let legacyBehavior = try container.decodeIfPresent( + LegacyMultilineStringReflowBehavior.self, + forKey: .reflowMultilineStringLiterals + ) { + return legacyBehavior.toMultilineStringReflowBehavior() + } + + // If the key is not present in the configuration at all, use the default value. + return defaults.reflowMultilineStringLiterals + }() + self.indentBlankLines = try container.decodeIfPresent( Bool.self, diff --git a/Tests/SwiftFormatTests/API/ConfigurationTests.swift b/Tests/SwiftFormatTests/API/ConfigurationTests.swift index 8fd982f5d..9c6977db8 100644 --- a/Tests/SwiftFormatTests/API/ConfigurationTests.swift +++ b/Tests/SwiftFormatTests/API/ConfigurationTests.swift @@ -64,4 +64,44 @@ final class ConfigurationTests: XCTestCase { let path = #"\\mount\test.swift"# XCTAssertNil(Configuration.url(forConfigurationFileApplyingTo: URL(fileURLWithPath: path))) } + + func testDecodingReflowMultilineStringLiteralsAsString() throws { + let testCases: [String: Configuration.MultilineStringReflowBehavior] = [ + "never": .never, + "always": .always, + "onlyLinesOverLength": .onlyLinesOverLength, + ] + + for (jsonString, expectedBehavior) in testCases { + let jsonData = """ + { + "reflowMultilineStringLiterals": "\(jsonString)" + } + """.data(using: .utf8)! + + let config = try JSONDecoder().decode(Configuration.self, from: jsonData) + XCTAssertEqual(config.reflowMultilineStringLiterals, expectedBehavior) + } + } + + func testDecodingReflowMultilineStringLiteralsAsObject() throws { + + let testCases: [String: Configuration.MultilineStringReflowBehavior] = [ + "{ \"never\": {} }": .never, + "{ \"always\": {} }": .always, + "{ \"onlyLinesOverLength\": {} }": .onlyLinesOverLength, + ] + + for (jsonString, expectedBehavior) in testCases { + let jsonData = """ + { + "reflowMultilineStringLiterals": \(jsonString) + } + """.data(using: .utf8)! + + let config = try JSONDecoder().decode(Configuration.self, from: jsonData) + XCTAssertEqual(config.reflowMultilineStringLiterals, expectedBehavior) + } + } + } diff --git a/api-breakages.txt b/api-breakages.txt index 0b04b73c8..519c9a091 100644 --- a/api-breakages.txt +++ b/api-breakages.txt @@ -13,4 +13,8 @@ API breakage: enum Finding.Severity has been removed API breakage: var Finding.severity has been removed API breakage: var FindingCategorizing.defaultSeverity has been removed API breakage: var FindingCategorizing.defaultSeverity has been removed -API breakage: func Rule.diagnose(_:on:severity:anchor:notes:) has been renamed to func diagnose(_:on:anchor:notes:) \ No newline at end of file +API breakage: func Rule.diagnose(_:on:severity:anchor:notes:) has been renamed to func diagnose(_:on:anchor:notes:) +API breakage: func Configuration.MultilineStringReflowBehavior.hash(into:) has been removed +API breakage: func Configuration.MultilineStringReflowBehavior.encode(to:) has been removed +API breakage: var Configuration.MultilineStringReflowBehavior.hashValue has been removed +API breakage: constructor Configuration.MultilineStringReflowBehavior.init(from:) has been removed