From 2e6b81caa52dd42918be8b5788f4b364d9ce760e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Fri, 10 Oct 2025 21:37:46 +0200 Subject: [PATCH 1/3] Add new `incompatible_concurrency_annotation` rule --- CHANGELOG.md | 8 +- .../Models/BuiltInRules.swift | 1 + ...ncompatibleConcurrencyAnnotationRule.swift | 180 +++++++++++++++ ...bleConcurrencyAnnotationRuleExamples.swift | 215 ++++++++++++++++++ ...leConcurrencyAnnotationConfiguration.swift | 13 ++ .../Models/RuleConfigurationDescription.swift | 1 + .../SwiftLintCore/Models/RuleParameter.swift | 1 + Tests/GeneratedTests/GeneratedTests_04.swift | 12 +- Tests/GeneratedTests/GeneratedTests_05.swift | 12 +- Tests/GeneratedTests/GeneratedTests_06.swift | 12 +- Tests/GeneratedTests/GeneratedTests_07.swift | 12 +- Tests/GeneratedTests/GeneratedTests_08.swift | 12 +- Tests/GeneratedTests/GeneratedTests_09.swift | 12 +- Tests/GeneratedTests/GeneratedTests_10.swift | 6 + .../default_rule_configurations.yml | 6 + 15 files changed, 466 insertions(+), 37 deletions(-) create mode 100644 Source/SwiftLintBuiltInRules/Rules/Lint/IncompatibleConcurrencyAnnotationRule.swift create mode 100644 Source/SwiftLintBuiltInRules/Rules/Lint/IncompatibleConcurrencyAnnotationRuleExamples.swift create mode 100644 Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/IncompatibleConcurrencyAnnotationConfiguration.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 22a9ee130f..ee63f924cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,13 @@ ### Enhancements -* None. +* Add new `incompatible_concurrency_annotation` rule that triggers when a declaration + isolated to a global actor, `@Sendable` closure arguments and/or generic sendable + constraints is not annotated with `@preconcurrency` in order to maintain compatibility + with Swift 5. + [mattmassicotte](https://github.com/mattmassicotte) + [SimplyDanny](https://github.com/SimplyDanny) + [#5987](https://github.com/realm/SwiftLint/issues/5987) ### Bug Fixes diff --git a/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift b/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift index 02b5e663d6..1c13be34bc 100644 --- a/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift +++ b/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift @@ -92,6 +92,7 @@ public let builtInRules: [any Rule.Type] = [ ImplicitReturnRule.self, ImplicitlyUnwrappedOptionalRule.self, InclusiveLanguageRule.self, + IncompatibleConcurrencyAnnotationRule.self, IndentationWidthRule.self, InvalidSwiftLintCommandRule.self, IsDisjointRule.self, diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/IncompatibleConcurrencyAnnotationRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/IncompatibleConcurrencyAnnotationRule.swift new file mode 100644 index 0000000000..cb52e0c6c6 --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/IncompatibleConcurrencyAnnotationRule.swift @@ -0,0 +1,180 @@ +import SwiftLintCore +import SwiftSyntax +import SwiftSyntaxBuilder + +@SwiftSyntaxRule(explicitRewriter: true, optIn: true) +struct IncompatibleConcurrencyAnnotationRule: Rule { + var configuration = IncompatibleConcurrencyAnnotationConfiguration() + + static let description = RuleDescription( + identifier: "incompatible_concurrency_annotation", + name: "Incompatible Concurrency Annotation", + description: "Declaration should be @preconcurrency to maintain compatibility with Swift 5", + rationale: """ + Declarations that use concurrency features such as `@Sendable` closures, `Sendable` generic type + arguments or `@MainActor` (or other global actors) should be annotated with `@preconcurrency` + to ensure compatibility with Swift 5. + + This rule detects public declarations that require `@preconcurrency` and can automatically add + the annotation. + """, + kind: .lint, + minSwiftVersion: .six, + nonTriggeringExamples: IncompatibleConcurrencyAnnotationRuleExamples.nonTriggeringExamples, + triggeringExamples: IncompatibleConcurrencyAnnotationRuleExamples.triggeringExamples, + corrections: IncompatibleConcurrencyAnnotationRuleExamples.corrections + ) +} + +private extension IncompatibleConcurrencyAnnotationRule { + final class Visitor: ViolationsSyntaxVisitor { + override func visitPost(_ node: ClassDeclSyntax) { + collectViolations(node, introducer: node.classKeyword) + } + + override func visitPost(_ node: EnumDeclSyntax) { + collectViolations(node, introducer: node.enumKeyword) + } + + override func visitPost(_ node: FunctionDeclSyntax) { + collectViolations(node, introducer: node.funcKeyword) + } + + override func visitPost(_ node: InitializerDeclSyntax) { + collectViolations(node, introducer: node.initKeyword) + } + + override func visitPost(_ node: ProtocolDeclSyntax) { + collectViolations(node, introducer: node.protocolKeyword) + } + + override func visitPost(_ node: StructDeclSyntax) { + collectViolations(node, introducer: node.structKeyword) + } + + override func visitPost(_ node: SubscriptDeclSyntax) { + collectViolations(node, introducer: node.subscriptKeyword) + } + + private func collectViolations(_ node: some WithModifiersSyntax & WithAttributesSyntax, + introducer: TokenSyntax) { + if preconcurrencyRequired(for: node, with: configuration.globalActors) { + violations.append(at: introducer.positionAfterSkippingLeadingTrivia) + } + } + } + + final class Rewriter: ViolationsSyntaxRewriter { + override func visit(_ node: ClassDeclSyntax) -> DeclSyntax { + super.visit(rewrite(node)) + } + + override func visit(_ node: EnumDeclSyntax) -> DeclSyntax { + super.visit(rewrite(node)) + } + + override func visit(_ node: FunctionDeclSyntax) -> DeclSyntax { + super.visit(rewrite(node)) + } + + override func visit(_ node: InitializerDeclSyntax) -> DeclSyntax { + super.visit(rewrite(node)) + } + + override func visit(_ node: ProtocolDeclSyntax) -> DeclSyntax { + super.visit(rewrite(node)) + } + + override func visit(_ node: StructDeclSyntax) -> DeclSyntax { + super.visit(rewrite(node)) + } + + override func visit(_ node: SubscriptDeclSyntax) -> DeclSyntax { + super.visit(rewrite(node)) + } + + private func rewrite(_ node: T) -> T { + if preconcurrencyRequired(for: node, with: configuration.globalActors) { + numberOfCorrections += 1 + return node.withPreconcurrencyPrepended + } + return node + } + } +} + +private func preconcurrencyRequired(for syntax: some WithModifiersSyntax & WithAttributesSyntax, + with globalActors: Set) -> Bool { + guard syntax.isPublic, !syntax.isPreconcurrency else { + return false + } + let attributeNames = syntax.attributes.compactMap { $0.as(AttributeSyntax.self)?.attributeNameText } + var required = globalActors.intersection(attributeNames).isNotEmpty + if let whereClause = syntax.asProtocol((any WithGenericParametersSyntax).self)?.genericWhereClause { + required = required || whereClause.requirements.contains { requirement in + if case let .conformanceRequirement(conformance) = requirement.requirement { + return conformance.rightType.isSendable + } + return false + } + } + if let function = syntax.as(FunctionDeclSyntax.self) { + required = required || preconcurrencyRequired(for: function.signature.parameterClause, with: globalActors) + } else if let initializer = syntax.as(InitializerDeclSyntax.self) { + required = required || preconcurrencyRequired( + for: initializer.signature.parameterClause, + with: globalActors + ) + } else if let subscriptDecl = syntax.as(SubscriptDeclSyntax.self) { + required = required || preconcurrencyRequired(for: subscriptDecl.parameterClause, with: globalActors) + } + return required +} + +private func preconcurrencyRequired(for parameters: FunctionParameterClauseSyntax, + with globalActors: Set) -> Bool { + parameters.parameters.contains { parameter in + guard let type = parameter.type.as(AttributedTypeSyntax.self) else { + return false + } + return type.attributes.contains { attribute in + if let attributeSyntax = attribute.as(AttributeSyntax.self) { + let attributeName = attributeSyntax.attributeNameText + return attributeName == "Sendable" || globalActors.contains(attributeName) + } + return false + } + } +} + +private extension WithAttributesSyntax where Self: WithModifiersSyntax { + var isPreconcurrency: Bool { + attributes.contains(attributeNamed: "preconcurrency") + } + + var isPublic: Bool { + modifiers.contains(keyword: .public) || modifiers.contains(keyword: .open) + } + + var withPreconcurrencyPrepended: Self { + let leadingWhitespace = Trivia(pieces: leadingTrivia.reversed().prefix { $0.isSpaceOrTab }.reversed()) + let attribute = AttributeListSyntax.Element.attribute("@preconcurrency") + .with(\.leadingTrivia, leadingTrivia) + .with(\.trailingTrivia, .newlines(1)) + return attributes.isEmpty + ? with(\.leadingTrivia, leadingWhitespace).with(\.attributes, [attribute]) + : with(\.attributes, [attribute] + attributes.with(\.leadingTrivia, leadingWhitespace)) + } +} + +private extension TypeSyntax { + var isSendable: Bool { + if let identifierType = self.as(IdentifierTypeSyntax.self) { + return identifierType.name.text == "Sendable" + } + if let compositeType = self.as(CompositionTypeSyntax.self) { + return compositeType.elements.contains { $0.type.isSendable } + } + return false + } +} diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/IncompatibleConcurrencyAnnotationRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/IncompatibleConcurrencyAnnotationRuleExamples.swift new file mode 100644 index 0000000000..66930167de --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/IncompatibleConcurrencyAnnotationRuleExamples.swift @@ -0,0 +1,215 @@ +// swiftlint:disable:next type_name +struct IncompatibleConcurrencyAnnotationRuleExamples { + static let nonTriggeringExamples = [ + // Sendable conformance is fine + Example("public struct S: Sendable {}"), + Example("public class C: Sendable {}"), + Example("public actor A {}"), + + // Non-public declarations are fine + Example("private @MainActor struct S { }"), + Example("@MainActor struct S { }"), + Example("internal @MainActor func globalActor()"), + Example("private @MainActor init() {}"), + Example("internal subscript(index: Int) -> String where String: Sendable { get }"), + + // @preconcurrency makes it compatible + Example("@preconcurrency @MainActor public struct S {}"), + Example("@preconcurrency @MainActor public class C {}"), + Example("@preconcurrency @MainActor public enum E { case a }"), + Example("@preconcurrency @MainActor public protocol P {}"), + Example("@preconcurrency @MainActor public func globalActor()"), + Example("@preconcurrency public func sendableClosure(_ block: @Sendable () -> Void)"), + Example("@preconcurrency public func globalActorClosure(_ block: @MainActor () -> Void)"), + Example("@preconcurrency public init(_ block: @Sendable () -> Void)"), + Example("@preconcurrency public subscript(index: Int) -> String where String: Sendable { get }"), + + // Non-concurrency related cases + Example("public func nonSendableClosure(_ block: () -> Void)"), + Example("public func generic() where T: Equatable"), + Example("public func generic()"), + Example("public init()"), + + // Custom global actors without configuration + Example("public @MyActor enum E { case a }"), + Example("public func customActor(_ block: @MyActor () -> Void)"), + ] + + static let triggeringExamples = [ + // Global actor on public declarations + Example("@MainActor public ↓struct S {}"), + Example("@MainActor public ↓class C {}"), + Example("@MainActor public ↓enum E { case a }"), + Example("@MainActor public ↓protocol GlobalActor {}"), + Example("@MainActor public ↓func globalActor()"), + + // Initializers with global actors + Example(""" + class C { + @MainActor public ↓init() {} + } + """), + Example("@MainActor public ↓init()"), + + // Subscripts with global actors and sendable generics + Example(""" + struct S { + @MainActor public ↓subscript(index: Int) -> String { get } + } + """), + Example("public ↓subscript(index: T) -> Int where T: ExpressibleByIntegerLiteral & Sendable { get }"), + + // Function parameters with concurrency attributes + Example("public ↓func sendableClosure(_ block: @Sendable () -> Void)"), + Example("public ↓func globalActorClosure(_ block: @MainActor () -> Void)"), + Example("public struct S { public ↓func sendableClosure(_ block: @Sendable () -> Void) }"), + Example("public ↓init(_ block: @Sendable () -> Void)"), + Example("public ↓init(param: @MainActor () -> Void)"), + + // Generic where clauses with Sendable + Example("public ↓func generic() where T: Sendable {}"), + Example("public ↓struct S where T: Sendable {}"), + Example("public ↓class C where T: Sendable {}"), + Example("public ↓enum E where T: Sendable { case a }"), + Example("public ↓init() where T: Sendable {}"), + + // Custom global actors with configuration + Example( + "@MyActor public ↓struct S {}", + configuration: ["global_actors": ["MyActor"]] + ), + Example( + "public ↓func globalActorClosure(_ block: @MyActor () -> Void)", + configuration: ["global_actors": ["MyActor"]] + ), + Example( + "@MyActor public ↓func customGlobalActor()", + configuration: ["global_actors": ["MyActor"]] + ), + Example( + "@MyActor public ↓init()", + configuration: ["global_actors": ["MyActor"]] + ), + ] + + static let corrections = [ + // Global actor on declarations + Example(""" + @MainActor + public enum E { case a } + """): + Example(""" + @preconcurrency + @MainActor + public enum E { case a } + """), + + Example("@MainActor public struct S {}"): + Example(""" + @preconcurrency + @MainActor public struct S {} + """), + + Example("@MainActor public class C {}"): + Example(""" + @preconcurrency + @MainActor public class C {} + """), + + Example("@MainActor public protocol P {}"): + Example(""" + @preconcurrency + @MainActor public protocol P {} + """), + + Example("@MainActor public func globalActor() {}"): + Example(""" + @preconcurrency + @MainActor public func globalActor() {} + """), + + // Initializers with global actors + Example(""" + class C { + @MainActor public init() {} + } + """): + Example(""" + class C { + @preconcurrency + @MainActor public init() {} + } + """), + + // Subscripts with global actors + Example(""" + struct S { + @MainActor public subscript(index: Int) -> String { get } + } + """): + Example(""" + struct S { + @preconcurrency + @MainActor public subscript(index: Int) -> String { get } + } + """), + + // Functions with Sendable parameters + Example("public func sendableClosure(_ block: @Sendable () -> Void) {}"): + Example(""" + @preconcurrency + public func sendableClosure(_ block: @Sendable () -> Void) {} + """), + + Example("public func globalActorClosure(_ block: @MainActor () -> Void) {}"): + Example(""" + @preconcurrency + public func globalActorClosure(_ block: @MainActor () -> Void) {} + """), + + // Initializers with Sendable parameters + Example("public init(_ block: @Sendable () -> Void) {}"): + Example(""" + @preconcurrency + public init(_ block: @Sendable () -> Void) {} + """), + + // Generic where clauses with Sendable + Example("public func generic() where T: Sendable {}"): + Example(""" + @preconcurrency + public func generic() where T: Sendable {} + """), + + Example("public struct S where T: Sendable {}"): + Example(""" + @preconcurrency + public struct S where T: Sendable {} + """), + + Example("public subscript(index: T) -> Int where T: Sendable { get }"): + Example(""" + @preconcurrency + public subscript(index: T) -> Int where T: Sendable { get } + """), + + // Custom global actors with configuration + Example( + "@MyActor public struct S {}", + configuration: ["global_actors": ["MyActor"]] + ): + Example(""" + @preconcurrency + @MyActor public struct S {} + """), + + Example( + "public func globalActorClosure(_ block: @MyActor () -> Void) {}", + configuration: ["global_actors": ["MyActor"]] + ): + Example(""" + @preconcurrency + public func globalActorClosure(_ block: @MyActor () -> Void) {} + """), + ] +} diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/IncompatibleConcurrencyAnnotationConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/IncompatibleConcurrencyAnnotationConfiguration.swift new file mode 100644 index 0000000000..32742eddb1 --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/IncompatibleConcurrencyAnnotationConfiguration.swift @@ -0,0 +1,13 @@ +import SwiftLintCore + +@AutoConfigParser +struct IncompatibleConcurrencyAnnotationConfiguration: SeverityBasedRuleConfiguration { + // swiftlint:disable:previous type_name + + typealias Parent = IncompatibleConcurrencyAnnotationRule + + @ConfigurationElement(key: "severity") + private(set) var severityConfiguration = SeverityConfiguration(.warning) + @ConfigurationElement(key: "global_actors", postprocessor: { $0.insert("MainActor") }) + private(set) var globalActors = Set() +} diff --git a/Source/SwiftLintCore/Models/RuleConfigurationDescription.swift b/Source/SwiftLintCore/Models/RuleConfigurationDescription.swift index b7e5de3b62..b035367a8e 100644 --- a/Source/SwiftLintCore/Models/RuleConfigurationDescription.swift +++ b/Source/SwiftLintCore/Models/RuleConfigurationDescription.swift @@ -458,6 +458,7 @@ public struct ConfigurationElement: Equatable, Sendable where T: Sendable { /// The severity that should be assigned to the violation of this parameter's value is met. public let severity: ViolationSeverity diff --git a/Tests/GeneratedTests/GeneratedTests_04.swift b/Tests/GeneratedTests/GeneratedTests_04.swift index 7162ba3e33..20a6b1a48b 100644 --- a/Tests/GeneratedTests/GeneratedTests_04.swift +++ b/Tests/GeneratedTests/GeneratedTests_04.swift @@ -97,6 +97,12 @@ final class InclusiveLanguageRuleGeneratedTests: SwiftLintTestCase { } } +final class IncompatibleConcurrencyAnnotationRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(IncompatibleConcurrencyAnnotationRule.description) + } +} + final class IndentationWidthRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(IndentationWidthRule.description) @@ -150,9 +156,3 @@ final class LegacyConstantRuleGeneratedTests: SwiftLintTestCase { verifyRule(LegacyConstantRule.description) } } - -final class LegacyConstructorRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(LegacyConstructorRule.description) - } -} diff --git a/Tests/GeneratedTests/GeneratedTests_05.swift b/Tests/GeneratedTests/GeneratedTests_05.swift index 26a5a1a72a..b99e15e1df 100644 --- a/Tests/GeneratedTests/GeneratedTests_05.swift +++ b/Tests/GeneratedTests/GeneratedTests_05.swift @@ -7,6 +7,12 @@ @testable import SwiftLintCore import TestHelpers +final class LegacyConstructorRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(LegacyConstructorRule.description) + } +} + final class LegacyHashingRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(LegacyHashingRule.description) @@ -150,9 +156,3 @@ final class NSObjectPreferIsEqualRuleGeneratedTests: SwiftLintTestCase { verifyRule(NSObjectPreferIsEqualRule.description) } } - -final class NestingRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(NestingRule.description) - } -} diff --git a/Tests/GeneratedTests/GeneratedTests_06.swift b/Tests/GeneratedTests/GeneratedTests_06.swift index 0067f2f7a0..fdaf0e0c07 100644 --- a/Tests/GeneratedTests/GeneratedTests_06.swift +++ b/Tests/GeneratedTests/GeneratedTests_06.swift @@ -7,6 +7,12 @@ @testable import SwiftLintCore import TestHelpers +final class NestingRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(NestingRule.description) + } +} + final class NimbleOperatorRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(NimbleOperatorRule.description) @@ -150,9 +156,3 @@ final class PreferConditionListRuleGeneratedTests: SwiftLintTestCase { verifyRule(PreferConditionListRule.description) } } - -final class PreferKeyPathRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(PreferKeyPathRule.description) - } -} diff --git a/Tests/GeneratedTests/GeneratedTests_07.swift b/Tests/GeneratedTests/GeneratedTests_07.swift index cee0783997..e8bf2eacc0 100644 --- a/Tests/GeneratedTests/GeneratedTests_07.swift +++ b/Tests/GeneratedTests/GeneratedTests_07.swift @@ -7,6 +7,12 @@ @testable import SwiftLintCore import TestHelpers +final class PreferKeyPathRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(PreferKeyPathRule.description) + } +} + final class PreferNimbleRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(PreferNimbleRule.description) @@ -150,9 +156,3 @@ final class RedundantObjcAttributeRuleGeneratedTests: SwiftLintTestCase { verifyRule(RedundantObjcAttributeRule.description) } } - -final class RedundantSelfInClosureRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(RedundantSelfInClosureRule.description) - } -} diff --git a/Tests/GeneratedTests/GeneratedTests_08.swift b/Tests/GeneratedTests/GeneratedTests_08.swift index 404c7ed757..b3c989ce75 100644 --- a/Tests/GeneratedTests/GeneratedTests_08.swift +++ b/Tests/GeneratedTests/GeneratedTests_08.swift @@ -7,6 +7,12 @@ @testable import SwiftLintCore import TestHelpers +final class RedundantSelfInClosureRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(RedundantSelfInClosureRule.description) + } +} + final class RedundantSendableRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(RedundantSendableRule.description) @@ -150,9 +156,3 @@ final class SuperfluousElseRuleGeneratedTests: SwiftLintTestCase { verifyRule(SuperfluousElseRule.description) } } - -final class SwitchCaseAlignmentRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(SwitchCaseAlignmentRule.description) - } -} diff --git a/Tests/GeneratedTests/GeneratedTests_09.swift b/Tests/GeneratedTests/GeneratedTests_09.swift index 0fedfd4eae..b7081609b9 100644 --- a/Tests/GeneratedTests/GeneratedTests_09.swift +++ b/Tests/GeneratedTests/GeneratedTests_09.swift @@ -7,6 +7,12 @@ @testable import SwiftLintCore import TestHelpers +final class SwitchCaseAlignmentRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(SwitchCaseAlignmentRule.description) + } +} + final class SwitchCaseOnNewlineRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(SwitchCaseOnNewlineRule.description) @@ -150,9 +156,3 @@ final class UnusedClosureParameterRuleGeneratedTests: SwiftLintTestCase { verifyRule(UnusedClosureParameterRule.description) } } - -final class UnusedControlFlowLabelRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(UnusedControlFlowLabelRule.description) - } -} diff --git a/Tests/GeneratedTests/GeneratedTests_10.swift b/Tests/GeneratedTests/GeneratedTests_10.swift index 648dde1162..ec711592c4 100644 --- a/Tests/GeneratedTests/GeneratedTests_10.swift +++ b/Tests/GeneratedTests/GeneratedTests_10.swift @@ -7,6 +7,12 @@ @testable import SwiftLintCore import TestHelpers +final class UnusedControlFlowLabelRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(UnusedControlFlowLabelRule.description) + } +} + final class UnusedDeclarationRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(UnusedDeclarationRule.description) diff --git a/Tests/IntegrationTests/default_rule_configurations.yml b/Tests/IntegrationTests/default_rule_configurations.yml index 89809ef4d8..2845345399 100644 --- a/Tests/IntegrationTests/default_rule_configurations.yml +++ b/Tests/IntegrationTests/default_rule_configurations.yml @@ -526,6 +526,12 @@ inclusive_language: meta: opt-in: false correctable: false +incompatible_concurrency_annotation: + severity: warning + global_actors: ["MainActor"] + meta: + opt-in: true + correctable: true indentation_width: severity: warning indentation_width: 4 From 30740ad25fcc3579f6f6967806b4d2be5b665807 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Wed, 15 Oct 2025 21:01:23 +0200 Subject: [PATCH 2/3] Support `sending` parameters and return types --- ...ncompatibleConcurrencyAnnotationRule.swift | 72 ++++++++----- ...bleConcurrencyAnnotationRuleExamples.swift | 101 +++++++++++++++++- 2 files changed, 144 insertions(+), 29 deletions(-) diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/IncompatibleConcurrencyAnnotationRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/IncompatibleConcurrencyAnnotationRule.swift index cb52e0c6c6..1fff95d308 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/IncompatibleConcurrencyAnnotationRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/IncompatibleConcurrencyAnnotationRule.swift @@ -108,8 +108,13 @@ private func preconcurrencyRequired(for syntax: some WithModifiersSyntax & WithA guard syntax.isPublic, !syntax.isPreconcurrency else { return false } + + // Check attributes for global actors. let attributeNames = syntax.attributes.compactMap { $0.as(AttributeSyntax.self)?.attributeNameText } var required = globalActors.intersection(attributeNames).isNotEmpty + if required { return true } + + // Check generic type constraints for `@Sendable`. if let whereClause = syntax.asProtocol((any WithGenericParametersSyntax).self)?.genericWhereClause { required = required || whereClause.requirements.contains { requirement in if case let .conformanceRequirement(conformance) = requirement.requirement { @@ -117,34 +122,26 @@ private func preconcurrencyRequired(for syntax: some WithModifiersSyntax & WithA } return false } + if required { return true } } - if let function = syntax.as(FunctionDeclSyntax.self) { - required = required || preconcurrencyRequired(for: function.signature.parameterClause, with: globalActors) - } else if let initializer = syntax.as(InitializerDeclSyntax.self) { - required = required || preconcurrencyRequired( - for: initializer.signature.parameterClause, - with: globalActors - ) - } else if let subscriptDecl = syntax.as(SubscriptDeclSyntax.self) { - required = required || preconcurrencyRequired(for: subscriptDecl.parameterClause, with: globalActors) - } - return required -} -private func preconcurrencyRequired(for parameters: FunctionParameterClauseSyntax, - with globalActors: Set) -> Bool { - parameters.parameters.contains { parameter in - guard let type = parameter.type.as(AttributedTypeSyntax.self) else { - return false + // Check parameters for `@Sendable`, `sending` and global actors. + let parameterClause = syntax.as(FunctionDeclSyntax.self)?.signature.parameterClause + ?? syntax.as(InitializerDeclSyntax.self)?.signature.parameterClause + ?? syntax.as(SubscriptDeclSyntax.self)?.parameterClause + let visitor = SendableTypeVisitor(globalActors: globalActors) + if let parameterClause { + required = required || parameterClause.parameters.contains { visitor.walk(tree: $0, handler: \.found) } + if required { return true } } - return type.attributes.contains { attribute in - if let attributeSyntax = attribute.as(AttributeSyntax.self) { - let attributeName = attributeSyntax.attributeNameText - return attributeName == "Sendable" || globalActors.contains(attributeName) - } - return false + + // Check return types for `@Sendable`, `sending` and global actors. + let returnType = syntax.as(FunctionDeclSyntax.self)?.signature.returnClause?.type + ?? syntax.as(SubscriptDeclSyntax.self)?.returnClause.type + if let returnType { + required = required || visitor.walk(tree: returnType, handler: \.found) } - } + return required } private extension WithAttributesSyntax where Self: WithModifiersSyntax { @@ -178,3 +175,30 @@ private extension TypeSyntax { return false } } + +private final class SendableTypeVisitor: SyntaxVisitor { + private(set) var found = false + + private let globalActors: Set + + init(globalActors: Set) { + self.globalActors = globalActors + super.init(viewMode: .sourceAccurate) + } + + override func visitPost(_ node: AttributedTypeSyntax) { + if found { + return + } + found = found || node.attributes.contains { + if let attribute = $0.as(AttributeSyntax.self) { + let name = attribute.attributeNameText + return name == "Sendable" || globalActors.contains(name) + } + return false + } + found = found || node.specifiers.contains { + $0.as(SimpleTypeSpecifierSyntax.self)?.specifier.text == "sending" + } + } +} diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/IncompatibleConcurrencyAnnotationRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/IncompatibleConcurrencyAnnotationRuleExamples.swift index 66930167de..3451892943 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/IncompatibleConcurrencyAnnotationRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/IncompatibleConcurrencyAnnotationRuleExamples.swift @@ -22,7 +22,28 @@ struct IncompatibleConcurrencyAnnotationRuleExamples { Example("@preconcurrency public func sendableClosure(_ block: @Sendable () -> Void)"), Example("@preconcurrency public func globalActorClosure(_ block: @MainActor () -> Void)"), Example("@preconcurrency public init(_ block: @Sendable () -> Void)"), - Example("@preconcurrency public subscript(index: Int) -> String where String: Sendable { get }"), + Example( + "@preconcurrency public subscript(index: Int) -> String where String: Sendable { get }"), + Example("@preconcurrency public func sendableReturningClosure() -> @Sendable () -> Void"), + Example( + "@preconcurrency public func globalActorReturningClosure() -> @MainActor () -> Void"), + Example("@preconcurrency public func sendingParameter(_ value: sending MyClass)"), + Example(""" + @preconcurrency public func tupleParameterClosures( + _ handlers: (@Sendable () -> Void, @MainActor () -> Void) + ) + """), + Example(""" + @preconcurrency public func tupleReturningClosures() -> ( + @Sendable () -> Void, + @MainActor () -> Void + ) + """), + Example(""" + @preconcurrency public func closureWithSendingArgument( + _ handler: (_ value: sending MyClass) -> Void + ) + """), // Non-concurrency related cases Example("public func nonSendableClosure(_ block: () -> Void)"), @@ -65,6 +86,18 @@ struct IncompatibleConcurrencyAnnotationRuleExamples { Example("public struct S { public ↓func sendableClosure(_ block: @Sendable () -> Void) }"), Example("public ↓init(_ block: @Sendable () -> Void)"), Example("public ↓init(param: @MainActor () -> Void)"), + Example("public ↓func sendingParameter(_ value: sending MyClass)"), + Example("public ↓func closureWithSendingArgument(_ handler: (_ value: sending MyClass) -> Void)"), + Example(""" + public ↓func tupleParameter( + _ handlers: (@Sendable () -> Void, @MainActor () -> Void) + ) + """), + Example(""" + public ↓func tupleWithSending( + _ handlers: ((_ value: sending MyClass) -> Void, @MainActor () -> Void) + ) + """), // Generic where clauses with Sendable Example("public ↓func generic() where T: Sendable {}"), @@ -73,6 +106,12 @@ struct IncompatibleConcurrencyAnnotationRuleExamples { Example("public ↓enum E where T: Sendable { case a }"), Example("public ↓init() where T: Sendable {}"), + // Return types with concurrency attributes + Example("public ↓func returnsSendableClosure() -> @Sendable () -> Void"), + Example("public ↓func returnsActorClosure() -> @MainActor () -> Void"), + Example("public ↓func returnsClosureTuple() -> (@Sendable () -> Void, @MainActor () -> Void)"), + Example("public ↓func returnsClosureWithSendingArgument() -> (_ value: sending MyClass) -> Void"), + // Custom global actors with configuration Example( "@MyActor public ↓struct S {}", @@ -167,6 +206,36 @@ struct IncompatibleConcurrencyAnnotationRuleExamples { public func globalActorClosure(_ block: @MainActor () -> Void) {} """), + Example("public func sendingParameter(_ value: sending MyClass) {}"): + Example(""" + @preconcurrency + public func sendingParameter(_ value: sending MyClass) {} + """), + + Example("public func closureWithSendingArgument(_ handler: (_ value: sending MyClass) -> Void) {}"): + Example(""" + @preconcurrency + public func closureWithSendingArgument(_ handler: (_ value: sending MyClass) -> Void) {} + """), + + Example("public func tupleParameter(_ handlers: (@Sendable () -> Void, @MainActor () -> Void)) {}"): + Example(""" + @preconcurrency + public func tupleParameter(_ handlers: (@Sendable () -> Void, @MainActor () -> Void)) {} + """), + + Example(""" + public func tupleWithSending( + _ handlers: ((_ value: sending MyClass) -> Void, @MainActor () -> Void) + ) {} + """): + Example(""" + @preconcurrency + public func tupleWithSending( + _ handlers: ((_ value: sending MyClass) -> Void, @MainActor () -> Void) + ) {} + """), + // Initializers with Sendable parameters Example("public init(_ block: @Sendable () -> Void) {}"): Example(""" @@ -193,11 +262,34 @@ struct IncompatibleConcurrencyAnnotationRuleExamples { public subscript(index: T) -> Int where T: Sendable { get } """), + Example("public func returnsSendableClosure() -> @Sendable () -> Void {}"): + Example(""" + @preconcurrency + public func returnsSendableClosure() -> @Sendable () -> Void {} + """), + + Example("public func returnsActorClosure() -> @MainActor () -> Void {}"): + Example(""" + @preconcurrency + public func returnsActorClosure() -> @MainActor () -> Void {} + """), + + Example("public func returnsClosureTuple() -> (@Sendable () -> Void, @MainActor () -> Void) {}"): + Example(""" + @preconcurrency + public func returnsClosureTuple() -> (@Sendable () -> Void, @MainActor () -> Void) {} + """), + + Example("public func returnsClosureWithSendingArgument() -> (_ value: sending MyClass) -> Void {}"): + Example(""" + @preconcurrency + public func returnsClosureWithSendingArgument() -> (_ value: sending MyClass) -> Void {} + """), + // Custom global actors with configuration Example( "@MyActor public struct S {}", - configuration: ["global_actors": ["MyActor"]] - ): + configuration: ["global_actors": ["MyActor"]]): Example(""" @preconcurrency @MyActor public struct S {} @@ -205,8 +297,7 @@ struct IncompatibleConcurrencyAnnotationRuleExamples { Example( "public func globalActorClosure(_ block: @MyActor () -> Void) {}", - configuration: ["global_actors": ["MyActor"]] - ): + configuration: ["global_actors": ["MyActor"]]): Example(""" @preconcurrency public func globalActorClosure(_ block: @MyActor () -> Void) {} From 81500d0f55e77d532299b54ed0fb561005d2d503 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Mon, 20 Oct 2025 19:31:37 +0200 Subject: [PATCH 3/3] Remove checking for `sending` --- ...ncompatibleConcurrencyAnnotationRule.swift | 7 ++----- ...bleConcurrencyAnnotationRuleExamples.swift | 21 ------------------- 2 files changed, 2 insertions(+), 26 deletions(-) diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/IncompatibleConcurrencyAnnotationRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/IncompatibleConcurrencyAnnotationRule.swift index 1fff95d308..8593fea751 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/IncompatibleConcurrencyAnnotationRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/IncompatibleConcurrencyAnnotationRule.swift @@ -125,7 +125,7 @@ private func preconcurrencyRequired(for syntax: some WithModifiersSyntax & WithA if required { return true } } - // Check parameters for `@Sendable`, `sending` and global actors. + // Check parameters for `@Sendable` and global actors. let parameterClause = syntax.as(FunctionDeclSyntax.self)?.signature.parameterClause ?? syntax.as(InitializerDeclSyntax.self)?.signature.parameterClause ?? syntax.as(SubscriptDeclSyntax.self)?.parameterClause @@ -135,7 +135,7 @@ private func preconcurrencyRequired(for syntax: some WithModifiersSyntax & WithA if required { return true } } - // Check return types for `@Sendable`, `sending` and global actors. + // Check return types for `@Sendable` and global actors. let returnType = syntax.as(FunctionDeclSyntax.self)?.signature.returnClause?.type ?? syntax.as(SubscriptDeclSyntax.self)?.returnClause.type if let returnType { @@ -197,8 +197,5 @@ private final class SendableTypeVisitor: SyntaxVisitor { } return false } - found = found || node.specifiers.contains { - $0.as(SimpleTypeSpecifierSyntax.self)?.specifier.text == "sending" - } } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/IncompatibleConcurrencyAnnotationRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/IncompatibleConcurrencyAnnotationRuleExamples.swift index 3451892943..d51097d159 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/IncompatibleConcurrencyAnnotationRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/IncompatibleConcurrencyAnnotationRuleExamples.swift @@ -86,8 +86,6 @@ struct IncompatibleConcurrencyAnnotationRuleExamples { Example("public struct S { public ↓func sendableClosure(_ block: @Sendable () -> Void) }"), Example("public ↓init(_ block: @Sendable () -> Void)"), Example("public ↓init(param: @MainActor () -> Void)"), - Example("public ↓func sendingParameter(_ value: sending MyClass)"), - Example("public ↓func closureWithSendingArgument(_ handler: (_ value: sending MyClass) -> Void)"), Example(""" public ↓func tupleParameter( _ handlers: (@Sendable () -> Void, @MainActor () -> Void) @@ -110,7 +108,6 @@ struct IncompatibleConcurrencyAnnotationRuleExamples { Example("public ↓func returnsSendableClosure() -> @Sendable () -> Void"), Example("public ↓func returnsActorClosure() -> @MainActor () -> Void"), Example("public ↓func returnsClosureTuple() -> (@Sendable () -> Void, @MainActor () -> Void)"), - Example("public ↓func returnsClosureWithSendingArgument() -> (_ value: sending MyClass) -> Void"), // Custom global actors with configuration Example( @@ -206,18 +203,6 @@ struct IncompatibleConcurrencyAnnotationRuleExamples { public func globalActorClosure(_ block: @MainActor () -> Void) {} """), - Example("public func sendingParameter(_ value: sending MyClass) {}"): - Example(""" - @preconcurrency - public func sendingParameter(_ value: sending MyClass) {} - """), - - Example("public func closureWithSendingArgument(_ handler: (_ value: sending MyClass) -> Void) {}"): - Example(""" - @preconcurrency - public func closureWithSendingArgument(_ handler: (_ value: sending MyClass) -> Void) {} - """), - Example("public func tupleParameter(_ handlers: (@Sendable () -> Void, @MainActor () -> Void)) {}"): Example(""" @preconcurrency @@ -280,12 +265,6 @@ struct IncompatibleConcurrencyAnnotationRuleExamples { public func returnsClosureTuple() -> (@Sendable () -> Void, @MainActor () -> Void) {} """), - Example("public func returnsClosureWithSendingArgument() -> (_ value: sending MyClass) -> Void {}"): - Example(""" - @preconcurrency - public func returnsClosureWithSendingArgument() -> (_ value: sending MyClass) -> Void {} - """), - // Custom global actors with configuration Example( "@MyActor public struct S {}",