From a0414c919d1ded68cdda585bd9a441eff43ccadb Mon Sep 17 00:00:00 2001 From: Nandhini Subramani <42835327+nandhinisubbu@users.noreply.github.com> Date: Wed, 8 Oct 2025 22:36:56 +0530 Subject: [PATCH] Ignore `@concurrent` functions in `async_without_await` rule (#6284) --- CHANGELOG.md | 5 ++++ .../Rules/Lint/AsyncWithoutAwaitRule.swift | 15 +++++++--- .../Lint/AsyncWithoutAwaitRuleExamples.swift | 28 +++++++++++++++++++ 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29af176ae..42d4a3111 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,6 +66,11 @@ [SimplyDanny](https://github.com/SimplyDanny) [#6253](https://github.com/realm/SwiftLint/issues/6235) +* Exclude `@concurrent` functions from `async_without_await` rule analysis. + `@concurrent` functions requires `aysnc` in any case. + [nandhinisubbu](https://github.com/nandhinisubbu) + [#6283](https://github.com/realm/SwiftLint/issues/6283) + ## 0.61.0: Even Fresher Breeze ### Breaking diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/AsyncWithoutAwaitRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/AsyncWithoutAwaitRule.swift index 9e3e67ec8..7b8b0755b 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/AsyncWithoutAwaitRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/AsyncWithoutAwaitRule.swift @@ -30,7 +30,9 @@ private extension AsyncWithoutAwaitRule { return .visitChildren } - let asyncToken = node.signature.effectSpecifiers?.asyncSpecifier + // @concurrent functions require the async keyword even without await calls + let asyncToken = node.attributes.contains(attributeNamed: "concurrent") + ? nil : node.signature.effectSpecifiers?.asyncSpecifier functionScopes.push(.init(asyncToken: asyncToken)) return .visitChildren @@ -42,8 +44,11 @@ private extension AsyncWithoutAwaitRule { } } - override func visit(_: ClosureExprSyntax) -> SyntaxVisitorContinueKind { - functionScopes.push(.init(asyncToken: pendingAsync)) + override func visit(_ node: ClosureExprSyntax) -> SyntaxVisitorContinueKind { + // @concurrent closures require the async keyword even without await calls + let asyncToken = (node.signature?.attributes.contains(attributeNamed: "concurrent") ?? false) + ? nil : pendingAsync + functionScopes.push(.init(asyncToken: asyncToken)) pendingAsync = nil return .visitChildren } @@ -78,7 +83,9 @@ private extension AsyncWithoutAwaitRule { return .visitChildren } - let asyncToken = node.signature.effectSpecifiers?.asyncSpecifier + // @concurrent can be applied to initializers + let asyncToken = node.attributes.contains(attributeNamed: "concurrent") + ? nil : node.signature.effectSpecifiers?.asyncSpecifier functionScopes.push(.init(asyncToken: asyncToken)) return .visitChildren diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/AsyncWithoutAwaitRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/AsyncWithoutAwaitRuleExamples.swift index 7d4a6564c..91b4a0112 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/AsyncWithoutAwaitRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/AsyncWithoutAwaitRuleExamples.swift @@ -160,6 +160,34 @@ internal struct AsyncWithoutAwaitRuleExamples { } } """, excludeFromDocumentation: true), + Example(""" + @concurrent + func concurrentFunction() async { + performWork() + } + """), + Example(""" + struct S: Sendable { + @concurrent + func alwaysSwitch() async { + // This is valid - @concurrent functions require async even without await + } + } + """), + Example(""" + struct ConcurrentInitExample { + @concurrent + init() async { + setup() + } + func setup() {} + } + """), + Example(""" + struct ConcurrentClosureExample { + let c: () async -> Int = { @concurrent in 1 } + } + """), ] static let triggeringExamples = [