From 1c9d74317759b240a04d1f064833189fe04394a9 Mon Sep 17 00:00:00 2001 From: Tigran Hambardzumyan Date: Sat, 13 Jul 2024 20:56:33 +0400 Subject: [PATCH 1/6] Add check to allow underscores in test functions marked with @Test attribute --- Documentation/RuleDocumentation.md | 1 + .../Rules/AlwaysUseLowerCamelCase.swift | 6 +++++- .../Rules/AlwaysUseLowerCamelCaseTests.swift | 20 +++++++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/Documentation/RuleDocumentation.md b/Documentation/RuleDocumentation.md index d9fa83760..9d6f8d657 100644 --- a/Documentation/RuleDocumentation.md +++ b/Documentation/RuleDocumentation.md @@ -79,6 +79,7 @@ Underscores (except at the beginning of an identifier) are disallowed. This rule does not apply to test code, defined as code which: * Contains the line `import XCTest` + * The function is marked with `@Test` attribute Lint: If an identifier contains underscores or begins with a capital letter, a lint error is raised. diff --git a/Sources/SwiftFormat/Rules/AlwaysUseLowerCamelCase.swift b/Sources/SwiftFormat/Rules/AlwaysUseLowerCamelCase.swift index e24d23178..8e11938ce 100644 --- a/Sources/SwiftFormat/Rules/AlwaysUseLowerCamelCase.swift +++ b/Sources/SwiftFormat/Rules/AlwaysUseLowerCamelCase.swift @@ -100,7 +100,11 @@ public final class AlwaysUseLowerCamelCase: SyntaxLintRule { // We allow underscores in test names, because there's an existing convention of using // underscores to separate phrases in very detailed test names. - let allowUnderscores = testCaseFuncs.contains(node) + let allowUnderscores = testCaseFuncs.contains(node) || node.attributes.contains { + // Allow underscore for test functions with the `@Test` attribute. + $0.as(AttributeSyntax.self)?.attributeName.as(IdentifierTypeSyntax.self)?.name.text == "Test" + } + diagnoseLowerCamelCaseViolations( node.name, allowUnderscores: allowUnderscores, description: identifierDescription(for: node)) diff --git a/Tests/SwiftFormatTests/Rules/AlwaysUseLowerCamelCaseTests.swift b/Tests/SwiftFormatTests/Rules/AlwaysUseLowerCamelCaseTests.swift index 59201e42e..750183609 100644 --- a/Tests/SwiftFormatTests/Rules/AlwaysUseLowerCamelCaseTests.swift +++ b/Tests/SwiftFormatTests/Rules/AlwaysUseLowerCamelCaseTests.swift @@ -210,4 +210,24 @@ final class AlwaysUseLowerCamelCaseTests: LintOrFormatRuleTestCase { ] ) } + + func testIgnoresFunctionsWithTestAttributes() { + assertLint( + AlwaysUseLowerCamelCase.self, + """ + @Test + func function_With_Test_Attribute() {} + @Test("Description for test functions", + .tags(.testTag)) + func function_With_Test_Attribute_And_Args() {} + func 1️⃣function_Without_Test_Attribute() {} + @objc + func 2️⃣function_With_Non_Test_Attribute() {} + """, + findings: [ + FindingSpec("1️⃣", message: "rename the function 'function_Without_Test_Attribute' using lowerCamelCase"), + FindingSpec("2️⃣", message: "rename the function 'function_With_Non_Test_Attribute' using lowerCamelCase"), + ] + ) + } } From 5a63aee663a33b860c6a3bdfe8c5c15944908158 Mon Sep 17 00:00:00 2001 From: Tigran Hambardzumyan Date: Sun, 14 Jul 2024 18:00:07 +0400 Subject: [PATCH 2/6] Add Swift Testing checks In the test code validation added checks to consider new Swift Testing syntax as well. --- Documentation/RuleDocumentation.md | 3 +++ .../Core/SyntaxProtocol+Convenience.swift | 15 +++++++++++++++ .../SwiftFormat/Rules/NeverForceUnwrap.swift | 4 ++++ .../SwiftFormat/Rules/NeverUseForceTry.swift | 2 ++ ...NeverUseImplicitlyUnwrappedOptionals.swift | 2 ++ .../Rules/NeverForceUnwrapTests.swift | 19 +++++++++++++++++++ .../Rules/NeverUseForceTryTests.swift | 16 ++++++++++++++++ ...UseImplicitlyUnwrappedOptionalsTests.swift | 16 ++++++++++++++++ 8 files changed, 77 insertions(+) diff --git a/Documentation/RuleDocumentation.md b/Documentation/RuleDocumentation.md index d9fa83760..521f64723 100644 --- a/Documentation/RuleDocumentation.md +++ b/Documentation/RuleDocumentation.md @@ -188,6 +188,7 @@ Force-unwraps are strongly discouraged and must be documented. This rule does not apply to test code, defined as code which: * Contains the line `import XCTest` + * The function is marked with `@Test` attribute Lint: If a force unwrap is used, a lint warning is raised. @@ -199,6 +200,7 @@ Force-try (`try!`) is forbidden. This rule does not apply to test code, defined as code which: * Contains the line `import XCTest` + * The function is marked with `@Test` attribute Lint: Using `try!` results in a lint error. @@ -214,6 +216,7 @@ Certain properties (e.g. `@IBOutlet`) tied to the UI lifecycle are ignored. This rule does not apply to test code, defined as code which: * Contains the line `import XCTest` + * The function is marked with `@Test` attribute TODO: Create exceptions for other UI elements (ex: viewDidLoad) diff --git a/Sources/SwiftFormat/Core/SyntaxProtocol+Convenience.swift b/Sources/SwiftFormat/Core/SyntaxProtocol+Convenience.swift index 033410212..02b9a328a 100644 --- a/Sources/SwiftFormat/Core/SyntaxProtocol+Convenience.swift +++ b/Sources/SwiftFormat/Core/SyntaxProtocol+Convenience.swift @@ -149,6 +149,21 @@ extension SyntaxProtocol { } return leadingTrivia.hasAnyComments } + + /// Indicates whether the node has any function ancestor marked with `@Test` attribute. + var hasTestAncestor: Bool { + var parent = self.parent + while let existingParent = parent { + if let functionDecl = existingParent.as(FunctionDeclSyntax.self), + functionDecl.attributes.contains(where: { + $0.as(AttributeSyntax.self)?.attributeName.as(IdentifierTypeSyntax.self)?.name.text == "Test" + }) { + return true + } + parent = existingParent.parent + } + return false + } } extension SyntaxCollection { diff --git a/Sources/SwiftFormat/Rules/NeverForceUnwrap.swift b/Sources/SwiftFormat/Rules/NeverForceUnwrap.swift index 29937b987..05a7b935c 100644 --- a/Sources/SwiftFormat/Rules/NeverForceUnwrap.swift +++ b/Sources/SwiftFormat/Rules/NeverForceUnwrap.swift @@ -34,6 +34,8 @@ public final class NeverForceUnwrap: SyntaxLintRule { public override func visit(_ node: ForceUnwrapExprSyntax) -> SyntaxVisitorContinueKind { guard context.importsXCTest == .doesNotImportXCTest else { return .skipChildren } + // Allow force unwrapping if it is in a function marked with @Test attribute. + if node.hasTestAncestor { return .skipChildren } diagnose(.doNotForceUnwrap(name: node.expression.trimmedDescription), on: node) return .skipChildren } @@ -44,6 +46,8 @@ public final class NeverForceUnwrap: SyntaxLintRule { guard context.importsXCTest == .doesNotImportXCTest else { return .skipChildren } guard let questionOrExclamation = node.questionOrExclamationMark else { return .skipChildren } guard questionOrExclamation.tokenKind == .exclamationMark else { return .skipChildren } + // Allow force cast if it is in a function marked with @Test attribute. + if node.hasTestAncestor { return .skipChildren } diagnose(.doNotForceCast(name: node.type.trimmedDescription), on: node) return .skipChildren } diff --git a/Sources/SwiftFormat/Rules/NeverUseForceTry.swift b/Sources/SwiftFormat/Rules/NeverUseForceTry.swift index 68d81f652..2be281cd1 100644 --- a/Sources/SwiftFormat/Rules/NeverUseForceTry.swift +++ b/Sources/SwiftFormat/Rules/NeverUseForceTry.swift @@ -36,6 +36,8 @@ public final class NeverUseForceTry: SyntaxLintRule { public override func visit(_ node: TryExprSyntax) -> SyntaxVisitorContinueKind { guard context.importsXCTest == .doesNotImportXCTest else { return .skipChildren } guard let mark = node.questionOrExclamationMark else { return .visitChildren } + // Allow force try if it is in a function marked with @Test attribute. + if node.hasTestAncestor { return .skipChildren } if mark.tokenKind == .exclamationMark { diagnose(.doNotForceTry, on: node.tryKeyword) } diff --git a/Sources/SwiftFormat/Rules/NeverUseImplicitlyUnwrappedOptionals.swift b/Sources/SwiftFormat/Rules/NeverUseImplicitlyUnwrappedOptionals.swift index 65635cc74..ff5fad1b5 100644 --- a/Sources/SwiftFormat/Rules/NeverUseImplicitlyUnwrappedOptionals.swift +++ b/Sources/SwiftFormat/Rules/NeverUseImplicitlyUnwrappedOptionals.swift @@ -39,6 +39,8 @@ public final class NeverUseImplicitlyUnwrappedOptionals: SyntaxLintRule { public override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind { guard context.importsXCTest == .doesNotImportXCTest else { return .skipChildren } + // Allow implicitly unwrapping if it is in a function marked with @Test attribute. + if node.hasTestAncestor { return .skipChildren } // Ignores IBOutlet variables for attribute in node.attributes { if (attribute.as(AttributeSyntax.self))?.attributeName.as(IdentifierTypeSyntax.self)?.name.text == "IBOutlet" { diff --git a/Tests/SwiftFormatTests/Rules/NeverForceUnwrapTests.swift b/Tests/SwiftFormatTests/Rules/NeverForceUnwrapTests.swift index bf68c4589..7ce22b686 100644 --- a/Tests/SwiftFormatTests/Rules/NeverForceUnwrapTests.swift +++ b/Tests/SwiftFormatTests/Rules/NeverForceUnwrapTests.swift @@ -40,4 +40,23 @@ final class NeverForceUnwrapTests: LintOrFormatRuleTestCase { findings: [] ) } + + func testIgnoreTestAttributeFunction() { + assertLint( + NeverForceUnwrap.self, + """ + @Test + func testSomeFunc() { + var b = a as! Int + } + @Test + func testAnotherFunc() { + func nestedFunc() { + let c = someValue()! + } + } + """, + findings: [] + ) + } } diff --git a/Tests/SwiftFormatTests/Rules/NeverUseForceTryTests.swift b/Tests/SwiftFormatTests/Rules/NeverUseForceTryTests.swift index 2be59ac47..0fd51be28 100644 --- a/Tests/SwiftFormatTests/Rules/NeverUseForceTryTests.swift +++ b/Tests/SwiftFormatTests/Rules/NeverUseForceTryTests.swift @@ -38,4 +38,20 @@ final class NeverUseForceTryTests: LintOrFormatRuleTestCase { findings: [] ) } + + func testAllowForceTryInTestAttributeFunction() { + assertLint( + NeverUseForceTry.self, + """ + @Test + func testSomeFunc() { + let document = try! Document(path: "important.data") + func nestedFunc() { + let x = try! someThrowingFunction() + } + } + """, + findings: [] + ) + } } diff --git a/Tests/SwiftFormatTests/Rules/NeverUseImplicitlyUnwrappedOptionalsTests.swift b/Tests/SwiftFormatTests/Rules/NeverUseImplicitlyUnwrappedOptionalsTests.swift index 6a210b826..c79ee3db0 100644 --- a/Tests/SwiftFormatTests/Rules/NeverUseImplicitlyUnwrappedOptionalsTests.swift +++ b/Tests/SwiftFormatTests/Rules/NeverUseImplicitlyUnwrappedOptionalsTests.swift @@ -35,4 +35,20 @@ final class NeverUseImplicitlyUnwrappedOptionalsTests: LintOrFormatRuleTestCase findings: [] ) } + + func testIgnoreTestAttrinuteFunction() { + assertLint( + NeverUseImplicitlyUnwrappedOptionals.self, + """ + @Test + func testSomeFunc() { + var s: String! + func nestedFunc() { + var f: Foo! + } + } + """, + findings: [] + ) + } } From 77779acc5919e0f3021b3694943c13c1460614c9 Mon Sep 17 00:00:00 2001 From: Tigran Hambardzumyan Date: Mon, 15 Jul 2024 19:06:13 +0400 Subject: [PATCH 3/6] Move attribute lookup to separate extension --- Sources/SwiftFormat/CMakeLists.txt | 1 + .../WithAttributesSyntax+Convenience.swift | 28 +++++++++++++++++++ .../Rules/AlwaysUseLowerCamelCase.swift | 5 +--- .../Rules/AlwaysUseLowerCamelCaseTests.swift | 2 +- 4 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 Sources/SwiftFormat/Core/WithAttributesSyntax+Convenience.swift diff --git a/Sources/SwiftFormat/CMakeLists.txt b/Sources/SwiftFormat/CMakeLists.txt index 7ea0c97f6..2564f90b8 100644 --- a/Sources/SwiftFormat/CMakeLists.txt +++ b/Sources/SwiftFormat/CMakeLists.txt @@ -42,6 +42,7 @@ add_library(SwiftFormat Core/SyntaxLintRule.swift Core/SyntaxProtocol+Convenience.swift Core/Trivia+Convenience.swift + Core/WithAttributesSyntax+Convenience.swift Core/WithSemicolonSyntax.swift PrettyPrint/Comment.swift PrettyPrint/Indent+Length.swift diff --git a/Sources/SwiftFormat/Core/WithAttributesSyntax+Convenience.swift b/Sources/SwiftFormat/Core/WithAttributesSyntax+Convenience.swift new file mode 100644 index 000000000..4e6c0a44b --- /dev/null +++ b/Sources/SwiftFormat/Core/WithAttributesSyntax+Convenience.swift @@ -0,0 +1,28 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import SwiftSyntax + +extension WithAttributesSyntax { + /// Indicates whether the node has attribute with the given `name`. + /// + /// - Parameter name: The name of the attribute to lookup. + /// - Returns: True if the node has an attribute with the given `name`, otherwise false. + func hasAttribute(_ name: String) -> Bool { + attributes.contains { attribute in + let attributeName = attribute.as(AttributeSyntax.self)?.attributeName + return attributeName?.as(IdentifierTypeSyntax.self)?.name.text == name + // support @Module.Attribute syntax as well + || attributeName?.as(MemberTypeSyntax.self)?.name.text == name + } + } +} diff --git a/Sources/SwiftFormat/Rules/AlwaysUseLowerCamelCase.swift b/Sources/SwiftFormat/Rules/AlwaysUseLowerCamelCase.swift index 8e11938ce..d248cb020 100644 --- a/Sources/SwiftFormat/Rules/AlwaysUseLowerCamelCase.swift +++ b/Sources/SwiftFormat/Rules/AlwaysUseLowerCamelCase.swift @@ -100,10 +100,7 @@ public final class AlwaysUseLowerCamelCase: SyntaxLintRule { // We allow underscores in test names, because there's an existing convention of using // underscores to separate phrases in very detailed test names. - let allowUnderscores = testCaseFuncs.contains(node) || node.attributes.contains { - // Allow underscore for test functions with the `@Test` attribute. - $0.as(AttributeSyntax.self)?.attributeName.as(IdentifierTypeSyntax.self)?.name.text == "Test" - } + let allowUnderscores = testCaseFuncs.contains(node) || node.hasAttribute("Test") diagnoseLowerCamelCaseViolations( node.name, allowUnderscores: allowUnderscores, diff --git a/Tests/SwiftFormatTests/Rules/AlwaysUseLowerCamelCaseTests.swift b/Tests/SwiftFormatTests/Rules/AlwaysUseLowerCamelCaseTests.swift index 750183609..89eba4765 100644 --- a/Tests/SwiftFormatTests/Rules/AlwaysUseLowerCamelCaseTests.swift +++ b/Tests/SwiftFormatTests/Rules/AlwaysUseLowerCamelCaseTests.swift @@ -217,7 +217,7 @@ final class AlwaysUseLowerCamelCaseTests: LintOrFormatRuleTestCase { """ @Test func function_With_Test_Attribute() {} - @Test("Description for test functions", + @Testing.Test("Description for test functions", .tags(.testTag)) func function_With_Test_Attribute_And_Args() {} func 1️⃣function_Without_Test_Attribute() {} From c7d80181e0c2601ea752db3ee7cd814a525b83da Mon Sep 17 00:00:00 2001 From: Tigran Hambardzumyan Date: Mon, 15 Jul 2024 19:09:16 +0400 Subject: [PATCH 4/6] Use new extension for attribute lookup --- Sources/SwiftFormat/Core/SyntaxProtocol+Convenience.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Sources/SwiftFormat/Core/SyntaxProtocol+Convenience.swift b/Sources/SwiftFormat/Core/SyntaxProtocol+Convenience.swift index 02b9a328a..f1dbf2a90 100644 --- a/Sources/SwiftFormat/Core/SyntaxProtocol+Convenience.swift +++ b/Sources/SwiftFormat/Core/SyntaxProtocol+Convenience.swift @@ -154,10 +154,7 @@ extension SyntaxProtocol { var hasTestAncestor: Bool { var parent = self.parent while let existingParent = parent { - if let functionDecl = existingParent.as(FunctionDeclSyntax.self), - functionDecl.attributes.contains(where: { - $0.as(AttributeSyntax.self)?.attributeName.as(IdentifierTypeSyntax.self)?.name.text == "Test" - }) { + if let functionDecl = existingParent.as(FunctionDeclSyntax.self), functionDecl.hasAttribute("Test") { return true } parent = existingParent.parent From 8019514fa1462b64be8272a90f34e6bf841d9b2d Mon Sep 17 00:00:00 2001 From: Tigran Hambardzumyan Date: Thu, 18 Jul 2024 22:19:14 +0400 Subject: [PATCH 5/6] Update hasAttribute extension signature to check module name as well. --- .../Core/SyntaxProtocol+Convenience.swift | 3 +- .../WithAttributesSyntax+Convenience.swift | 29 ++++++++++++------- .../Rules/AlwaysUseLowerCamelCase.swift | 2 +- .../Rules/AlwaysUseLowerCamelCaseTests.swift | 3 ++ 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/Sources/SwiftFormat/Core/SyntaxProtocol+Convenience.swift b/Sources/SwiftFormat/Core/SyntaxProtocol+Convenience.swift index f1dbf2a90..94147ef05 100644 --- a/Sources/SwiftFormat/Core/SyntaxProtocol+Convenience.swift +++ b/Sources/SwiftFormat/Core/SyntaxProtocol+Convenience.swift @@ -154,7 +154,8 @@ extension SyntaxProtocol { var hasTestAncestor: Bool { var parent = self.parent while let existingParent = parent { - if let functionDecl = existingParent.as(FunctionDeclSyntax.self), functionDecl.hasAttribute("Test") { + if let functionDecl = existingParent.as(FunctionDeclSyntax.self), + functionDecl.hasAttribute("Test", inModule: "Testing") { return true } parent = existingParent.parent diff --git a/Sources/SwiftFormat/Core/WithAttributesSyntax+Convenience.swift b/Sources/SwiftFormat/Core/WithAttributesSyntax+Convenience.swift index 4e6c0a44b..55616c6ab 100644 --- a/Sources/SwiftFormat/Core/WithAttributesSyntax+Convenience.swift +++ b/Sources/SwiftFormat/Core/WithAttributesSyntax+Convenience.swift @@ -13,16 +13,23 @@ import SwiftSyntax extension WithAttributesSyntax { - /// Indicates whether the node has attribute with the given `name`. - /// - /// - Parameter name: The name of the attribute to lookup. - /// - Returns: True if the node has an attribute with the given `name`, otherwise false. - func hasAttribute(_ name: String) -> Bool { - attributes.contains { attribute in - let attributeName = attribute.as(AttributeSyntax.self)?.attributeName - return attributeName?.as(IdentifierTypeSyntax.self)?.name.text == name - // support @Module.Attribute syntax as well - || attributeName?.as(MemberTypeSyntax.self)?.name.text == name - } + /// Indicates whether the node has attribute with the given `name`. + /// + /// - Parameter name: The name of the attribute to lookup. + /// - Parameter module: The module name to lookup the attribute in. + /// - Returns: True if the node has an attribute with the given `name`, otherwise false. + func hasAttribute(_ name: String, inModule module: String) -> Bool { + attributes.contains { attribute in + let attributeName = attribute.as(AttributeSyntax.self)?.attributeName + if let identifier = attributeName?.as(IdentifierTypeSyntax.self) { + return identifier.name.text == name + } + // support @Module.Attribute syntax as well + if let memberType = attributeName?.as(MemberTypeSyntax.self) { + return memberType.name.text == name + && memberType.baseType.as(IdentifierTypeSyntax.self)?.name.text == module + } + return false } + } } diff --git a/Sources/SwiftFormat/Rules/AlwaysUseLowerCamelCase.swift b/Sources/SwiftFormat/Rules/AlwaysUseLowerCamelCase.swift index d248cb020..368d8d69d 100644 --- a/Sources/SwiftFormat/Rules/AlwaysUseLowerCamelCase.swift +++ b/Sources/SwiftFormat/Rules/AlwaysUseLowerCamelCase.swift @@ -100,7 +100,7 @@ public final class AlwaysUseLowerCamelCase: SyntaxLintRule { // We allow underscores in test names, because there's an existing convention of using // underscores to separate phrases in very detailed test names. - let allowUnderscores = testCaseFuncs.contains(node) || node.hasAttribute("Test") + let allowUnderscores = testCaseFuncs.contains(node) || node.hasAttribute("Test", inModule: "Testing") diagnoseLowerCamelCaseViolations( node.name, allowUnderscores: allowUnderscores, diff --git a/Tests/SwiftFormatTests/Rules/AlwaysUseLowerCamelCaseTests.swift b/Tests/SwiftFormatTests/Rules/AlwaysUseLowerCamelCaseTests.swift index 89eba4765..1dbca2191 100644 --- a/Tests/SwiftFormatTests/Rules/AlwaysUseLowerCamelCaseTests.swift +++ b/Tests/SwiftFormatTests/Rules/AlwaysUseLowerCamelCaseTests.swift @@ -223,10 +223,13 @@ final class AlwaysUseLowerCamelCaseTests: LintOrFormatRuleTestCase { func 1️⃣function_Without_Test_Attribute() {} @objc func 2️⃣function_With_Non_Test_Attribute() {} + @Foo.Test + func 3️⃣function_With_Test_Attribute_From_Foo_Module() {} """, findings: [ FindingSpec("1️⃣", message: "rename the function 'function_Without_Test_Attribute' using lowerCamelCase"), FindingSpec("2️⃣", message: "rename the function 'function_With_Non_Test_Attribute' using lowerCamelCase"), + FindingSpec("3️⃣", message: "rename the function 'function_With_Test_Attribute_From_Foo_Module' using lowerCamelCase"), ] ) } From d2d5dc3c5754bc17df950d2710f6cfda09bb5074 Mon Sep 17 00:00:00 2001 From: Tigran Hambardzumyan Date: Fri, 19 Jul 2024 01:23:26 +0400 Subject: [PATCH 6/6] updating the doc for hasAttribute --- .../SwiftFormat/Core/WithAttributesSyntax+Convenience.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/SwiftFormat/Core/WithAttributesSyntax+Convenience.swift b/Sources/SwiftFormat/Core/WithAttributesSyntax+Convenience.swift index 55616c6ab..f5938d01e 100644 --- a/Sources/SwiftFormat/Core/WithAttributesSyntax+Convenience.swift +++ b/Sources/SwiftFormat/Core/WithAttributesSyntax+Convenience.swift @@ -13,7 +13,8 @@ import SwiftSyntax extension WithAttributesSyntax { - /// Indicates whether the node has attribute with the given `name`. + /// Indicates whether the node has attribute with the given `name` and `module`. + /// The `module` is only considered if the attribute is written as `@Module.Attribute`. /// /// - Parameter name: The name of the attribute to lookup. /// - Parameter module: The module name to lookup the attribute in. @@ -22,10 +23,11 @@ extension WithAttributesSyntax { attributes.contains { attribute in let attributeName = attribute.as(AttributeSyntax.self)?.attributeName if let identifier = attributeName?.as(IdentifierTypeSyntax.self) { + // @Attribute syntax return identifier.name.text == name } - // support @Module.Attribute syntax as well if let memberType = attributeName?.as(MemberTypeSyntax.self) { + // @Module.Attribute syntax return memberType.name.text == name && memberType.baseType.as(IdentifierTypeSyntax.self)?.name.text == module }