Skip to content

Commit 9ed79f4

Browse files
committed
chore: add SwiftLint 0.65.0 with pinned CI version
Adds SwiftLint as a code-smell/correctness linter alongside swift-format. No changes to Package.swift — downstream consumers are unaffected. Setup: - .swiftlint.yml: all default rules disabled; enable one-by-one in follow-up PRs as violations are fixed - Makefile: adds `make lint` (strict) and `make lint-fix` (autocorrect) - CI: new SwiftLint job pinned to 0.65.0 via portable_swiftlint.zip from GitHub releases, cached by version+OS, version-verified before running. Added to ci-success required-jobs gate. - AGENTS.md: documents the linting workflow Install locally: brew install swiftlint
1 parent 7499802 commit 9ed79f4

13 files changed

Lines changed: 191 additions & 27 deletions

File tree

.github/workflows/ci.yml

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,10 +317,42 @@ jobs:
317317
echo "✅ All changed Swift files are properly formatted"
318318
fi
319319
320+
lint:
321+
name: SwiftLint
322+
runs-on: macos-26
323+
timeout-minutes: 10
324+
env:
325+
SWIFTLINT_VERSION: "0.65.0"
326+
steps:
327+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
328+
- name: Cache SwiftLint binary
329+
id: cache-swiftlint
330+
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
331+
with:
332+
path: /usr/local/bin/swiftlint
333+
key: swiftlint-${{ env.SWIFTLINT_VERSION }}-${{ runner.os }}
334+
- name: Install SwiftLint ${{ env.SWIFTLINT_VERSION }}
335+
if: steps.cache-swiftlint.outputs.cache-hit != 'true'
336+
run: |
337+
curl -fL \
338+
"https://github.com/realm/SwiftLint/releases/download/${SWIFTLINT_VERSION}/portable_swiftlint.zip" \
339+
-o /tmp/swiftlint.zip
340+
unzip -q -o /tmp/swiftlint.zip -d /tmp/swiftlint
341+
sudo mv /tmp/swiftlint/swiftlint /usr/local/bin/swiftlint
342+
- name: Verify SwiftLint version
343+
run: |
344+
INSTALLED=$(swiftlint version)
345+
if [ "$INSTALLED" != "$SWIFTLINT_VERSION" ]; then
346+
echo "Expected SwiftLint $SWIFTLINT_VERSION, got $INSTALLED" && exit 1
347+
fi
348+
echo "SwiftLint $INSTALLED"
349+
- name: Run SwiftLint
350+
run: make lint
351+
320352
ci-success:
321353
name: CI Success
322354
if: always()
323-
needs: [macos, macos-legacy, spm, linux, integration-tests, library-evolution, examples, spell-check, docs, format-check]
355+
needs: [macos, macos-legacy, spm, linux, integration-tests, library-evolution, examples, spell-check, docs, format-check, lint]
324356
runs-on: ubuntu-latest
325357
steps:
326358
- name: Check all jobs

.swiftlint.yml

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# SwiftLint configuration for supabase-swift
2+
# https://github.com/realm/SwiftLint
3+
#
4+
# Pinned version: 0.65.0
5+
# Install locally: brew install swiftlint
6+
# CI pins the exact binary via portable_swiftlint.zip from GitHub releases.
7+
#
8+
# This project uses Apple's swift-format for formatting/whitespace/layout
9+
# (run via `make format`). SwiftLint is therefore scoped to code-smell and
10+
# correctness rules, and all purely stylistic rules that swift-format already
11+
# governs are disabled to avoid the two tools fighting each other.
12+
#
13+
# Run locally with `make lint` (or `make lint-fix` to autocorrect).
14+
15+
included:
16+
- Sources
17+
- Tests
18+
19+
excluded:
20+
- .build
21+
- Tests/IntegrationTests/supabase
22+
- "**/__Snapshots__"
23+
# Legacy code kept for backwards-compatibility; not subject to new lint rules.
24+
- Sources/Realtime/Deprecated
25+
26+
# ── Rule configuration ────────────────────────────────────────────────────────
27+
28+
identifier_name:
29+
# Allow single-character names used for crypto/math fields (n, e, x, y, k)
30+
# and common loop variables (i, h, l, r).
31+
min_length:
32+
warning: 1
33+
error: 1
34+
max_length:
35+
warning: 50
36+
error: 60
37+
# Internal helpers are intentionally underscore-prefixed (_getAccessToken,
38+
# _remove, _path, _task, _clock, JWKS_TTL, _20240101, etc.).
39+
validates_start_with_lowercase: false
40+
allowed_symbols: ["_"]
41+
excluded:
42+
- id
43+
- db
44+
- to
45+
- vs
46+
# Decodable struct properties whose names are dictated by the server response.
47+
- Key
48+
- Id
49+
50+
type_name:
51+
# Private/internal types use leading underscores (_WeakPassword, _Clock…)
52+
# and some public types exceed 40 chars (AuthMFAGetAuthenticatorAssuranceLevelResponse).
53+
min_length: 2
54+
max_length:
55+
warning: 50
56+
error: 60
57+
allowed_symbols: ["_"]
58+
59+
# The project has deliberately large source files and types; raise limits to
60+
# avoid noise on code that is intentionally structured this way.
61+
file_length:
62+
warning: 3400
63+
error: 4000
64+
65+
type_body_length:
66+
warning: 2800
67+
error: 3000
68+
69+
function_body_length:
70+
warning: 330
71+
error: 400
72+
73+
cyclomatic_complexity:
74+
warning: 20
75+
error: 30
76+
77+
function_parameter_count:
78+
warning: 6
79+
error: 10
80+
81+
# Types nested 2 levels deep appear legitimately in MultipartFormData and tests.
82+
nesting:
83+
type_level:
84+
warning: 2
85+
error: 3
86+
87+
# ── Opt-in rules ──────────────────────────────────────────────────────────────
88+
89+
opt_in_rules:
90+
- empty_count
91+
- empty_string
92+
93+
# ── Disabled rules ────────────────────────────────────────────────────────────
94+
95+
disabled_rules:
96+
# Owned by swift-format — let it be the single source of truth for layout.
97+
- trailing_whitespace
98+
- trailing_comma
99+
- opening_brace
100+
- comment_spacing
101+
- vertical_whitespace
102+
- trailing_newline
103+
- closure_parameter_position
104+
- line_length
105+
106+
# Intentionally used in Sources (StorageApi, RealtimeChannelV2) and heavily
107+
# in tests. Remove from disabled_rules once the codebase is cleaned up.
108+
- force_try
109+
- force_cast
110+
111+
# Pervasive in test helpers for String <-> Data conversions.
112+
- non_optional_string_data_conversion
113+
- optional_data_string_conversion
114+
115+
# `let _ = subscription` is an intentional pattern in tests to retain
116+
# ObservationToken long enough for the callback to fire (see EventEmitter.swift).
117+
- redundant_discardable_let
118+
119+
# Style preferences that don't affect correctness.
120+
- multiple_closures_with_trailing_closure
121+
- for_where
122+
- implicit_optional_initialization
123+
# swift-format handles wrapping of very long function names across lines.
124+
- function_name_whitespace

AGENTS.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,21 @@ make spell-check # cspell - Swift and Markdown sources
8686

8787
Legitimate technical terms and project-specific words go in `dictionary.txt` at the repository root.
8888

89+
### Linting
90+
91+
```bash
92+
# Lint Sources and Tests (fails on any new violation)
93+
make lint
94+
95+
# Autocorrect violations that SwiftLint can fix
96+
make lint-fix
97+
```
98+
99+
This uses [SwiftLint](https://github.com/realm/SwiftLint) for code-smell and
100+
correctness rules; `swift-format` remains the source of truth for formatting,
101+
so the SwiftLint config (`.swiftlint.yml`) disables the purely stylistic rules
102+
that overlap with it. Install SwiftLint locally with `brew install swiftlint`.
103+
89104
### Documentation
90105

91106
```bash
@@ -297,6 +312,7 @@ supabase stop
297312
## Important Notes for AI Coding Agents
298313

299314
- Always run `make format` before committing Swift code
315+
- Run `make lint` before committing; it must not report new violations
300316
- Ensure new public APIs have DocC documentation comments
301317
- Add tests for all new functionality
302318
- Keep changes minimal and focused

Makefile

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,13 @@ format:
8181
-not -path '*/.*' -print0 \
8282
| xargs -0 xcrun swift-format --ignore-unparsable-files --in-place
8383

84-
.PHONY: build-for-library-evolution format warm-simulator xcodebuild test-docs test-integration
84+
lint:
85+
swiftlint lint --strict
86+
87+
lint-fix:
88+
swiftlint lint --fix
89+
90+
.PHONY: build-for-library-evolution format lint lint-fix warm-simulator xcodebuild test-docs test-integration
8591

8692
.PHONY: coverage
8793
coverage:

Sources/Helpers/Base64URL.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ package enum Base64URL {
1717
let paddingLength = requiredLength - length
1818
if paddingLength > 0 {
1919
let padding = "".padding(toLength: Int(paddingLength), withPad: "=", startingAt: 0)
20-
base64 = base64 + padding
20+
base64 += padding
2121
}
2222
return Data(base64Encoded: base64, options: .ignoreUnknownCharacters)
2323
}

Sources/RealtimeV2/WebSocket/URLSessionWebSocket.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ final class URLSessionWebSocket: WebSocket {
7474

7575
let session = URLSession.sessionWithConfiguration(
7676
configuration ?? .default,
77-
onComplete: { session, task, error in
77+
onComplete: { _, _, error in
7878
mutableState.withValue {
7979
if let webSocket = $0.webSocket {
8080
// There are three possibilities here:
@@ -104,14 +104,14 @@ final class URLSessionWebSocket: WebSocket {
104104
}
105105
}
106106
},
107-
onWebSocketTaskOpened: { session, task, `protocol` in
107+
onWebSocketTaskOpened: { _, task, `protocol` in
108108
mutableState.withValue {
109109
$0.webSocket = URLSessionWebSocket(
110110
_task: task, _protocol: `protocol` ?? "", session: session)
111111
$0.continuation.resume(returning: $0.webSocket!)
112112
}
113113
},
114-
onWebSocketTaskClosed: { session, task, code, reason in
114+
onWebSocketTaskClosed: { _, _, code, reason in
115115
mutableState.withValue {
116116
assert($0.webSocket != nil, "connection should exist by this time")
117117
$0.webSocket!._connectionClosed(code: code, reason: reason)

Sources/TestHelpers/URLRequestSnapshot.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
#endif
1616

1717
extension Snapshotting where Value == URLRequest, Format == String {
18-
/// A snapshot strategy for comparing requests based on a cURL representation.
19-
///
18+
// A snapshot strategy for comparing requests based on a cURL representation.
19+
//
2020
// ``` swift
2121
// assertSnapshot(of: request, as: .curl)
2222
// ```

Tests/FunctionsTests/FunctionsClientTests.swift

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,6 @@ final class FunctionsClientTests: XCTestCase {
3838
sessionConfiguration: sessionConfiguration
3939
)
4040

41-
override func setUp() {
42-
super.setUp()
43-
// isRecording = true
44-
}
45-
4641
func testInit() async {
4742
let client = FunctionsClient(
4843
url: url,

Tests/IntegrationTests/RealtimeIntegrationTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -785,7 +785,7 @@
785785

786786
// Verify both clients can decode presence
787787
// Note: Due to timing, exact presence changes may vary, but structure should be correct
788-
XCTAssertTrue(presenceChanges1.count > 0, "Client 1 should receive presence changes")
788+
XCTAssertTrue(!presenceChanges1.isEmpty, "Client 1 should receive presence changes")
789789

790790
// Verify messages were received by both clients
791791
XCTAssertEqual(messages1.count, 3, "Client 1 should receive all 3 messages")
@@ -816,7 +816,7 @@
816816

817817
// Verify user 1 leaving is detected by user 2
818818
// Note: Due to timing, exact presence changes may vary, but structure should be correct
819-
XCTAssertTrue(presenceChanges2.count > 0, "Client 2 should receive presence changes")
819+
XCTAssertTrue(!presenceChanges2.isEmpty, "Client 2 should receive presence changes")
820820

821821
// Cleanup
822822
await channel1.unsubscribe()

Tests/PostgRESTTests/PostgrestFilterBuilderTests.swift

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,6 @@ import XCTest
1212

1313
final class PostgrestFilterBuilderTests: PostgrestQueryTests {
1414

15-
override func setUp() {
16-
super.setUp()
17-
// isRecording = true
18-
}
19-
2015
func testNotFilter() async throws {
2116
Mock(
2217
url: url.appendingPathComponent("users"),

0 commit comments

Comments
 (0)