From 325d0ee1e44a87fc82afeb874b83ceb82f6728cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Tue, 31 Jan 2023 22:34:11 +0100 Subject: [PATCH] Consider custom attributes in `attributes` rule (#4616) --- .swiftlint.yml | 3 + CHANGELOG.md | 4 ++ .../Rules/Style/AttributesRule.swift | 70 +++++++++++++++---- .../Rules/Style/AttributesRuleExamples.swift | 7 ++ Source/swiftlint/Commands/GenerateDocs.swift | 3 +- Source/swiftlint/Commands/Rules.swift | 3 +- 6 files changed, 76 insertions(+), 14 deletions(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index 7c60ecd94..f874044ef 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -79,6 +79,9 @@ opt_in_rules: - xct_specific_matcher - yoda_condition +attributes: + always_on_line_above: + - "@OptionGroup" identifier_name: excluded: - id diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a92dc64d..8937fdd2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,10 @@ [SimplyDanny](https://github.com/SimplyDanny) [#4645](https://github.com/realm/SwiftLint/issues/4645) +* Consider custom attributes in `attributes` rule. + [SimplyDanny](https://github.com/SimplyDanny) + [#4599](https://github.com/realm/SwiftLint/issues/4599) + * Fix false positives on `private_subject` rule when using subjects inside functions. [Marcelo Fabri](https://github.com/marcelofabri) diff --git a/Source/SwiftLintFramework/Rules/Style/AttributesRule.swift b/Source/SwiftLintFramework/Rules/Style/AttributesRule.swift index d5e39072b..4939c6c19 100644 --- a/Source/SwiftLintFramework/Rules/Style/AttributesRule.swift +++ b/Source/SwiftLintFramework/Rules/Style/AttributesRule.swift @@ -121,7 +121,7 @@ private struct RuleHelper { func hasViolation( locationConverter: SourceLocationConverter, - attributesAndPlacements: [(AttributeSyntax, AttributePlacement)] + attributesAndPlacements: [(SyntaxProtocol, AttributePlacement)] ) -> Bool { var linesWithAttributes: Set = [keywordLine] for (attribute, placement) in attributesAndPlacements { @@ -147,23 +147,69 @@ private struct RuleHelper { } } +private enum Attribute { + case builtIn(AttributeSyntax) + case custom(CustomAttributeSyntax) + + static func from(syntax: SyntaxProtocol) -> Self? { + if let attribute = syntax.as(AttributeSyntax.self) { + return .builtIn(attribute) + } + if let attribute = syntax.as(CustomAttributeSyntax.self) { + return .custom(attribute) + } + return nil + } + + var hasArguments: Bool { + switch self { + case let .builtIn(attribute): + return attribute.argument != nil + case let .custom(attribute): + return attribute.argumentList != nil + } + } + + var name: String? { + switch self { + case let .builtIn(attribute): + return attribute.attributeName.text + case let .custom(attribute): + return attribute.attributeName.as(SimpleTypeIdentifierSyntax.self)?.typeName + } + } + + var syntaxNode: SyntaxProtocol { + switch self { + case let .builtIn(attribute): + return attribute + case let .custom(attribute): + return attribute + } + } +} + private extension AttributeListSyntax { func attributesAndPlacements(configuration: AttributesConfiguration, shouldBeOnSameLine: Bool) - -> [(AttributeSyntax, AttributePlacement)] { - self - .children(viewMode: .sourceAccurate) - .compactMap { $0.as(AttributeSyntax.self) } - .map { attribute in - let atPrefixedName = "@\(attribute.attributeName.text)" + -> [(SyntaxProtocol, AttributePlacement)] { + self.children(viewMode: .sourceAccurate) + .compactMap { Attribute.from(syntax: $0) } + .compactMap { attribute in + guard let attributeName = attribute.name else { + return nil + } + let atPrefixedName = "@\(attributeName)" if configuration.alwaysOnSameLine.contains(atPrefixedName) { - return (attribute, .sameLineAsDeclaration) + return (attribute.syntaxNode, .sameLineAsDeclaration) } else if configuration.alwaysOnNewLine.contains(atPrefixedName) { - return (attribute, .dedicatedLine) - } else if attribute.argument != nil { - return (attribute, .dedicatedLine) + return (attribute.syntaxNode, .dedicatedLine) + } else if attribute.hasArguments { + return (attribute.syntaxNode, .dedicatedLine) } - return shouldBeOnSameLine ? (attribute, .sameLineAsDeclaration) : (attribute, .dedicatedLine) + return shouldBeOnSameLine + ? (attribute.syntaxNode, .sameLineAsDeclaration) + : (attribute.syntaxNode, .dedicatedLine) } } diff --git a/Source/SwiftLintFramework/Rules/Style/AttributesRuleExamples.swift b/Source/SwiftLintFramework/Rules/Style/AttributesRuleExamples.swift index d37c0be34..15f90e113 100644 --- a/Source/SwiftLintFramework/Rules/Style/AttributesRuleExamples.swift +++ b/Source/SwiftLintFramework/Rules/Style/AttributesRuleExamples.swift @@ -73,6 +73,13 @@ internal struct AttributesRuleExamples { func refreshable(action: @escaping @Sendable () async -> Void) -> some View { modifier(RefreshableModifier(action: action)) } + """), + Example(""" + import AppKit + + @NSApplicationMain + @MainActor + final class AppDelegate: NSAppDelegate {} """) ] diff --git a/Source/swiftlint/Commands/GenerateDocs.swift b/Source/swiftlint/Commands/GenerateDocs.swift index 82b6d4f89..fc448fc59 100644 --- a/Source/swiftlint/Commands/GenerateDocs.swift +++ b/Source/swiftlint/Commands/GenerateDocs.swift @@ -12,7 +12,8 @@ extension SwiftLint { var path = "rule_docs" @Option(help: "The path to a SwiftLint configuration file") var config: String? - @OptionGroup var rulesFilterOptions: RulesFilterOptions + @OptionGroup + var rulesFilterOptions: RulesFilterOptions func run() throws { let configuration = Configuration(configurationFiles: [config].compactMap({ $0 })) diff --git a/Source/swiftlint/Commands/Rules.swift b/Source/swiftlint/Commands/Rules.swift index a4142f8bd..d3c4efd90 100644 --- a/Source/swiftlint/Commands/Rules.swift +++ b/Source/swiftlint/Commands/Rules.swift @@ -17,7 +17,8 @@ extension SwiftLint { @Option(help: "The path to a SwiftLint configuration file") var config: String? - @OptionGroup var rulesFilterOptions: RulesFilterOptions + @OptionGroup + var rulesFilterOptions: RulesFilterOptions @Flag(name: .shortAndLong, help: "Display full configuration details") var verbose = false @Argument(help: "The rule identifier to display description for")