diff --git a/.github/workflows/automerge.yml b/.github/workflows/automerge.yml new file mode 100644 index 000000000..1cd94a7e0 --- /dev/null +++ b/.github/workflows/automerge.yml @@ -0,0 +1,50 @@ +name: Create PR to merge main into release branch + +on: + schedule: + - cron: '0 0 * * MON' + workflow_dispatch: + # To stop creating these weekly PRs, disable the workflow as described in https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-workflow-runs/disabling-and-enabling-a-workflow + +jobs: + create_merge_pr: + name: Create PR to merge main into release branch + runs-on: ubuntu-latest + steps: + - name: Set up variables + id: variables + run: | + echo "release_branch=release/6.2" >> "$GITHUB_OUTPUT" + echo "pr_branch=automerge/merge-main-$(date +%Y-%m-%d)" >> "$GITHUB_OUTPUT" + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Create merge commit + id: create_merge_commit + run: | + # Without this, we can't perform git operations in GitHub actions. + git config --global --add safe.directory "$(realpath .)" + git config --local user.name 'swift-ci' + git config --local user.email 'swift-ci@users.noreply.github.com' + + git checkout ${{ steps.variables.outputs.release_branch }} + git merge main + + if [[ "$(git rev-parse HEAD)" = "$(git rev-parse main)" ]]; then + echo "has_merged_commits=true" >> "$GITHUB_OUTPUT" + else + echo "has_merged_commits=false" >> "$GITHUB_OUTPUT" + fi + - name: Push branch and create PR + id: push_branch + if: ${{ steps.create_merge_commit.outputs.has_merged_commits }} + env: + GH_TOKEN: ${{ github.token }} + run: | + git checkout -b "${{ steps.variables.outputs.pr_branch }}" + git push --set-upstream origin "${{ steps.variables.outputs.pr_branch }}" + + gh pr create -B "${{ steps.variables.outputs.release_branch }}" -H "${{ steps.variables.outputs.pr_branch }}" \ + --title 'Merge `main` into `${{ steps.variables.outputs.release_branch }}`' \ + --body 'This PR was automatically opened by a GitHub action. Review the changes included in this PR and determine if they should be included in the release branch. If yes, merge the PR. Otherwise revert changes that should not be included on this branch.' diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index ab7f274bf..b09ca0d56 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -4,6 +4,10 @@ on: pull_request: types: [opened, reopened, synchronize] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: tests: name: Test 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