import SwiftLintCore import SwiftSyntax @SwiftSyntaxRule(correctable: true) struct AttributeNameSpacingRule: Rule { var configuration = SeverityConfiguration(.error) static let description = RuleDescription( identifier: "attribute_name_spacing", name: "Attribute Name Spacing", description: """ This rule prevents trailing spaces after attribute names, ensuring compatibility \ with Swift 6 where a space between an attribute name and the opening parenthesis \ results in a compilation error (e.g. `@MyPropertyWrapper ()`, `private (set)`). """, kind: .style, nonTriggeringExamples: [ Example("private(set) var foo: Bool = false"), Example("fileprivate(set) var foo: Bool = false"), Example("@MainActor class Foo {}"), Example("func funcWithEscapingClosure(_ x: @escaping () -> Int) {}"), Example("@available(*, deprecated)"), Example("@MyPropertyWrapper(param: 2) "), Example("nonisolated(unsafe) var _value: X?"), Example("@testable import SwiftLintCore"), Example("func func_type_attribute_with_space(x: @convention(c) () -> Int) {}"), Example(""" @propertyWrapper struct MyPropertyWrapper { var wrappedValue: Int = 1 init(param: Int) {} } """), Example(""" let closure2 = { @MainActor (a: Int, b: Int) in } """), Example(""" let closure1 = { @MainActor (a, b) in } """), ], triggeringExamples: [ Example("private ↓(set) var foo: Bool = false"), Example("fileprivate ↓(set) var foo: Bool = false"), Example("public ↓(set) var foo: Bool = false"), Example(" public ↓(set) var foo: Bool = false"), Example("@ ↓MainActor class Foo {}"), Example("func funcWithEscapingClosure(_ x: @ ↓escaping () -> Int) {}"), Example("func funcWithEscapingClosure(_ x: @escaping↓() -> Int) {}"), Example("@available ↓(*, deprecated)"), Example("@MyPropertyWrapper ↓(param: 2) let a = 1"), Example("nonisolated ↓(unsafe) var _value: X?"), Example("@MyProperty ↓() class Foo {}"), ], corrections: [ Example("private↓ (set) var foo: Bool = false"): Example("private(set) var foo: Bool = false"), Example("fileprivate↓ (set) var foo: Bool = false"): Example("fileprivate(set) var foo: Bool = false"), Example("internal↓ (set) var foo: Bool = false"): Example("internal(set) var foo: Bool = false"), Example("public↓ (set) var foo: Bool = false"): Example("public(set) var foo: Bool = false"), Example("public↓ (set) var foo: Bool = false"): Example("public(set) var foo: Bool = false"), Example("@↓ MainActor"): Example("@MainActor"), Example("func test(_ x: @↓ escaping () -> Int) {}"): Example("func test(_ x: @escaping () -> Int) {}"), Example("func test(_ x: @escaping↓() -> Int) {}"): Example("func test(_ x: @escaping () -> Int) {}"), Example("@available↓ (*, deprecated)"): Example("@available(*, deprecated)"), Example("@MyPropertyWrapper↓ (param: 2) let a = 1"): Example("@MyPropertyWrapper(param: 2) let a = 1"), Example("nonisolated↓ (unsafe) var _value: X?"): Example("nonisolated(unsafe) var _value: X?"), Example("@MyProperty↓ () let a = 1"): Example("@MyProperty() let a = 1"), ] ) } private extension AttributeNameSpacingRule { final class Visitor: ViolationsSyntaxVisitor { override func visitPost(_ node: DeclModifierSyntax) { guard node.detail != nil, node.name.trailingTrivia.isNotEmpty else { return } addViolation( startPosition: node.name.endPositionBeforeTrailingTrivia, endPosition: node.name.endPosition, replacement: "", reason: "There must not be any space between access control modifier and scope" ) } override func visitPost(_ node: AttributeSyntax) { // Check for trailing trivia after the '@' sign. Handles cases like `@ MainActor` / `@ escaping`. if node.atSign.trailingTrivia.isNotEmpty { addViolation( startPosition: node.atSign.endPositionBeforeTrailingTrivia, endPosition: node.atSign.endPosition, replacement: "", reason: "Attributes must not have trivia between `@` and the identifier" ) } let hasTrailingTrivia = node.attributeName.trailingTrivia.isNotEmpty // Handles cases like `@MyPropertyWrapper (param: 2)`. if node.arguments != nil, hasTrailingTrivia { addViolation( startPosition: node.attributeName.endPositionBeforeTrailingTrivia, endPosition: node.attributeName.endPosition, replacement: "", reason: "Attribute declarations with arguments must not have trailing trivia" ) } if !hasTrailingTrivia, node.isEscaping { // Handles cases where escaping has the wrong spacing: `@escaping()` addViolation( startPosition: node.attributeName.endPositionBeforeTrailingTrivia, endPosition: node.attributeName.endPosition, replacement: " ", reason: "`@escaping` must have a trailing space before the associated type" ) } } private func addViolation( startPosition: AbsolutePosition, endPosition: AbsolutePosition, replacement: String, reason: String ) { let correction = ReasonedRuleViolation.ViolationCorrection( start: startPosition, end: endPosition, replacement: replacement ) let violation = ReasonedRuleViolation( position: endPosition, reason: reason, severity: configuration.severity, correction: correction ) violations.append(violation) } } } private extension AttributeSyntax { var isEscaping: Bool { attributeNameText == "escaping" } }