-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path.swiftlint.yml
More file actions
566 lines (557 loc) · 35.3 KB
/
Copy path.swiftlint.yml
File metadata and controls
566 lines (557 loc) · 35.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
# Tier 1 — Swift Institute ecosystem-wide canonical SwiftLint configuration.
#
# This is the root of the parent_config: chain for the Swift Institute
# ecosystem. Org-specific canonicals (e.g. swift-primitives/.github/.swiftlint.yml)
# inherit from this file via:
#
# parent_config: https://raw.githubusercontent.com/swift-institute/.github/main/.swiftlint.yml
#
# Per-repo .swiftlint.yml files inherit from their org canonical and are
# the natural home for per-repo overrides only.
#
# Phase 1 changes (2026-05-05) — see swift-institute/Research/
# rollout-phase-1-results.md for the rationale.
#
# [API-IMPL-005] one_declaration_per_file was originally part of Phase 1's
# opt_in_rules but was DROPPED after canary testing surfaced an
# architectural mismatch with [TEST-005] @Suite test patterns: SwiftLint
# does not support per-rule path scoping for built-in rules (verified
# 2026-05-05 against rule docs + empirical test), so the rule cannot be
# limited to Sources/ only. The [TEST-005] pattern uses multiple top-level
# fixture types per test file, which one_declaration_per_file flags as
# violations. Deferred to Phase 1.5+ pending either (a) tests refactored
# to nest fixtures, (b) CI restructured for separate Sources/Tests
# invocations, or (c) sub-config fanout infrastructure built.
disabled_rules:
- line_length
- redundant_discardable_let
- identifier_name # Allow short variable names (i, x, y, etc.)
- large_tuple # Allow tuples with more than 2 members
- optional_data_string_conversion # False positive with String(decoding:as:)
- for_where # False positives when for loops have complex logic
- todo # Allow TODO comments for tracking future work
- type_body_length # Don't enforce maximum type body length
- nesting # Allow nested types for hierarchical APIs
- type_name # Allow flexible test suite naming (lowercase, special chars)
- cyclomatic_complexity # Allow complex inline logic over helper extraction
- implicit_optional_initialization # Allow explicit = nil initialization
- function_body_length # Allow longer inline functions
- file_length # Allow longer files
- closure_parameter_position # Conflicts with swift-format line breaking
- opening_brace # Conflicts with swift-format for long declarations
- comma # Conflicts with swift-format (@_implements attribute-argument spacing); swift-format owns comma spacing
# trailing_comma (ruled swift-institute/.github#135, 2026-07-30): swift-format
# owns collection-comma PLACEMENT exactly as it owns comma SPACING (see the
# `comma` entry above). The two tools use different predicates for "multiline
# collection literal": swift-format's is element-COUNT based
# (`multiElementCollectionTrailingCommas` — literally multi-ELEMENT: it adds
# the comma at >=2 elements and REMOVES it at exactly 1), SwiftLint's is
# line-SPAN based (element list spans >1 line). They differ on exactly one
# cell — a one-element literal whose single element itself spans several
# lines — where the two demands are contradictory and no setting of either
# tool satisfies both (`mandatory_comma: true` fails that cell;
# `mandatory_comma: false` fails every >=2-element literal). Nothing is lost
# by yielding: the gating `format` leg runs `swift-format lint --strict`,
# which emits `[TrailingComma] add trailing comma to the last element in
# multiline collection literal` and fails the build on precisely the case
# 57f03cc set out to enforce. Verified on swift-ietf/swift-rfc-2822@8d7f192
# (9 sites) with swiftlint 0.65.0 / swift-format 6.3.3.
- trailing_comma
# function_name_whitespace (ruled swift-institute/.github#135, 2026-07-30):
# swift-format MANUFACTURES this violation. A [TEST-005] backticked test name
# inside the nested `@Suite struct Test` shape can exceed lineLength on its
# own; swift-format then wraps BETWEEN `func` and the name, and SwiftLint
# reads that line break plus continuation indent as "too many spaces between
# 'func' and function name". The source it flags is the formatter's output,
# never the author's input. The rule takes `severity` only — nothing narrows
# it — and raising `lineLength` merely moves the threshold (a descriptive
# test name at sufficient nesting depth exceeds any bound). Same jurisdiction
# as `opening_brace` above: swift-format owns declaration wrapping. Verified
# on swift-ietf/swift-rfc-2822@8d7f192 (2 sites) and isolated reproduction
# with swiftlint 0.65.0 / swift-format 6.3.3.
- function_name_whitespace
# prefer_self_in_static_references (master adjudication 2026-07-10): pulled
# from the Tier A opt-in cohort — see the removal note at that list entry
# below. Misfires on the institute's nested `struct Test` suite pattern
# (`extension Foo { @Suite struct Test { @Test func ... } } }`): the rule
# flags `@Test` MACRO ATTRIBUTES as static-member references, and its
# autocorrect corrupts them (rewrites the attribute text, breaking the
# macro). Confirmed across swift-color, swift-lexer, swift-systems,
# swift-bitset-primitives, swift-time-primitives, swift-logic-primitives.
# disabled_rules wins over opt_in_rules regardless of declaration order or
# parent_config depth, so this single Tier 1 entry kills the false-positive
# class fleet-wide for every repo in the parent_config chain.
- prefer_self_in_static_references
opt_in_rules:
- explicit_init
- closure_spacing
- empty_string
- fatal_error_message
- first_where
- joined_default_parameter
- operator_usage_whitespace
- overridden_super_call
# Tier A cohort (2026-05-05): consistency / harness rules.
# prefer_self_in_static_references REMOVED from this cohort (master
# adjudication 2026-07-10) — see the disabled_rules entry above for the
# false-positive rationale (nested `@Suite struct Test` / `@Test`-attribute
# autocorrect corruption). Left here as a struck-through breadcrumb so a
# future cohort re-audit doesn't silently re-add it without re-litigating.
- direct_return
- redundant_self
- vertical_whitespace_between_cases
# Tier A.5 cohort (2026-05-05): additional pure-consistency opt-ins.
- modifier_order # consistent attribute / access-modifier ordering
- redundant_nil_coalescing # `?? nil` is dead code
- shorthand_optional_binding # `if let x` over `if let x = x` (Swift 5.7+)
- unneeded_parentheses_in_closure_argument # `{ x in }` over `{ (x) in }`
# Tier A.6 cohort (2026-05-05): perf / clarity opt-ins.
# toggle_bool deliberately NOT enabled — user prefers `x = !x` over `x.toggle()`.
- last_where # `.reversed().first(where:)` → `.last(where:)` (perf)
- flatmap_over_map_reduce # `.map().reduce(into: [], +=)` → `.flatMap()` (perf+clarity)
- prefer_zero_over_explicit_init # `Int(0)` → `0`
- contains_over_filter_is_empty # `.filter().isEmpty` → `!.contains(where:)`
included:
- Sources
- Tests
excluded:
- .build
# Nested SwiftPM packages (e.g. `Tests/<Suite>/`) keep their own `.build`
# checkout under the parent's `Tests/`; the bare `.build` entry only matches
# the top-level directory, so the recursive glob catches the nested ones too.
- "**/.build"
# Validator corpora deliberately contain invalid and non-canonical source.
- "**/Fixtures"
- Carthage
- Pods
- fastlane
# DocC tutorial code samples are didactic fixtures, not API surface;
# excluded ecosystem-wide ([Phase 1.5] 2026-05-05).
- "**/*.docc/Resources"
function_parameter_count:
warning: 6
error: 8
# inclusive_language (swift-institute/.github#268, principal ruling
# recommendation (a), 2026-08-04; corrected per review 4850697512): `
# inclusive_language` is not itself a Tier 1 opt_in_rules member — it is
# enabled per-repo, e.g. by swift-ietf/swift-rfc-8446's own
# `.swiftlint.yml` — but a repo that opts in inherits this rule's config
# from the Tier 1 parent_config chain, so the narrowest fleet-wide fix is
# a term allowlist here rather than a per-repo severity override or a
# blanket `disabled_rules` entry.
#
# SwiftLint 0.63.3's `inclusive_language` rule has NO `allowed_terms` key
# — that key is silently ignored (does not error, does not apply). The
# real key is `override_allowed_terms`, and it REPLACES the rule's
# built-in default allowed-terms set rather than extending it, so the
# built-in default (`mastercard` — upstream's own false-positive
# exemption for "master" as a payment-network substring, SwiftLint#3415)
# must be re-listed here alongside the RFC 8446 family or it is silently
# dropped fleet-wide. Matching is lowercased range-overlap, so the
# camelCase spec identifiers below work as-is against SwiftLint's
# lowercased flagged-term scan.
#
# swift-rfc-8446 mirrors RFC 8446's own normative key-schedule
# terminology: `masterSecret`, `resumptionMasterSecret`,
# `exporterMasterSecret`, `earlyExporterMasterSecret`,
# `earlyExporterMaster` (Sources/RFC 8446/RFC_8446.KeySchedule.Label.swift,
# RFC_8446.KeySchedule.Stages.swift). These identifiers encode the RFC's
# own wire-visible label strings (e.g. "s hs traffic", "res master";
# https://www.rfc-editor.org/rfc/rfc8446#section-7.1) — renaming breaks the
# spec's documented correspondence between identifier and label, and
# spec-fidelity is swift-ietf's reason to exist. The exemption is scoped to
# this exact identifier family, not to "master" as a bare term or to
# swift-ietf repos wholesale: any non-spec-mirroring use of a flagged term
# elsewhere in the fleet, including other swift-ietf repos, still fires.
inclusive_language:
override_allowed_terms:
- mastercard
- masterSecret
- resumptionMasterSecret
- exporterMasterSecret
- earlyExporterMasterSecret
- earlyExporterMaster
# RSS 2.0 mirrors the spec's own `<webMaster>` channel element
# (swift-standards/swift-rss-standard); same spec-fidelity class as
# the RFC 8446 family above.
- webMaster
# ── Custom rules ─────────────────────────────────────────────────────────────
#
# Phase 1.5 (2026-05-05): Tier 1 ecosystem-wide custom rules. Each is a
# one-liner regex catching a specific violation pattern from the institute's
# code-surface / testing skills.
custom_rules:
typed_throws_required:
name: "Typed Throws Required ([API-ERR-001])"
regex: '(?<!\bfunc provideScope\([^{}]{0,500})(?<!\bfunc encode\(to encoder:\s{0,100}(?:any\s{1,100})?Encoder\)\s{0,100})(?<!\binit\(from decoder:\s{0,100}(?:any\s{1,100})?Decoder\)\s{0,100})(?<!\bfunc expansion\([^{}]{0,500}\bin context:\s{0,100}(?:some|any)\s{1,100}MacroExpansionContext\s{0,100}\)\s{0,100})(?<!:\s{0,10}\([^()]{0,200}\)\s{0,10}(?:async\s{0,10})?)(?<!:\s{0,10}\([^()]{0,200}\)\s{0,10}(?:async\s{0,10})?throws\s{0,10}->\s{0,10}[A-Za-z_][A-Za-z0-9_]{0,60}\s{0,20}\)\s{0,10}(?:async\s{0,10})?)\bthrows\b\s*(?![\(:])'
message: "Throwing functions/methods/inits MUST use typed throws ([API-ERR-001]). Use 'throws(SomeError)' rather than untyped 'throws'."
severity: warning
match_kinds:
- keyword
# Tests legitimately use untyped throws: propagation through stdlib
# untyped-`rethrows` boundaries (e.g. `withUnsafeBytes`) makes a typed
# `throws(E)` not cleanly expressible, and [API-ERR-001] targets API
# surface, not test code (principal-confirmed 2026-06-02).
#
# `func provideScope(...)` declarations are exempted via a bounded
# negative lookbehind (ICU/NSRegularExpression requires an explicit
# upper bound on variable-length lookbehind quantifiers — unbounded
# `*`/`+` fails to compile): Apple's Testing.TestScoping.provideScope
# is declared with untyped `throws` upstream, so any conforming
# override is signature-forced and cannot express `throws(E)`. Mirrors
# the AST untyped-throws rule's TestScoping.provideScope allowlist.
# Does NOT cover non-`func` shapes (e.g. a stored `_provideScope`
# closure property or an init parameter named `provideScope`) — those
# still require a per-site shield.
#
# swift-institute/.github#219 (principal ruling, comment 5164746902,
# 2026-08-03) extends the same bounded-lookbehind technique to three
# more protocol-witness shapes whose untyped `throws` is dictated by
# a stdlib/SwiftSyntaxMacros protocol requirement, not by the
# conforming author: a `throws(E)` witness here must catch-and-wrap
# errors from the untyped container/macro-expansion APIs, and no
# caller can ever observe the typed error because the protocol
# erases it — ceremony, not a fix.
# - stdlib `Encodable.encode(to:)`: `func encode(to encoder: Encoder)`
# / `func encode(to encoder: any Encoder)` (both spellings are
# live in the fleet — swift-standards/swift-domain-standard and
# swift-standards/swift-emailaddress-standard use `any Encoder`;
# swift-standards/swift-locale-standard and
# swift-iso/swift-iso-15924 predate `any` and omit it).
# - stdlib `Decodable.init(from:)`: `init(from decoder: Decoder)` /
# `init(from decoder: any Decoder)` (same two spellings).
# - SwiftSyntaxMacros witnesses (`MemberMacro`, `ExtensionMacro`,
# `MemberAttributeMacro`, `AccessorMacro`, and siblings): every
# role protocol names its requirement `static func expansion(...)`
# and ends the parameter list `in context: some MacroExpansionContext`
# (verified against swift-foundations/swift-copy-on-write's
# `CoWMacro.swift`, the #219 red-A evidence) immediately before
# the untyped `throws`.
# Each lookbehind anchors on the parameter's INTERNAL name
# (`encoder`/`decoder`/`context`) AND its stdlib/SwiftSyntaxMacros
# type, not just the external label — a same-named parameter typed
# against a DIFFERENT protocol (e.g. `to encoder: any Writable`,
# `from decoder: any JSONSource`) does not match the lookbehind text
# and the rule still fires, which is the discriminator [#219] rules
# on: ordinary `any <Protocol>`/untyped-`throws` API surface (e.g.
# swift-ietf/swift-rfc-3987's `URL+IRI.swift`) is unaffected.
#
# swift-institute/.github#260 (coordinator ruling, comment
# 5169948783, 2026-08-03) extends the same bounded-lookbehind family
# to the closure-passthrough rethrow shape: a `throws` function whose
# ONLY untyped-throws source is rethrowing a generic closure
# parameter's error alongside the function's own failures. Swift
# typed throws has no error-union type — `throws(E | F)` is
# inexpressible — so a function that must independently surface its
# own typed failures (e.g. `PoolError.timeout`) AND transparently
# propagate whatever a caller-supplied closure throws cannot express
# both under one `throws(E)` clause without an API-breaking wrapper
# error type. The stdlib itself uses untyped throws for exactly this
# shape (`withThrowingTaskGroup`, `withCheckedThrowingContinuation`).
# Witness: swift-foundations/swift-resource-pool `ResourcePool.
# withResource(timeout:_:)` (head 8ca28f8) —
# public func withResource<T: Sendable>(
# timeout: Duration = .seconds(30),
# _ operation: (Resource) async throws -> T
# ) async throws -> T
# Two lookbehinds cover the two `throws` occurrences this shape
# produces:
# 1. The closure PARAMETER's own declared `throws` (its function-
# type annotation, e.g. `(Resource) async throws -> T`) is
# always exempt when it is a labeled parameter's function type —
# that `throws` describes what the closure TYPE accepts, not
# the enclosing declaration's own error contract, and a caller-
# supplied closure's error type is unconstrained by definition.
# Anchor: `:` then a bounded `(...)` closure-parameter type then
# optional `async`, immediately before `throws`.
# 2. The ENCLOSING function's own trailing `throws` (e.g. the
# `) async throws -> T {` after the full parameter list) is
# exempt ONLY when that same closure-parameter shape is the
# LAST parameter in the list, i.e. immediately (modulo
# whitespace) followed by the parameter list's closing `)` and
# the function's own `async throws`. This is the conservative,
# mechanically recognizable proxy for "the function's sole
# untyped-throws source is the passthrough closure": if another
# parameter follows the closure, or the closure is not
# immediately adjacent to the outer `)`, the second lookbehind
# does not match and the rule still fires (per-site
# disable+REASON remains available for shapes this proxy
# misses — under-matching is preferred to over-matching).
# Known limitation (documented, not hidden): this is a structural/
# textual match, not a data-flow analysis — a function whose last
# parameter happens to be a stored-not-called throwing closure (never
# actually invoked to produce the rethrow) matches the same shape and
# is also exempted. This mirrors the #219 precedent, which is
# likewise a signature-shape match, not a semantic one; the fleet
# surface this could over-match is a function accepting a trailing
# throwing closure it does not call, which is itself an unusual and
# narrow shape.
# Ruling conditions: an exempted declaration must document its error
# contract in its doc comment (which typed failures it throws itself,
# and that the closure's error is rethrown transparently) — the
# pattern already on `withResource` at resource-pool head 8ca28f8.
# Revisit trigger: this exemption is reviewed the day Swift ships an
# error-union or typed-rethrows capability; it exists because of the
# expressiveness gap, not despite it.
excluded:
- 'Tests/.*'
no_xctest_import:
name: "No XCTest Import ([TEST-001])"
regex: '^[ \t]*(?:@[a-zA-Z_]+[ \t]+)*(?:public[ \t]+|package[ \t]+|internal[ \t]+|fileprivate[ \t]+|private[ \t]+)?(?:@testable[ \t]+)?import[ \t]+XCTest\b'
message: "Tests must use 'import Testing' (Apple Swift Testing framework), not 'import XCTest' ([TEST-001])."
severity: error
no_xctestcase_subclass:
name: "No XCTestCase Subclass ([TEST-001])"
regex: '\bclass\s+\w+\s*:\s*XCTestCase\b'
message: "Tests must use Swift Testing @Suite, not XCTestCase subclasses ([TEST-001])."
severity: error
exports_swift_strict_shape:
name: "exports.swift Strict @_exported public Shape ([TEST-020])"
regex: '^[ \t]*(?:public[ \t]+|package[ \t]+|internal[ \t]+|fileprivate[ \t]+|private[ \t]+)?import[ \t]+\w+'
message: "exports.swift files must use '@_exported public import' shape; plain 'import' or non-canonical access modifiers are forbidden ([TEST-020])."
severity: error
included:
- 'Sources/.*/exports\.swift'
- 'Tests/.*/exports\.swift'
# [TEST-025] test_support_module_in_tests was authored 2026-05-05 but
# canary surfaced a structural false-positive class: test files routinely
# need BOTH `import <Module>_Test_Support` (for fixtures) AND
# `@testable import <Module>` (for internal access — `@_exported public`
# re-exports don't propagate @testable). The regex flags the @testable
# import even when Test_Support is also imported in the same file (3/7
# such legitimate-dual-import false positives observed on swift-tagged-primitives).
# Rule needs file-context-aware design (e.g., flag @testable import X only
# when import X_Test_Support is NOT also in the file). Deferred pending
# redesign — likely a SwiftSyntax-based custom rule or per-file shape check.
swift_error_qualification:
name: "Swift.Error Qualification ([PLAT-ARCH-011])"
# Catches bare `Error` references that should be `Swift.Error`. The
# seven fixed-length negative lookbehinds exclude: six DECLARATION
# sites where `Error` is the type's own name (`struct`, `enum`,
# `class`, `actor`, `typealias`, `associatedtype` — each followed by
# one space); plus `throws(`, since bare `Error` in a typed-throws
# clause (`throws(Error)`) is a concrete nested error type in scope
# (e.g. `Pool.Error`), never the `Swift.Error` existential — so it
# must not trip (campaign rule-fix 2026-06-25). Only genuine
# REFERENCE sites are flagged. This is the [API-ERR-002]
# Nest.Name exemption — `enum X { struct Error: Swift.Error {} }`,
# `extension Foo { enum Error: Swift.Error { case ... } }`, and
# `protocol Codec { associatedtype Error: Swift.Error }` are valid
# canonical patterns and must not trip. End-state: swift-linter rule
# using SwiftSyntax's IdentifierTypeSyntax-vs-decl traversal, which
# makes the declaration-vs-reference distinction AST-level and
# removes the regex fragility. Until that ships, the lookbehinds
# are the interim fix.
# §NOTES fixes (2026-07-07): two false-positive classes exempted.
# (a) Backtick-quoted test function names containing "Error" (e.g.
# `@Test func \`Error is Sendable\`()`) — the "Error" substring sits
# inside a declaration-name `identifier` token, never a type
# reference. Dropping `identifier` from match_kinds (keeping only
# `typeidentifier`) exempts the whole class; genuine unqualified-
# Error references (conformances `: Error`, generic constraints
# `<E: Error>`, associatedtype bounds, function types `(Error)->`)
# are all `typeidentifier` and still fire.
# (b) Associated-type same-type constraint positions (`Error ==` in a
# where-clause, e.g. `extension Codec where Error == Never`) — the
# added `(?![ \t]*==)` lookahead exempts a bare `Error` immediately
# followed by `==` (dot-qualified `Foo.Error ==` was already exempt
# via the `(?<!\.)` lookbehind).
# Validated: the 35 unshielded FP sites across swift-linux/witnesses/
# paths/io stop firing with NO shields added; genuine violations fire.
# §NOTES fix (2026-07-30, #136): a bare-`Error` REFERENCE (not
# `throws(...)`, not dot-qualified) still fires when it resolves to a
# local NESTED error type rather than the `Swift.Error` existential —
# e.g. `private static func f() -> Error { ... }` inside
# `extension Foo.Bar { ... }` where `Foo.Bar.Error` is declared in a
# SIBLING file. This is a genuine reference-position false positive
# the regex cannot resolve: SwiftLint custom rules match per-file text
# with no cross-file or cross-extension symbol table, so there is no
# regex-expressible way to tell "`Error` here means the local nested
# `Foo.Bar.Error` declared elsewhere" apart from "`Error` here means
# `Swift.Error`" — both are the identical token in the identical
# syntactic position (return-type, parameter type, etc.), and a
# positive-control case (`func f() -> Error` where `Error` genuinely
# is the stdlib existential) must keep firing. Confirmed empirically
# against swift-rfc-2822 RFC_2822.AddrSpec.swift:430 (fix: swift-ietf/
# swift-rfc-2822#136-fix, qualifying with the actual local type
# `RFC_2822.AddrSpec.Error` rather than `Swift.Error`). The prescribed
# fix for THIS class is: qualify with the reference's actual local
# type path (`Outer.Inner.Error`), not `Swift.Error` — qualifying as
# `Swift.Error` would silently change the type to the protocol
# existential and break typed-throws call sites and error-producing
# call sites alike. `Swift.Error` remains correct only when the bare
# `Error` genuinely denotes the stdlib protocol. End-state (SwiftSyntax
# symbol resolution) is the only way to distinguish the two cases
# automatically; until then this stays a manual per-site judgment call.
regex: '\b(?<!Swift\.)(?<!\.)(?<!struct )(?<!enum )(?<!class )(?<!actor )(?<!typealias )(?<!associatedtype )(?<!throws\()Error\b(?!\.)(?![ \t]*==)'
message: "Qualify bare 'Error' ([PLAT-ARCH-011]; user-confirmed 2026-05-05). Applies to generic constraints (`<E: Swift.Error>`), conformances (`: Swift.Error`), and any other reference. Avoids ambiguity with namespace-scoped Error types and is the institute's convention everywhere. If the bare 'Error' genuinely denotes the stdlib protocol, qualify as 'Swift.Error'. If it resolves to a local NESTED error type (e.g. a sibling-file `Outer.Inner.Error`), qualify with that type's actual path instead — qualifying those as 'Swift.Error' is semantically wrong and breaks typed-throws/error-producing call sites (see #136). Declaration sites (`struct/enum/class/actor/typealias/associatedtype Error: Swift.Error`) are exempt — that's [API-ERR-002]'s Nest.Name pattern. Backtick-quoted test function names and associated-type `Error ==` constraint positions are also exempt (§NOTES 2026-07-07)."
severity: warning
match_kinds:
- typeidentifier
# ── Wave 2b additions (2026-05-10) ────────────────────────────────────────
# Twelve new rules per Wave 2b extraction inventory (see
# `HANDOFF-skill-to-ci-cd-extraction-inventory.md`). Each cites a skill
# rule the regex now mechanically enforces; corresponding skill prose
# is trimmed in the same Wave 2b cycle. Disable a rule on a specific
# line via `// swiftlint:disable:next <rule_name> // reason: <citation>`.
no_try_optional:
name: "No try? — Silent Error Swallowing ([IMPL-108])"
regex: '\btry\?\s'
message: "try? silently discards typed errors. Use 'do throws(E) { try ... } catch { }' for explicit handling, or 'try!' if the precondition is provably guaranteed ([IMPL-108])."
severity: warning
excluded:
- 'Tests/.*'
no_existential_throws:
name: "No Existential Throws ([API-ERR-006])"
regex: '\bthrows\(\s*any\s+Error\s*\)'
message: "Existential 'throws(any Error)' is forbidden. [API-ERR-001] requires typed throws and is non-negotiable. Make the containing type generic over the error type ([API-ERR-006])."
severity: error
no_any_protocol_existential:
name: "No `any <Protocol>` Existential ([API-ERR-006] extension)"
# Decision 3 (Wave 2b authorization): extends [API-ERR-006] to ban
# `any <Protocol>` references in Sources/ generally — typed-throws
# discipline composes with general anti-existential discipline.
# `any Error` is already covered (and stricter) via `no_existential_throws`.
# Opt-out at deliberate dynamic-dispatch sites with:
# // swiftlint:disable:next no_any_protocol_existential // reason: <citation>
# No `match_kinds` filter — `any` is a contextual keyword, not an
# identifier; SwiftLint's `match_kinds` would exclude keyword tokens
# and the regex would never fire.
# excluded_match_kinds added 2026-07-02: comment/doccomment/string prose
# legitimately says "any <Word>" (e.g. swift-manifest-primitives
# Manifest.swift:31 "…or any L2/L3 type"); exclusion sidesteps the
# keyword-inclusion breakage above while keeping the regex unchanged.
#
# swift-institute/.github#219 (principal ruling, comment 5164746902,
# 2026-08-03): `any Encoder` / `any Decoder` in the exact stdlib
# `Encodable.encode(to:)` / `Decodable.init(from:)` witness parameter
# position are STRICTLY unavoidable — Swift does not permit a generic
# method to witness an existential-parameter protocol requirement, so
# there is no rewrite that removes the existential without breaking
# the conformance. Because the flagged text ("any Encoder"/"any
# Decoder") sits AHEAD of the match position rather than behind it,
# the exemption cannot be a plain bounded negative lookbehind (as
# `typed_throws_required` uses above); each exemption is instead one
# negative lookahead containing a nested bounded lookbehind — `(?!
# (?<=to encoder: )any Encoder\))` reads "fail the match iff this
# position is both preceded by the witness's parameter label AND
# immediately followed by its exact stdlib type + closing paren".
# Anchoring on the parameter's internal name AND its literal type
# means a same-named parameter typed against a different protocol
# (`to encoder: any Writable`, `from decoder: any JSONSource`) does
# not match either half and the rule still fires — required per
# [#219]'s near-miss discriminator. The SwiftSyntaxMacros witness
# class (#219 red-A evidence, verified against swift-foundations/
# swift-copy-on-write's `CoWMacro.swift`) uses `some
# MacroExpansionContext`, never `any`, so it needs no entry here —
# only `typed_throws_required` is extended for that class.
regex: '(?!(?<=\bto encoder:\s{0,100})any\s{1,100}Encoder\))(?!(?<=\bfrom decoder:\s{0,100})any\s{1,100}Decoder\))\bany\s+[A-Z][A-Za-z0-9_]*'
excluded_match_kinds:
- comment
- comment.mark
- comment.url
- doccomment
- doccomment.field
- string
message: "Existential `any <Protocol>` typically indicates dynamic dispatch the type system could express more precisely. Prefer generic constraints / typed throws / concrete types ([API-ERR-006] extension; Wave 2b decision 3). Opt out at deliberate sites with '// swiftlint:disable:next no_any_protocol_existential // reason: <citation>'."
severity: warning
included:
- 'Sources/.*'
no_tag_suffix_phantom:
name: "No *Tag Suffix in Tagged Phantom Types ([API-NAME-010])"
regex: 'Tagged<\s*\w+Tag\b'
message: "Phantom-type tags use the bare concept name. The *Tag suffix adds nothing — the tag IS the concept ([API-NAME-010])."
severity: warning
options_not_flags:
name: "Options Suffix for OptionSet ([API-NAME-011])"
# Catches `struct *Flags` / `enum *Flags` declarations. Spec-mirroring
# exception per [API-NAME-003]: types whose `Flags` is the spec's
# literal term may opt out via // swiftlint:disable:next options_not_flags.
regex: '\b(struct|enum)\s+\w+Flags\b'
message: "OptionSet types use .Options suffix, not .Flags (C-flag idiom). The Swift API layer models the concept, not the C naming ([API-NAME-011])."
severity: warning
no_impl_obj_inst_bindings:
name: "No impl/obj/inst Local Binding Abbreviations ([API-NAME-012])"
regex: '\b(let|var)\s+(impl|obj|inst|instance)\s*='
message: "Local bindings use the type's own name or a domain-qualified word, not impl/obj/inst/instance ([API-NAME-012])."
severity: warning
no_unsafe_block_form:
name: "unsafe Is Expression Keyword, Not Block ([MEM-UNSAFE-004])"
regex: '\bunsafe\s*\{'
message: "unsafe wraps an expression, not a block. Move 'unsafe' to the leftmost position of the expression ([MEM-UNSAFE-004])."
severity: error
no_typed_catch_let_error_where:
name: "Use Implicit error Binding in Typed catch ([PATTERN-009])"
regex: '\bcatch\s+let\s+error\s+where\b'
message: "In typed-throws catch blocks, use the implicit 'error' binding with 'catch where ...', not 'catch let error where' ([PATTERN-009])."
severity: warning
no_thread_ismainthread_in_primitives:
name: "No Thread.isMainThread in Primitives ([SWIFT-TEST-012], [PRIM-FOUND-001])"
regex: '\bThread\.isMainThread\b'
message: "Primitives MUST NOT use Foundation's Thread.isMainThread; use pthread_main_np() ([SWIFT-TEST-012])."
severity: error
included:
- 'Sources/.*'
no_macros_test_support_legacy:
name: "Use SwiftSyntaxMacrosGenericTestSupport ([SWIFT-TEST-016])"
regex: '^[ \t]*import[ \t]+SwiftSyntaxMacrosTestSupport\b'
message: "Macro tests use SwiftSyntaxMacrosGenericTestSupport, not SwiftSyntaxMacrosTestSupport ([SWIFT-TEST-016])."
severity: error
workaround_marker_present:
name: "WORKAROUND Comment Discipline ([DOC-045], [PATTERN-016])"
# Multiline template check (2026-07-07 §C1 rewrite, replacing the prior
# blunt single-line `//\s*WORKAROUND:` regex). Fires on a `// WORKAROUND:`
# marker that is NOT followed — on the comment lines immediately after
# it, in order — by the complete four-part template: `// WHY:`,
# `// WHEN TO REMOVE:`, `// TRACKING:`. The lazy `(?:\n[ \t]*//[^\n]*)*?`
# gaps between parts allow each part (and the WORKAROUND description
# itself) to wrap across multiple `//` continuation lines. A well-formed
# four-part template no longer fires (retiring the per-site
# `// swiftlint:disable:next workaround_marker_present` shields); a
# truncated, bare, or out-of-order template still fires. Validated
# against the four real shielded template shapes (machine-primitives,
# swift-tests Assertions + Scope.Provider) plus truncated/bare/out-of-
# order negatives.
regex: '//[ \t]*WORKAROUND:(?![^\n]*(?:\n[ \t]*//[^\n]*)*?\n[ \t]*//[ \t]*WHY:[^\n]*(?:\n[ \t]*//[^\n]*)*?\n[ \t]*//[ \t]*WHEN TO REMOVE:[^\n]*(?:\n[ \t]*//[^\n]*)*?\n[ \t]*//[ \t]*TRACKING:)'
message: "Workarounds MUST be paired with the four-part // WHY: // WHEN TO REMOVE: // TRACKING: marker template, in that order, on the comment lines immediately following the // WORKAROUND: marker ([DOC-045], [PATTERN-016])."
match_kinds:
- comment
severity: warning
no_int_bitpattern_arithmetic:
name: "No Int(bitPattern:) Arithmetic ([CONV-010], [IMPL-010])"
# Catches arithmetic on Int(bitPattern:) call sites. Boundary
# overloads in `*Standard_Library_Integration` targets are excluded
# since that's where Int(bitPattern:) legitimately lives per
# [IMPL-010]. Same-package internal use is also excluded via path —
# this rule fires at *consumer* call sites.
#
# The `/` alternative excludes a following `/` or `*` (`(?![/*])`) —
# #218 (swift-tensors witness): a statement-ending call followed by a
# blank line and a `//` comment, or by a same-line `// trailing`
# comment, matched the comment's opening slash as division. Trailing
# whitespace before the operator (`\s*`) still spans newlines
# deliberately, so ordinary wrapped arithmetic with the operator on
# the following line keeps firing; only a `/` that opens a comment is
# excluded. True division (`/` not opening a comment) still matches;
# `+`, `-`, `*` are unambiguous and unchanged.
regex: 'Int\(bitPattern:\s*\w+(\.\w+)*\)\s*(?:[+\-*]|/(?![/*]))'
message: "Arithmetic on Int(bitPattern:) defeats type safety. Use typed operators on primitives types; convert at the boundary, not in the middle of expressions ([CONV-010], [IMPL-010])."
severity: warning
excluded:
- 'Sources/.*Standard_Library_Integration.*'
- 'Sources/.*Standard Library Integration.*'
# ── Disabling a rule ──────────────────────────────────────────────────────────
#
# When you must disable a rule on a specific line, prefer:
#
# // swiftlint:disable:next <rule_name> // reason: <skill-rule | research-doc | tracking-link>
#
# The `// reason:` suffix is RECOMMENDED — not enforced — but makes the
# disable auditable. Examples:
#
# // swiftlint:disable:next force_unwrapping // reason: [MEM-COPY-001a] precondition checked above
# // swiftlint:disable:next custom_rules // reason: research §3.4.2 — Tests/* exception
# // swiftlint:disable:next no_foundation_import_warning // reason: tracking-issue swift-foo#42
#
# Reviewers SHOULD push back on disables without a reason citation. A
# disable without a reason is structurally indistinguishable from drift.