diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9425750d3..a32f35b71 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -321,10 +321,44 @@ jobs: echo "✅ All changed Swift files are properly formatted" fi + lint: + name: SwiftLint + runs-on: macos-26 + timeout-minutes: 10 + env: + SWIFTLINT_VERSION: "0.65.0" + SWIFTLINT_SHA256: "d6cb0aa7a2f5f1ef306fc9e37bcb54dc9a26facc8f7784ac0c3dd3eccf5c6ba6" + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - name: Cache SwiftLint binary + id: cache-swiftlint + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: /usr/local/bin/swiftlint + key: swiftlint-${{ env.SWIFTLINT_VERSION }}-${{ runner.os }} + - name: Install SwiftLint ${{ env.SWIFTLINT_VERSION }} + if: steps.cache-swiftlint.outputs.cache-hit != 'true' + run: | + curl -fL \ + "https://github.com/realm/SwiftLint/releases/download/${SWIFTLINT_VERSION}/portable_swiftlint.zip" \ + -o /tmp/swiftlint.zip + echo "${SWIFTLINT_SHA256} /tmp/swiftlint.zip" | shasum -a 256 --check + unzip -q -o /tmp/swiftlint.zip -d /tmp/swiftlint + sudo mv /tmp/swiftlint/swiftlint /usr/local/bin/swiftlint + - name: Verify SwiftLint version + run: | + INSTALLED=$(swiftlint version) + if [ "$INSTALLED" != "$SWIFTLINT_VERSION" ]; then + echo "Expected SwiftLint $SWIFTLINT_VERSION, got $INSTALLED" && exit 1 + fi + echo "SwiftLint $INSTALLED" + - name: Run SwiftLint + run: swiftlint lint --strict + ci-success: name: CI Success if: always() - needs: [macos, macos-legacy, spm, linux, integration-tests, library-evolution, examples, spell-check, docs, format-check] + needs: [macos, macos-legacy, spm, linux, integration-tests, library-evolution, examples, spell-check, docs, format-check, lint] runs-on: ubuntu-latest steps: - name: Check all jobs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2e9aba347..764b24a5e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,6 +7,7 @@ on: branches: - main - release/* + - v3 workflow_dispatch: permissions: diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 000000000..d1aa10eb2 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,47 @@ +# SwiftLint configuration for supabase-swift +# https://github.com/realm/SwiftLint +# +# Pinned version: 0.65.0 +# Install locally: brew install swiftlint +# CI pins the exact binary via portable_swiftlint.zip from GitHub releases. +# +# Only rules that currently trigger violations are disabled. Enable them +# one-by-one in follow-up PRs once the violations are fixed or configured. + +included: + - Sources + - Tests + +excluded: + - .build + - Tests/IntegrationTests/supabase + - "**/__Snapshots__" + +# Rules disabled because they currently have violations. +# Remove entries here as violations are fixed in follow-up PRs. +disabled_rules: + - class_delegate_protocol + - closure_parameter_position + - comment_spacing + - cyclomatic_complexity + - file_length + - for_where + - force_cast + - force_try + - function_body_length + - function_name_whitespace + - function_parameter_count + - identifier_name + - implicit_optional_initialization + - line_length + - multiple_closures_with_trailing_closure + - nesting + - non_optional_string_data_conversion + - opening_brace + - optional_data_string_conversion + - orphaned_doc_comment + - redundant_discardable_let + - todo + - trailing_comma + - type_body_length + - type_name diff --git a/AGENTS.md b/AGENTS.md index d268685ea..41e9d20a4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -86,6 +86,21 @@ npm ci --prefix tools/node # one-time setup (re-run only when tools/node/packa Legitimate technical terms and project-specific words go in `dictionary.txt` at the repository root. +### Linting + +```bash +# Lint Sources and Tests (fails on any new violation) +swiftlint lint --strict + +# Autocorrect violations that SwiftLint can fix +swiftlint lint --fix +``` + +This uses [SwiftLint](https://github.com/realm/SwiftLint) for code-smell and +correctness rules; `swift-format` remains the source of truth for formatting, +so the SwiftLint config (`.swiftlint.yml`) disables the purely stylistic rules +that overlap with it. Install SwiftLint locally with `brew install swiftlint`. + ### Documentation ```bash @@ -297,6 +312,7 @@ supabase stop ## Important Notes for AI Coding Agents - Always run `./scripts/format.sh` before committing Swift code +- Run `swiftlint lint --strict` before committing; it must not report new violations - Ensure new public APIs have DocC documentation comments - Add tests for all new functionality - Keep changes minimal and focused diff --git a/Sources/Helpers/Base64URL.swift b/Sources/Helpers/Base64URL.swift index daf3fec9a..3e19f3de8 100644 --- a/Sources/Helpers/Base64URL.swift +++ b/Sources/Helpers/Base64URL.swift @@ -17,7 +17,7 @@ package enum Base64URL { let paddingLength = requiredLength - length if paddingLength > 0 { let padding = "".padding(toLength: Int(paddingLength), withPad: "=", startingAt: 0) - base64 = base64 + padding + base64 += padding } return Data(base64Encoded: base64, options: .ignoreUnknownCharacters) } diff --git a/Sources/RealtimeV2/WebSocket/URLSessionWebSocket.swift b/Sources/RealtimeV2/WebSocket/URLSessionWebSocket.swift index 9a4c50f18..d09c187f3 100644 --- a/Sources/RealtimeV2/WebSocket/URLSessionWebSocket.swift +++ b/Sources/RealtimeV2/WebSocket/URLSessionWebSocket.swift @@ -74,7 +74,7 @@ final class URLSessionWebSocket: WebSocket { let session = URLSession.sessionWithConfiguration( configuration ?? .default, - onComplete: { session, task, error in + onComplete: { _, _, error in mutableState.withValue { if let webSocket = $0.webSocket { // There are three possibilities here: @@ -111,7 +111,7 @@ final class URLSessionWebSocket: WebSocket { $0.continuation.resume(returning: $0.webSocket!) } }, - onWebSocketTaskClosed: { session, task, code, reason in + onWebSocketTaskClosed: { _, _, code, reason in mutableState.withValue { assert($0.webSocket != nil, "connection should exist by this time") $0.webSocket!._connectionClosed(code: code, reason: reason) diff --git a/Sources/TestHelpers/URLRequestSnapshot.swift b/Sources/TestHelpers/URLRequestSnapshot.swift index e9e59e0f6..7c69cd5df 100644 --- a/Sources/TestHelpers/URLRequestSnapshot.swift +++ b/Sources/TestHelpers/URLRequestSnapshot.swift @@ -15,8 +15,8 @@ #endif extension Snapshotting where Value == URLRequest, Format == String { - /// A snapshot strategy for comparing requests based on a cURL representation. - /// + // A snapshot strategy for comparing requests based on a cURL representation. + // // ``` swift // assertSnapshot(of: request, as: .curl) // ``` diff --git a/Tests/FunctionsTests/FunctionsClientTests.swift b/Tests/FunctionsTests/FunctionsClientTests.swift index ae72229f3..1bf327e9b 100644 --- a/Tests/FunctionsTests/FunctionsClientTests.swift +++ b/Tests/FunctionsTests/FunctionsClientTests.swift @@ -38,11 +38,6 @@ final class FunctionsClientTests: XCTestCase { sessionConfiguration: sessionConfiguration ) - override func setUp() { - super.setUp() - // isRecording = true - } - func testInit() async { let client = FunctionsClient( url: url, diff --git a/Tests/IntegrationTests/RealtimeIntegrationTests.swift b/Tests/IntegrationTests/RealtimeIntegrationTests.swift index 126cb3829..7d2343b15 100644 --- a/Tests/IntegrationTests/RealtimeIntegrationTests.swift +++ b/Tests/IntegrationTests/RealtimeIntegrationTests.swift @@ -785,7 +785,7 @@ // Verify both clients can decode presence // Note: Due to timing, exact presence changes may vary, but structure should be correct - XCTAssertTrue(presenceChanges1.count > 0, "Client 1 should receive presence changes") + XCTAssertTrue(!presenceChanges1.isEmpty, "Client 1 should receive presence changes") // Verify messages were received by both clients XCTAssertEqual(messages1.count, 3, "Client 1 should receive all 3 messages") @@ -816,7 +816,7 @@ // Verify user 1 leaving is detected by user 2 // Note: Due to timing, exact presence changes may vary, but structure should be correct - XCTAssertTrue(presenceChanges2.count > 0, "Client 2 should receive presence changes") + XCTAssertTrue(!presenceChanges2.isEmpty, "Client 2 should receive presence changes") // Cleanup await channel1.unsubscribe() diff --git a/Tests/PostgRESTTests/PostgrestFilterBuilderTests.swift b/Tests/PostgRESTTests/PostgrestFilterBuilderTests.swift index e9787894a..f5eb9bb83 100644 --- a/Tests/PostgRESTTests/PostgrestFilterBuilderTests.swift +++ b/Tests/PostgRESTTests/PostgrestFilterBuilderTests.swift @@ -12,11 +12,6 @@ import XCTest final class PostgrestFilterBuilderTests: PostgrestQueryTests { - override func setUp() { - super.setUp() - // isRecording = true - } - func testNotFilter() async throws { Mock( url: url.appendingPathComponent("users"), diff --git a/Tests/PostgRESTTests/PostgrestQueryBuilderTests.swift b/Tests/PostgRESTTests/PostgrestQueryBuilderTests.swift index cb8d935ec..b0d356756 100644 --- a/Tests/PostgRESTTests/PostgrestQueryBuilderTests.swift +++ b/Tests/PostgRESTTests/PostgrestQueryBuilderTests.swift @@ -12,10 +12,6 @@ import TestHelpers import XCTest final class PostgrestQueryBuilderTests: PostgrestQueryTests { - override func setUp() { - super.setUp() - // isRecording = true - } func testSetAuth() { XCTAssertNil(sut.configuration.headers["Authorization"]) diff --git a/Tests/RealtimeTests/RealtimeTests.swift b/Tests/RealtimeTests/RealtimeTests.swift index 1864c00f6..b6440ed37 100644 --- a/Tests/RealtimeTests/RealtimeTests.swift +++ b/Tests/RealtimeTests/RealtimeTests.swift @@ -76,7 +76,7 @@ import XCTest "custom.access.token" } ), - wsTransport: { url, headers in + wsTransport: { url, _ in assertInlineSnapshot(of: url, as: .description) { """ ws://localhost:54321/realtime/v1/websocket?apikey=publishable.api.key&vsn=2.0.0&log_level=warn diff --git a/Tests/StorageTests/StorageFileAPITests.swift b/Tests/StorageTests/StorageFileAPITests.swift index 3fd915502..22da4ce07 100644 --- a/Tests/StorageTests/StorageFileAPITests.swift +++ b/Tests/StorageTests/StorageFileAPITests.swift @@ -454,7 +454,7 @@ final class StorageFileAPITests: XCTestCase { do { try await storage.from("bucket") .move(from: "source", to: "destination") - XCTFail() + XCTFail("Expected StorageError to be thrown") } catch let error as StorageError { XCTAssertEqual(error.message, "Error") } @@ -485,7 +485,7 @@ final class StorageFileAPITests: XCTestCase { do { try await storage.from("bucket") .move(from: "source", to: "destination") - XCTFail() + XCTFail("Expected HTTPError to be thrown") } catch let error as HTTPError { XCTAssertEqual(error.data, Data("error".utf8)) XCTAssertEqual(error.response.statusCode, 412) diff --git a/dictionary.txt b/dictionary.txt index 7858a3f3a..32d67b95a 100644 --- a/dictionary.txt +++ b/dictionary.txt @@ -137,6 +137,7 @@ Supabase supabase SUPABASE supabot +swiftlint testpath testuser timestamptz