diff --git a/Tests/MacroTestingTests/AddAsyncCompletionHandlerTests.swift b/Tests/MacroTestingTests/AddAsyncCompletionHandlerTests.swift deleted file mode 100644 index 868aa4e..0000000 --- a/Tests/MacroTestingTests/AddAsyncCompletionHandlerTests.swift +++ /dev/null @@ -1,134 +0,0 @@ -import MacroTesting -import XCTest - -final class AddAsyncCompletionHandlerMacroTests: BaseTestCase { - override func invokeTest() { - withMacroTesting(macros: [AddAsyncMacro.self, AddCompletionHandlerMacro.self]) { - super.invokeTest() - } - } - - func testAddAsyncCompletionHandler() { - assertMacro { - #""" - struct MyStruct { - @AddCompletionHandler - func f(a: Int, for b: String, _ value: Double) async -> String { - return b - } - - @AddAsync - func c(a: Int, for b: String, _ value: Double, completionBlock: @escaping (Result) -> Void) -> Void { - completionBlock(.success("a: \(a), b: \(b), value: \(value)")) - } - - @AddAsync - func d(a: Int, for b: String, _ value: Double, completionBlock: @escaping (Bool) -> Void) -> Void { - completionBlock(true) - } - } - """# - } expansion: { - #""" - struct MyStruct { - func f(a: Int, for b: String, _ value: Double) async -> String { - return b - } - - func f(a: Int, for b: String, _ value: Double, completionHandler: @escaping (String) -> Void) { - Task { - completionHandler(await f(a: a, for: b, value)) - } - } - func c(a: Int, for b: String, _ value: Double, completionBlock: @escaping (Result) -> Void) -> Void { - completionBlock(.success("a: \(a), b: \(b), value: \(value)")) - } - - func c(a: Int, for b: String, _ value: Double) async throws -> String { - try await withCheckedThrowingContinuation { continuation in - c(a: a, for: b, value) { returnValue in - - switch returnValue { - case .success(let value): - continuation.resume(returning: value) - case .failure(let error): - continuation.resume(throwing: error) - } - - } - } - } - func d(a: Int, for b: String, _ value: Double, completionBlock: @escaping (Bool) -> Void) -> Void { - completionBlock(true) - } - - func d(a: Int, for b: String, _ value: Double) async -> Bool { - await withCheckedContinuation { continuation in - d(a: a, for: b, value) { returnValue in - - continuation.resume(returning: returnValue) - - } - } - } - } - """# - } - } - - func testNonFunctionDiagnostic() { - assertMacro { - """ - @AddCompletionHandler - struct Foo {} - """ - } diagnostics: { - """ - @AddCompletionHandler - ┬──────────────────── - ╰─ 🛑 @addCompletionHandler only works on functions - struct Foo {} - """ - } - } - - func testNonAsyncFunctionDiagnostic() { - assertMacro { - """ - @AddCompletionHandler - func f(a: Int, for b: String, _ value: Double) -> String { - return b - } - """ - } diagnostics: { - """ - @AddCompletionHandler - func f(a: Int, for b: String, _ value: Double) -> String { - ┬─── - ╰─ 🛑 can only add a completion-handler variant to an 'async' function - ✏️ add 'async' - return b - } - """ - } fixes: { - """ - @AddCompletionHandler - func f(a: Int, for b: String, _ value: Double) async -> String { - return b - } - """ - } expansion: { - """ - func f(a: Int, for b: String, _ value: Double) async -> String { - return b - } - - func f(a: Int, for b: String, _ value: Double, completionHandler: @escaping (String) -> Void) { - Task { - completionHandler(await f(a: a, for: b, value)) - } - } - """ - } - } -} diff --git a/Tests/MacroTestingTests/AddAsyncTests.swift b/Tests/MacroTestingTests/AddAsyncTests.swift new file mode 100644 index 0000000..e2d54f1 --- /dev/null +++ b/Tests/MacroTestingTests/AddAsyncTests.swift @@ -0,0 +1,109 @@ +import MacroTesting +import XCTest + +final class AddAsyncMacroTests: BaseTestCase { + override func invokeTest() { + withMacroTesting(macros: [AddAsyncMacro.self]) { + super.invokeTest() + } + } + + func testExpansionTransformsFunctionWithResultCompletionToAsyncThrows() { + assertMacro { + #""" + @AddAsync + func c(a: Int, for b: String, _ value: Double, completionBlock: @escaping (Result) -> Void) -> Void { + completionBlock(.success("a: \(a), b: \(b), value: \(value)")) + } + """# + } expansion: { + #""" + func c(a: Int, for b: String, _ value: Double, completionBlock: @escaping (Result) -> Void) -> Void { + completionBlock(.success("a: \(a), b: \(b), value: \(value)")) + } + + func c(a: Int, for b: String, _ value: Double) async throws -> String { + try await withCheckedThrowingContinuation { continuation in + c(a: a, for: b, value) { returnValue in + + switch returnValue { + case .success(let value): + continuation.resume(returning: value) + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } + """# + } + } + + func testExpansionTransformsFunctionWithBoolCompletionToAsync() { + assertMacro { + """ + @AddAsync + func d(a: Int, for b: String, _ value: Double, completionBlock: @escaping (Bool) -> Void) -> Void { + completionBlock(true) + } + """ + } expansion: { + """ + func d(a: Int, for b: String, _ value: Double, completionBlock: @escaping (Bool) -> Void) -> Void { + completionBlock(true) + } + + func d(a: Int, for b: String, _ value: Double) async -> Bool { + await withCheckedContinuation { continuation in + d(a: a, for: b, value) { returnValue in + + continuation.resume(returning: returnValue) + } + } + } + """ + } + } + + func testExpansionOnStoredPropertyEmitsError() { + assertMacro { + """ + struct Test { + @AddAsync + var name: String + } + """ + } diagnostics: { + """ + struct Test { + @AddAsync + ┬──────── + ╰─ 🛑 @addAsync only works on functions + var name: String + } + """ + } + } + + func testExpansionOnAsyncFunctionEmitsError() { + assertMacro { + """ + struct Test { + @AddAsync + async func sayHello() { + } + } + """ + } diagnostics: { + """ + struct Test { + @AddAsync + ┬──────── + ╰─ 🛑 @addAsync requires an function that returns void + async func sayHello() { + } + } + """ + } + } +} diff --git a/Tests/MacroTestingTests/AddBlockerTests.swift b/Tests/MacroTestingTests/AddBlockerTests.swift index 69b9dbc..3193c65 100644 --- a/Tests/MacroTestingTests/AddBlockerTests.swift +++ b/Tests/MacroTestingTests/AddBlockerTests.swift @@ -8,19 +8,13 @@ final class AddBlockerTests: BaseTestCase { } } - func testAddBlocker() { + func testExpansionTransformsAdditionToSubtractionAndEmitsWarning() { assertMacro { """ - let x = 1 - let y = 2 - let z = 3 #addBlocker(x * y + z) """ } diagnostics: { """ - let x = 1 - let y = 2 - let z = 3 #addBlocker(x * y + z) ───── ┬ ─ ╰─ ⚠️ blocked an add; did you mean to subtract? @@ -28,40 +22,23 @@ final class AddBlockerTests: BaseTestCase { """ } fixes: { """ - let x = 1 - let y = 2 - let z = 3 #addBlocker(x * y - z) """ } expansion: { """ - let x = 1 - let y = 2 - let z = 3 x * y - z """ } } - func testAddBlocker_Inline() { + func testExpansionPreservesSubtraction() { assertMacro { """ - #addBlocker(1 * 2 + 3) - """ - } diagnostics: { - """ - #addBlocker(1 * 2 + 3) - ───── ┬ ─ - ╰─ ⚠️ blocked an add; did you mean to subtract? - ✏️ use '-' - """ - } fixes: { - """ - #addBlocker(1 * 2 - 3) + #addBlocker(x * y - z) """ } expansion: { """ - 1 * 2 - 3 + x * y - z """ } } diff --git a/Tests/MacroTestingTests/AddCompletionHandlerTests.swift b/Tests/MacroTestingTests/AddCompletionHandlerTests.swift new file mode 100644 index 0000000..f577d9c --- /dev/null +++ b/Tests/MacroTestingTests/AddCompletionHandlerTests.swift @@ -0,0 +1,101 @@ +import MacroTesting +import XCTest + +final class AddCompletionHandlerTests: BaseTestCase { + override func invokeTest() { + withMacroTesting(macros: [AddCompletionHandlerMacro.self]) { + super.invokeTest() + } + } + + func testExpansionTransformsAsyncFunctionToCompletion() { + assertMacro { + """ + @AddCompletionHandler + func f(a: Int, for b: String, _ value: Double) async -> String { + return b + } + """ + } expansion: { + """ + func f(a: Int, for b: String, _ value: Double) async -> String { + return b + } + + func f(a: Int, for b: String, _ value: Double, completionHandler: @escaping (String) -> Void) { + Task { + completionHandler(await f(a: a, for: b, value)) + } + } + """ + } + } + + func testExpansionOnStoredPropertyEmitsError() { + assertMacro { + """ + struct Test { + @AddCompletionHandler + var value: Int + } + """ + } diagnostics: { + """ + struct Test { + @AddCompletionHandler + ┬──────────────────── + ╰─ 🛑 @addCompletionHandler only works on functions + var value: Int + } + """ + } + } + + func testExpansionOnNonAsyncFunctionEmitsErrorWithFixItSuggestion() { + assertMacro { + """ + struct Test { + @AddCompletionHandler + func fetchData() -> String { + return "Hello, World!" + } + } + """ + } diagnostics: { + """ + struct Test { + @AddCompletionHandler + func fetchData() -> String { + ┬─── + ╰─ 🛑 can only add a completion-handler variant to an 'async' function + ✏️ add 'async' + return "Hello, World!" + } + } + """ + } fixes: { + """ + struct Test { + @AddCompletionHandler + func fetchData() async-> String { + return "Hello, World!" + } + } + """ + } expansion: { + """ + struct Test { + func fetchData() async-> String { + return "Hello, World!" + } + + func fetchData(completionHandler: @escaping (String) -> Void) { + Task { + completionHandler(await fetchData()) + } + } + } + """ + } + } +} diff --git a/Tests/MacroTestingTests/CaseDetectionMacroTests.swift b/Tests/MacroTestingTests/CaseDetectionMacroTests.swift index ffd0747..79190b2 100644 --- a/Tests/MacroTestingTests/CaseDetectionMacroTests.swift +++ b/Tests/MacroTestingTests/CaseDetectionMacroTests.swift @@ -8,24 +8,20 @@ final class CaseDetectionMacroTests: BaseTestCase { } } - func testCaseDetection() { + func testExpansionAddsComputedProperties() { assertMacro { - #""" + """ @CaseDetection - enum Pet { + enum Animal { case dog case cat(curious: Bool) - case parrot - case snake } - """# + """ } expansion: { """ - enum Pet { + enum Animal { case dog case cat(curious: Bool) - case parrot - case snake var isDog: Bool { if case .dog = self { @@ -42,22 +38,6 @@ final class CaseDetectionMacroTests: BaseTestCase { return false } - - var isParrot: Bool { - if case .parrot = self { - return true - } - - return false - } - - var isSnake: Bool { - if case .snake = self { - return true - } - - return false - } } """ } diff --git a/Tests/MacroTestingTests/CustomCodableMacroTests.swift b/Tests/MacroTestingTests/CustomCodableMacroTests.swift index c1dc24d..fe83f8b 100644 --- a/Tests/MacroTestingTests/CustomCodableMacroTests.swift +++ b/Tests/MacroTestingTests/CustomCodableMacroTests.swift @@ -8,27 +8,52 @@ final class CustomCodableMacroTests: BaseTestCase { } } - func testCustomCodable() { + func testExpansionAddsDefaultCodingKeys() { assertMacro { """ @CustomCodable - struct CustomCodableString: Codable { - @CodableKey(name: "OtherName") - var propertyWithOtherName: String - var propertyWithSameName: Bool + struct Person { + let name: String + let age: Int + } + """ + } expansion: { + """ + struct Person { + let name: String + let age: Int + + enum CodingKeys: String, CodingKey { + case name + case age + } + } + """ + } + } + + func testExpansionWithCodableKeyAddsCustomCodingKeys() { + assertMacro { + """ + @CustomCodable + struct Person { + let name: String + @CodableKey("user_age") let age: Int + func randomFunction() {} } """ } expansion: { """ - struct CustomCodableString: Codable { - var propertyWithOtherName: String - var propertyWithSameName: Bool + struct Person { + let name: String + let age: Int + func randomFunction() {} enum CodingKeys: String, CodingKey { - case propertyWithOtherName = "OtherName" - case propertyWithSameName + case name + case age = "user_age" } } """ diff --git a/Tests/MacroTestingTests/DictionaryStorageMacroTests.swift b/Tests/MacroTestingTests/DictionaryStorageMacroTests.swift index a70af05..2483154 100644 --- a/Tests/MacroTestingTests/DictionaryStorageMacroTests.swift +++ b/Tests/MacroTestingTests/DictionaryStorageMacroTests.swift @@ -3,12 +3,17 @@ import XCTest final class DictionaryStorageMacroTests: BaseTestCase { override func invokeTest() { - withMacroTesting(macros: [DictionaryStorageMacro.self]) { + withMacroTesting( + macros: [ + DictionaryStorageMacro.self, + DictionaryStoragePropertyMacro.self, + ] + ) { super.invokeTest() } } - func testDictionaryStorage() { + func testExpansionConvertsStoredProperties() { assertMacro { """ @DictionaryStorage @@ -64,4 +69,29 @@ final class DictionaryStorageMacroTests: BaseTestCase { """ } } + + func testExpansionIgnoresComputedProperties() { + assertMacro { + """ + @DictionaryStorage + struct Test { + var value: Int { + get { return 0 } + set {} + } + } + """ + } expansion: { + """ + struct Test { + var value: Int { + get { return 0 } + set {} + } + + var _storage: [String: Any] = [:] + } + """ + } + } } diff --git a/Tests/MacroTestingTests/EquatableExtensionMacroTests.swift b/Tests/MacroTestingTests/EquatableExtensionMacroTests.swift new file mode 100644 index 0000000..ee87d79 --- /dev/null +++ b/Tests/MacroTestingTests/EquatableExtensionMacroTests.swift @@ -0,0 +1,32 @@ +import MacroTesting +import XCTest + +final class EquatableExtensionMacroTests: XCTestCase { + override func invokeTest() { + withMacroTesting(macros: ["equatable": EquatableExtensionMacro.self]) { + super.invokeTest() + } + } + + func testExpansionAddsExtensionWithEquatableConformance() { + assertMacro { + """ + @equatable + final public class Message { + let text: String + let sender: String + } + """ + } expansion: { + """ + final public class Message { + let text: String + let sender: String + } + + extension Message: Equatable { + } + """ + } + } +} diff --git a/Tests/MacroTestingTests/FontLiteralMacroTests.swift b/Tests/MacroTestingTests/FontLiteralMacroTests.swift index 9712cb3..53739f3 100644 --- a/Tests/MacroTestingTests/FontLiteralMacroTests.swift +++ b/Tests/MacroTestingTests/FontLiteralMacroTests.swift @@ -8,24 +8,26 @@ final class FontLiteralMacroTests: BaseTestCase { } } - func testFontLiteral() { + func testExpansionWithNamedArguments() { assertMacro { """ - struct Font: ExpressibleByFontLiteral { - init(fontLiteralName: String, size: Int, weight: MacroExamplesLib.FontWeight) { - } - } - - let _: Font = #fontLiteral(name: "Comic Sans", size: 14, weight: .thin) + #fontLiteral(name: "Comic Sans", size: 14, weight: .thin) """ } expansion: { """ - struct Font: ExpressibleByFontLiteral { - init(fontLiteralName: String, size: Int, weight: MacroExamplesLib.FontWeight) { - } - } + .init(fontLiteralName: "Comic Sans", size: 14, weight: .thin) + """ + } + } - let _: Font = .init(fontLiteralName: "Comic Sans", size: 14, weight: .thin) + func testExpansionWithUnlabeledFirstArgument() { + assertMacro { + """ + #fontLiteral("Copperplate Gothic", size: 69, weight: .bold) + """ + } expansion: { + """ + .init(fontLiteralName: "Copperplate Gothic", size: 69, weight: .bold) """ } } diff --git a/Tests/MacroTestingTests/FuncUniqueMacroTests.swift b/Tests/MacroTestingTests/FuncUniqueMacroTests.swift new file mode 100644 index 0000000..ea1a71d --- /dev/null +++ b/Tests/MacroTestingTests/FuncUniqueMacroTests.swift @@ -0,0 +1,27 @@ +import MacroTesting +import XCTest + +final class FuncUniqueMacroTests: BaseTestCase { + override func invokeTest() { + withMacroTesting( + macros: [FuncUniqueMacro.self,] + ) { + super.invokeTest() + } + } + + func testExpansionCreatesDeclarationWithUniqueFunction() { + assertMacro { + """ + #FuncUnique() + """ + } expansion: { + """ + class MyClass { + func __macro_local_6uniquefMu_() { + } + } + """ + } + } +} diff --git a/Tests/MacroTestingTests/MacroExamples/AddAsyncMacro.swift b/Tests/MacroTestingTests/MacroExamples/AddAsyncMacro.swift index 9675b5c..da9faa6 100644 --- a/Tests/MacroTestingTests/MacroExamples/AddAsyncMacro.swift +++ b/Tests/MacroTestingTests/MacroExamples/AddAsyncMacro.swift @@ -1,3 +1,15 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 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 import SwiftSyntaxMacros @@ -30,20 +42,15 @@ public struct AddAsyncMacro: PeerMacro { } // This only makes sense void functions - if funcDecl.signature.returnClause?.type.with(\.leadingTrivia, []).with(\.trailingTrivia, []) - .description != "Void" - { + if funcDecl.signature.returnClause?.type.with(\.leadingTrivia, []).with(\.trailingTrivia, []).description != "Void" { throw CustomError.message( "@addAsync requires an function that returns void" ) } // Requires a completion handler block as last parameter - guard - let completionHandlerParameterAttribute = funcDecl.signature.parameterClause.parameters.last? - .type.as(AttributedTypeSyntax.self), - let completionHandlerParameter = completionHandlerParameterAttribute.baseType.as( - FunctionTypeSyntax.self) + guard let completionHandlerParameterAttribute = funcDecl.signature.parameterClause.parameters.last?.type.as(AttributedTypeSyntax.self), + let completionHandlerParameter = completionHandlerParameterAttribute.baseType.as(FunctionTypeSyntax.self) else { throw CustomError.message( "@addAsync requires an function that has a completion handler as last parameter" @@ -51,9 +58,7 @@ public struct AddAsyncMacro: PeerMacro { } // Completion handler needs to return Void - if completionHandlerParameter.returnClause.type.with(\.leadingTrivia, []).with( - \.trailingTrivia, [] - ).description != "Void" { + if completionHandlerParameter.returnClause.type.with(\.leadingTrivia, []).with(\.trailingTrivia, []).description != "Void" { throw CustomError.message( "@addAsync requires an function that has a completion handler that returns Void" ) @@ -62,18 +67,14 @@ public struct AddAsyncMacro: PeerMacro { let returnType = completionHandlerParameter.parameters.first?.type let isResultReturn = returnType?.children(viewMode: .all).first?.description == "Result" - let successReturnType = - isResultReturn - ? returnType!.as(IdentifierTypeSyntax.self)!.genericArgumentClause?.arguments.first!.argument - : returnType + let successReturnType = isResultReturn ? returnType!.as(IdentifierTypeSyntax.self)!.genericArgumentClause?.arguments.first!.argument : returnType // Remove completionHandler and comma from the previous parameter var newParameterList = funcDecl.signature.parameterClause.parameters newParameterList.removeLast() let newParameterListLastParameter = newParameterList.last! newParameterList.removeLast() - newParameterList.append( - newParameterListLastParameter.with(\.trailingTrivia, []).with(\.trailingComma, nil)) + newParameterList.append(newParameterListLastParameter.with(\.trailingTrivia, []).with(\.trailingComma, nil)) // Drop the @addAsync attribute from the new declaration. let newAttributeList = funcDecl.attributes.filter { @@ -100,22 +101,21 @@ public struct AddAsyncMacro: PeerMacro { let switchBody: ExprSyntax = """ - switch returnValue { - case .success(let value): - continuation.resume(returning: value) - case .failure(let error): - continuation.resume(throwing: error) - } + switch returnValue { + case .success(let value): + continuation.resume(returning: value) + case .failure(let error): + continuation.resume(throwing: error) + } """ let newBody: ExprSyntax = """ \(raw: isResultReturn ? "try await withCheckedThrowingContinuation { continuation in" : "await withCheckedContinuation { continuation in") - \(funcDecl.name)(\(raw: callArguments.joined(separator: ", "))) { \(raw: returnType != nil ? "returnValue in" : "") - - \(isResultReturn ? switchBody : "continuation.resume(returning: \(raw: returnType != nil ? "returnValue" : "()"))") + \(raw: funcDecl.name)(\(raw: callArguments.joined(separator: ", "))) { \(raw: returnType != nil ? "returnValue in" : "") + \(raw: isResultReturn ? switchBody : "continuation.resume(returning: \(raw: returnType != nil ? "returnValue" : "()"))") } } @@ -129,14 +129,14 @@ public struct AddAsyncMacro: PeerMacro { .with( \.effectSpecifiers, FunctionEffectSpecifiersSyntax( - leadingTrivia: .space, asyncSpecifier: "async", - throwsSpecifier: isResultReturn ? " throws" : nil) // add async + leadingTrivia: .space, + asyncSpecifier: .keyword(.async), + throwsSpecifier: isResultReturn ? .keyword(.throws) : nil + ) // add async ) .with( \.returnClause, - successReturnType != nil - ? ReturnClauseSyntax( - leadingTrivia: .space, type: successReturnType!.with(\.leadingTrivia, .space)) : nil + successReturnType != nil ? ReturnClauseSyntax(leadingTrivia: .space, type: successReturnType!.with(\.leadingTrivia, .space)) : nil ) // add result type .with( \.parameterClause, diff --git a/Tests/MacroTestingTests/MacroExamples/AddBlocker.swift b/Tests/MacroTestingTests/MacroExamples/AddBlocker.swift index 8f381f7..b0db043 100644 --- a/Tests/MacroTestingTests/MacroExamples/AddBlocker.swift +++ b/Tests/MacroTestingTests/MacroExamples/AddBlocker.swift @@ -1,3 +1,15 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 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 SwiftDiagnostics import SwiftOperators import SwiftSyntax @@ -83,7 +95,7 @@ public struct AddBlocker: ExpressionMacro { in context: some MacroExpansionContext ) throws -> ExprSyntax { let visitor = AddVisitor() - let result = visitor.rewrite(node) + let result = visitor.rewrite(Syntax(node)) for diag in visitor.diagnostics { context.diagnose(diag) diff --git a/Tests/MacroTestingTests/MacroExamples/AddCompletionHandlerMacro.swift b/Tests/MacroTestingTests/MacroExamples/AddCompletionHandlerMacro.swift index fd0cd51..3f1d833 100644 --- a/Tests/MacroTestingTests/MacroExamples/AddCompletionHandlerMacro.swift +++ b/Tests/MacroTestingTests/MacroExamples/AddCompletionHandlerMacro.swift @@ -1,3 +1,15 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 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 SwiftDiagnostics import SwiftSyntax import SwiftSyntaxMacros @@ -21,9 +33,9 @@ public struct AddCompletionHandlerMacro: PeerMacro { if funcDecl.signature.effectSpecifiers?.asyncSpecifier == nil { let newEffects: FunctionEffectSpecifiersSyntax if let existingEffects = funcDecl.signature.effectSpecifiers { - newEffects = existingEffects.with(\.asyncSpecifier, "async ") + newEffects = existingEffects.with(\.asyncSpecifier, .keyword(.async)) } else { - newEffects = FunctionEffectSpecifiersSyntax(asyncSpecifier: "async ") + newEffects = FunctionEffectSpecifiersSyntax(asyncSpecifier: .keyword(.async)) } let newSignature = funcDecl.signature.with(\.effectSpecifiers, newEffects) @@ -39,6 +51,7 @@ public struct AddCompletionHandlerMacro: PeerMacro { severity: .error ), fixIts: [ + // Fix-It to replace the '+' with a '-'. FixIt( message: SimpleDiagnosticMessage( message: "add 'async'", @@ -60,8 +73,7 @@ public struct AddCompletionHandlerMacro: PeerMacro { } // Form the completion handler parameter. - let resultType: TypeSyntax? = funcDecl.signature.returnClause?.type.with(\.leadingTrivia, []) - .with(\.trailingTrivia, []) + let resultType: TypeSyntax? = funcDecl.signature.returnClause?.type.with(\.leadingTrivia, []).with(\.trailingTrivia, []) let completionHandlerParam = FunctionParameterSyntax( diff --git a/Tests/MacroTestingTests/MacroExamples/CaseDetectionMacro.swift b/Tests/MacroTestingTests/MacroExamples/CaseDetectionMacro.swift index 2f4ae37..4e9e184 100644 --- a/Tests/MacroTestingTests/MacroExamples/CaseDetectionMacro.swift +++ b/Tests/MacroTestingTests/MacroExamples/CaseDetectionMacro.swift @@ -1,24 +1,23 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 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 import SwiftSyntaxMacros -extension TokenSyntax { - fileprivate var initialUppercased: String { - let name = self.text - guard let initial = name.first else { - return name - } - - return "\(initial.uppercased())\(name.dropFirst())" - } -} - -public struct CaseDetectionMacro: MemberMacro { - public static func expansion< - Declaration: DeclGroupSyntax, Context: MacroExpansionContext - >( +public enum CaseDetectionMacro: MemberMacro { + public static func expansion( of node: AttributeSyntax, - providingMembersOf declaration: Declaration, - in context: Context + providingMembersOf declaration: some DeclGroupSyntax, + in context: some MacroExpansionContext ) throws -> [DeclSyntax] { declaration.memberBlock.members .compactMap { $0.decl.as(EnumCaseDeclSyntax.self) } @@ -37,3 +36,14 @@ public struct CaseDetectionMacro: MemberMacro { } } } + +extension TokenSyntax { + fileprivate var initialUppercased: String { + let name = self.text + guard let initial = name.first else { + return name + } + + return "\(initial.uppercased())\(name.dropFirst())" + } +} diff --git a/Tests/MacroTestingTests/MacroExamples/CodableKey.swift b/Tests/MacroTestingTests/MacroExamples/CodableKey.swift deleted file mode 100644 index 9f0081f..0000000 --- a/Tests/MacroTestingTests/MacroExamples/CodableKey.swift +++ /dev/null @@ -1,14 +0,0 @@ -import SwiftSyntax -import SwiftSyntaxMacros - -public struct CodableKey: MemberMacro { - public static func expansion( - of node: AttributeSyntax, - providingMembersOf declaration: some DeclGroupSyntax, - in context: some MacroExpansionContext - ) throws -> [DeclSyntax] { - // Does nothing, used only to decorate members with data - return [] - } - -} diff --git a/Tests/MacroTestingTests/MacroExamples/CustomCodable.swift b/Tests/MacroTestingTests/MacroExamples/CustomCodable.swift index 2b8c737..1b4fffe 100644 --- a/Tests/MacroTestingTests/MacroExamples/CustomCodable.swift +++ b/Tests/MacroTestingTests/MacroExamples/CustomCodable.swift @@ -1,35 +1,41 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 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 import SwiftSyntaxMacros -public struct CustomCodable: MemberMacro { - +public enum CustomCodable: MemberMacro { public static func expansion( of node: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, in context: some MacroExpansionContext ) throws -> [DeclSyntax] { - let memberList = declaration.memberBlock.members let cases = memberList.compactMap({ member -> String? in // is a property guard - let propertyName = member.decl.as(VariableDeclSyntax.self)?.bindings.first?.pattern.as( - IdentifierPatternSyntax.self)?.identifier.text + let propertyName = member.decl.as(VariableDeclSyntax.self)?.bindings.first?.pattern.as(IdentifierPatternSyntax.self)?.identifier.text else { return nil } // if it has a CodableKey macro on it - if let customKeyMacro = member.decl.as(VariableDeclSyntax.self)?.attributes.first(where: { - element in - element.as(AttributeSyntax.self)?.attributeName.as(IdentifierTypeSyntax.self)?.description - == "CodableKey" + if let customKeyMacro = member.decl.as(VariableDeclSyntax.self)?.attributes.first(where: { element in + element.as(AttributeSyntax.self)?.attributeName.as(IdentifierTypeSyntax.self)?.description == "CodableKey" }) { // Uses the value in the Macro - let customKeyValue = customKeyMacro.as(AttributeSyntax.self)!.arguments!.as( - LabeledExprListSyntax.self)!.first!.expression + let customKeyValue = customKeyMacro.as(AttributeSyntax.self)!.arguments!.as(LabeledExprListSyntax.self)!.first!.expression return "case \(propertyName) = \(customKeyValue)" } else { @@ -39,12 +45,21 @@ public struct CustomCodable: MemberMacro { let codingKeys: DeclSyntax = """ enum CodingKeys: String, CodingKey { - \(raw: cases.joined(separator: "\n")) + \(raw: cases.joined(separator: "\n")) } """ - return [ - codingKeys - ] + return [codingKeys] + } +} + +public struct CodableKey: PeerMacro { + public static func expansion( + of node: AttributeSyntax, + providingPeersOf declaration: some DeclSyntaxProtocol, + in context: some MacroExpansionContext + ) throws -> [DeclSyntax] { + // Does nothing, used only to decorate members with data + return [] } } diff --git a/Tests/MacroTestingTests/MacroExamples/Diagnostics.swift b/Tests/MacroTestingTests/MacroExamples/Diagnostics.swift index 1959b49..83a08df 100644 --- a/Tests/MacroTestingTests/MacroExamples/Diagnostics.swift +++ b/Tests/MacroTestingTests/MacroExamples/Diagnostics.swift @@ -1,3 +1,15 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 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 SwiftDiagnostics import SwiftSyntax diff --git a/Tests/MacroTestingTests/MacroExamples/DictionaryIndirectionMacro.swift b/Tests/MacroTestingTests/MacroExamples/DictionaryIndirectionMacro.swift index 3a5cf4c..23d7ce4 100644 --- a/Tests/MacroTestingTests/MacroExamples/DictionaryIndirectionMacro.swift +++ b/Tests/MacroTestingTests/MacroExamples/DictionaryIndirectionMacro.swift @@ -1,9 +1,58 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 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 import SwiftSyntaxMacros public struct DictionaryStorageMacro {} -extension DictionaryStorageMacro: AccessorMacro { +extension DictionaryStorageMacro: MemberMacro { + public static func expansion( + of node: AttributeSyntax, + providingMembersOf declaration: some DeclGroupSyntax, + in context: some MacroExpansionContext + ) throws -> [DeclSyntax] { + let storage: DeclSyntax = "var _storage: [String: Any] = [:]" + return [ + storage.with(\.leadingTrivia, [.newlines(1), .spaces(2)]) + ] + } +} + +extension DictionaryStorageMacro: MemberAttributeMacro { + public static func expansion( + of node: AttributeSyntax, + attachedTo declaration: some DeclGroupSyntax, + providingAttributesFor member: some DeclSyntaxProtocol, + in context: some MacroExpansionContext + ) throws -> [AttributeSyntax] { + guard let property = member.as(VariableDeclSyntax.self), + property.isStoredProperty + else { + return [] + } + + return [ + AttributeSyntax( + attributeName: IdentifierTypeSyntax( + name: .identifier("DictionaryStorageProperty") + ) + ) + .with(\.leadingTrivia, [.newlines(1), .spaces(2)]) + ] + } +} + +public struct DictionaryStoragePropertyMacro: AccessorMacro { public static func expansion< Context: MacroExpansionContext, Declaration: DeclSyntaxProtocol @@ -33,50 +82,14 @@ extension DictionaryStorageMacro: AccessorMacro { return [ """ get { - _storage[\(literal: identifier.text), default: \(defaultValue)] as! \(type) + _storage[\(literal: identifier.text), default: \(defaultValue)] as! \(type) } """, """ set { - _storage[\(literal: identifier.text)] = newValue + _storage[\(literal: identifier.text)] = newValue } """, ] } } - -extension DictionaryStorageMacro: MemberMacro { - public static func expansion( - of node: AttributeSyntax, - providingMembersOf declaration: some DeclGroupSyntax, - in context: some MacroExpansionContext - ) throws -> [DeclSyntax] { - let storage: DeclSyntax = "var _storage: [String: Any] = [:]" - return [ - storage.with(\.leadingTrivia, [.newlines(1), .spaces(2)]) - ] - } -} - -extension DictionaryStorageMacro: MemberAttributeMacro { - public static func expansion( - of node: AttributeSyntax, attachedTo declaration: some DeclGroupSyntax, - providingAttributesFor member: some DeclSyntaxProtocol, - in context: some MacroExpansionContext - ) throws -> [AttributeSyntax] { - guard let property = member.as(VariableDeclSyntax.self), - property.isStoredProperty - else { - return [] - } - - return [ - AttributeSyntax( - attributeName: IdentifierTypeSyntax( - name: .identifier("DictionaryStorage") - ) - ) - .with(\.leadingTrivia, [.newlines(1), .spaces(2)]) - ] - } -} diff --git a/Tests/MacroTestingTests/MacroExamples/EquatableExtensionMacro.swift b/Tests/MacroTestingTests/MacroExamples/EquatableExtensionMacro.swift new file mode 100644 index 0000000..f13cf98 --- /dev/null +++ b/Tests/MacroTestingTests/MacroExamples/EquatableExtensionMacro.swift @@ -0,0 +1,28 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 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 +import SwiftSyntaxMacros + +public enum EquatableExtensionMacro: ExtensionMacro { + public static func expansion( + of node: AttributeSyntax, + attachedTo declaration: some DeclGroupSyntax, + providingExtensionsOf type: some TypeSyntaxProtocol, + conformingTo protocols: [TypeSyntax], + in context: some MacroExpansionContext + ) throws -> [ExtensionDeclSyntax] { + let equatableExtension = try ExtensionDeclSyntax("extension \(type.trimmed): Equatable {}") + + return [equatableExtension] + } +} diff --git a/Tests/MacroTestingTests/MacroExamples/FontLiteralMacro.swift b/Tests/MacroTestingTests/MacroExamples/FontLiteralMacro.swift index 7039f64..97aedda 100644 --- a/Tests/MacroTestingTests/MacroExamples/FontLiteralMacro.swift +++ b/Tests/MacroTestingTests/MacroExamples/FontLiteralMacro.swift @@ -1,16 +1,28 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 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 import SwiftSyntaxMacros /// Implementation of the `#fontLiteral` macro, which is similar in spirit /// to the built-in expressions `#colorLiteral`, `#imageLiteral`, etc., but in /// a small macro. -public struct FontLiteralMacro: ExpressionMacro { +public enum FontLiteralMacro: ExpressionMacro { public static func expansion( - of macro: some FreestandingMacroExpansionSyntax, + of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext - ) -> ExprSyntax { + ) throws -> ExprSyntax { let argList = replaceFirstLabel( - of: macro.argumentList, + of: node.argumentList, with: "fontLiteralName" ) return ".init(\(argList))" @@ -28,6 +40,9 @@ private func replaceFirstLabel( } var tuple = tuple - tuple[tuple.startIndex] = firstElement.with(\.label, .identifier(newLabel)) + tuple[tuple.startIndex] = + firstElement + .with(\.label, .identifier(newLabel)) + .with(\.colon, .colonToken()) return tuple } diff --git a/Tests/MacroTestingTests/MacroExamples/FuncUniqueMacro.swift b/Tests/MacroTestingTests/MacroExamples/FuncUniqueMacro.swift new file mode 100644 index 0000000..9f495c9 --- /dev/null +++ b/Tests/MacroTestingTests/MacroExamples/FuncUniqueMacro.swift @@ -0,0 +1,32 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 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 +import SwiftSyntaxBuilder +import SwiftSyntaxMacros + +/// Func With unique name. +public enum FuncUniqueMacro: DeclarationMacro { + public static func expansion( + of node: some FreestandingMacroExpansionSyntax, + in context: some MacroExpansionContext + ) throws -> [DeclSyntax] { + let name = context.makeUniqueName("unique") + return [ + """ + class MyClass { + func \(name)() {} + } + """ + ] + } +} diff --git a/Tests/MacroTestingTests/MacroExamples/MemberDeprecatedMacro.swift b/Tests/MacroTestingTests/MacroExamples/MemberDeprecatedMacro.swift new file mode 100644 index 0000000..27f7418 --- /dev/null +++ b/Tests/MacroTestingTests/MacroExamples/MemberDeprecatedMacro.swift @@ -0,0 +1,26 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 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 +import SwiftSyntaxMacros + +/// Add '@available(*, deprecated)' to members. +public enum MemberDeprecatedMacro: MemberAttributeMacro { + public static func expansion( + of node: AttributeSyntax, + attachedTo declaration: some DeclGroupSyntax, + providingAttributesFor member: some DeclSyntaxProtocol, + in context: some MacroExpansionContext + ) throws -> [AttributeSyntax] { + return ["@available(*, deprecated)"] + } +} diff --git a/Tests/MacroTestingTests/MacroExamples/MetaEnumMacro.swift b/Tests/MacroTestingTests/MacroExamples/MetaEnumMacro.swift index 24557dd..4120d05 100644 --- a/Tests/MacroTestingTests/MacroExamples/MetaEnumMacro.swift +++ b/Tests/MacroTestingTests/MacroExamples/MetaEnumMacro.swift @@ -1,3 +1,15 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 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 SwiftDiagnostics import SwiftSyntax import SwiftSyntaxBuilder @@ -9,9 +21,7 @@ public struct MetaEnumMacro { let access: DeclModifierListSyntax.Element? let parentParamName: TokenSyntax - init( - node: AttributeSyntax, declaration: some DeclGroupSyntax, context: some MacroExpansionContext - ) throws { + init(node: AttributeSyntax, declaration: some DeclGroupSyntax, context: some MacroExpansionContext) throws { guard let enumDecl = declaration.as(EnumDeclSyntax.self) else { throw DiagnosticsError(diagnostics: [ CaseMacroDiagnostic.notAnEnum(declaration).diagnose(at: Syntax(node)) @@ -31,9 +41,12 @@ public struct MetaEnumMacro { func makeMetaEnum() -> DeclSyntax { // FIXME: Why does this need to be a string to make trailing trivia work properly? - let caseDecls = childCases.map { childCase in - "case \(childCase.name)" - }.joined(separator: "\n") + let caseDecls = + childCases + .map { childCase in + " case \(childCase.name)" + } + .joined(separator: "\n") return """ \(access)enum Meta { @@ -44,18 +57,22 @@ public struct MetaEnumMacro { } func makeMetaInit() -> DeclSyntax { - let caseStatements = childCases.map { childCase in - """ - case .\(childCase.name): - self = .\(childCase.name) - """ - }.joined(separator: "\n") + // FIXME: Why does this need to be a string to make trailing trivia work properly? + let caseStatements = + childCases + .map { childCase in + """ + case .\(childCase.name): + self = .\(childCase.name) + """ + } + .joined(separator: "\n") return """ \(access)init(_ \(parentParamName): \(parentTypeName)) { - switch \(parentParamName) { + switch \(parentParamName) { \(raw: caseStatements) - } + } } """ } @@ -77,7 +94,7 @@ extension EnumDeclSyntax { var caseElements: [EnumCaseElementSyntax] { memberBlock.members.flatMap { member in guard let caseDecl = member.decl.as(EnumCaseDeclSyntax.self) else { - return [EnumCaseElementSyntax]() + return Array() } return Array(caseDecl.elements) @@ -93,8 +110,7 @@ extension CaseMacroDiagnostic: DiagnosticMessage { var message: String { switch self { case .notAnEnum(let decl): - return - "'@MetaEnum' can only be attached to an enum, not \(decl.descriptiveDeclKind(withArticle: true))" + return "'@MetaEnum' can only be attached to an enum, not \(decl.descriptiveDeclKind(withArticle: true))" } } diff --git a/Tests/MacroTestingTests/MacroExamples/NewTypeMacro.swift b/Tests/MacroTestingTests/MacroExamples/NewTypeMacro.swift index 4723d77..1740aaf 100644 --- a/Tests/MacroTestingTests/MacroExamples/NewTypeMacro.swift +++ b/Tests/MacroTestingTests/MacroExamples/NewTypeMacro.swift @@ -1,3 +1,15 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 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 import SwiftSyntaxBuilder import SwiftSyntaxMacros @@ -18,8 +30,7 @@ extension NewTypeMacro: MemberMacro { .expression.as(MemberAccessExprSyntax.self), let rawType = memberAccessExn.base?.as(DeclReferenceExprSyntax.self) else { - throw CustomError.message( - #"@NewType requires the raw type as an argument, in the form "RawType.self"."#) + throw CustomError.message(#"@NewType requires the raw type as an argument, in the form "RawType.self"."#) } guard let declaration = declaration.as(StructDeclSyntax.self) else { diff --git a/Tests/MacroTestingTests/MacroExamples/OptionSetMacro.swift b/Tests/MacroTestingTests/MacroExamples/OptionSetMacro.swift index 16d2aa5..5d2e0c8 100644 --- a/Tests/MacroTestingTests/MacroExamples/OptionSetMacro.swift +++ b/Tests/MacroTestingTests/MacroExamples/OptionSetMacro.swift @@ -1,3 +1,15 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 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 SwiftDiagnostics import SwiftSyntax import SwiftSyntaxBuilder @@ -79,9 +91,7 @@ public struct OptionSetMacro { stringLiteral.segments.count == 1, case let .stringSegment(optionsEnumNameString)? = stringLiteral.segments.first else { - context.diagnose( - OptionSetMacroDiagnostic.requiresStringLiteral(optionsEnumNameArgumentLabel).diagnose( - at: optionEnumNameArg.expression)) + context.diagnose(OptionSetMacroDiagnostic.requiresStringLiteral(optionsEnumNameArgumentLabel).diagnose(at: optionEnumNameArg.expression)) return nil } @@ -108,15 +118,12 @@ public struct OptionSetMacro { return nil }).first else { - context.diagnose( - OptionSetMacroDiagnostic.requiresOptionsEnum(optionsEnumName).diagnose(at: decl)) + context.diagnose(OptionSetMacroDiagnostic.requiresOptionsEnum(optionsEnumName).diagnose(at: decl)) return nil } // Retrieve the raw type from the attribute. - guard - let genericArgs = attribute.attributeName.as(IdentifierTypeSyntax.self)? - .genericArgumentClause, + guard let genericArgs = attribute.attributeName.as(IdentifierTypeSyntax.self)?.genericArgumentClause, let rawType = genericArgs.arguments.first?.argument else { context.diagnose(OptionSetMacroDiagnostic.requiresOptionsEnumRawType.diagnose(at: attribute)) @@ -136,24 +143,18 @@ extension OptionSetMacro: ExtensionMacro { in context: some MacroExpansionContext ) throws -> [ExtensionDeclSyntax] { // Decode the expansion arguments. - guard let (structDecl, _, _) = decodeExpansion(of: node, attachedTo: declaration, in: context) - else { + guard let (structDecl, _, _) = decodeExpansion(of: node, attachedTo: declaration, in: context) else { return [] } // If there is an explicit conformance to OptionSet already, don't add one. if let inheritedTypes = structDecl.inheritanceClause?.inheritedTypes, - inheritedTypes.contains(where: { inherited in inherited.type.trimmedDescription == "OptionSet" - }) + inheritedTypes.contains(where: { inherited in inherited.type.trimmedDescription == "OptionSet" }) { return [] } - let ext: DeclSyntax = - """ - extension \(type.trimmed): OptionSet {} - """ - return [ext.cast(ExtensionDeclSyntax.self)] + return [try ExtensionDeclSyntax("extension \(type): OptionSet {}")] } } @@ -164,16 +165,14 @@ extension OptionSetMacro: MemberMacro { in context: some MacroExpansionContext ) throws -> [DeclSyntax] { // Decode the expansion arguments. - guard - let (_, optionsEnum, rawType) = decodeExpansion(of: attribute, attachedTo: decl, in: context) - else { + guard let (_, optionsEnum, rawType) = decodeExpansion(of: attribute, attachedTo: decl, in: context) else { return [] } // Find all of the case elements. let caseElements = optionsEnum.memberBlock.members.flatMap { member in guard let caseDecl = member.decl.as(EnumCaseDeclSyntax.self) else { - return [EnumCaseElementSyntax]() + return Array() } return Array(caseDecl.elements) diff --git a/Tests/MacroTestingTests/MacroExamples/PeerValueWithSuffixMacro.swift b/Tests/MacroTestingTests/MacroExamples/PeerValueWithSuffixMacro.swift new file mode 100644 index 0000000..1e0aa91 --- /dev/null +++ b/Tests/MacroTestingTests/MacroExamples/PeerValueWithSuffixMacro.swift @@ -0,0 +1,28 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 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 +import SwiftSyntaxMacros + +/// Peer 'var' with the name suffixed with '_peer'. +public enum PeerValueWithSuffixNameMacro: PeerMacro { + public static func expansion( + of node: AttributeSyntax, + providingPeersOf declaration: some DeclSyntaxProtocol, + in context: some MacroExpansionContext + ) throws -> [DeclSyntax] { + guard let identified = declaration.asProtocol(NamedDeclSyntax.self) else { + return [] + } + return ["var \(raw: identified.name.text)_peer: Int { 1 }"] + } +} diff --git a/Tests/MacroTestingTests/MacroExamples/StringifyMacro.swift b/Tests/MacroTestingTests/MacroExamples/StringifyMacro.swift index 57047ab..e351b6b 100644 --- a/Tests/MacroTestingTests/MacroExamples/StringifyMacro.swift +++ b/Tests/MacroTestingTests/MacroExamples/StringifyMacro.swift @@ -1,3 +1,15 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 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 import SwiftSyntaxBuilder import SwiftSyntaxMacros @@ -11,7 +23,7 @@ import SwiftSyntaxMacros /// will expand to /// /// (x + y, "x + y") -public struct StringifyMacro: ExpressionMacro { +public enum StringifyMacro: ExpressionMacro { public static func expansion( of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext diff --git a/Tests/MacroTestingTests/MacroExamples/URLMacro.swift b/Tests/MacroTestingTests/MacroExamples/URLMacro.swift index e20c41f..44054a2 100644 --- a/Tests/MacroTestingTests/MacroExamples/URLMacro.swift +++ b/Tests/MacroTestingTests/MacroExamples/URLMacro.swift @@ -1,15 +1,26 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 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 Foundation import SwiftSyntax import SwiftSyntaxMacros /// Creates a non-optional URL from a static string. The string is checked to /// be valid during compile time. -public struct URLMacro: ExpressionMacro { +public enum URLMacro: ExpressionMacro { public static func expansion( of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext ) throws -> ExprSyntax { - guard let argument = node.argumentList.first?.expression, let segments = argument.as(StringLiteralExprSyntax.self)?.segments, segments.count == 1, diff --git a/Tests/MacroTestingTests/MacroExamples/WarningMacro.swift b/Tests/MacroTestingTests/MacroExamples/WarningMacro.swift index 33e5a3f..35a0c9d 100644 --- a/Tests/MacroTestingTests/MacroExamples/WarningMacro.swift +++ b/Tests/MacroTestingTests/MacroExamples/WarningMacro.swift @@ -1,15 +1,27 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 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 SwiftDiagnostics import SwiftSyntax import SwiftSyntaxMacros /// Implementation of the `myWarning` macro, which mimics the behavior of the /// built-in `#warning`. -public struct WarningMacro: ExpressionMacro { +public enum WarningMacro: ExpressionMacro { public static func expansion( - of macro: some FreestandingMacroExpansionSyntax, + of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext ) throws -> ExprSyntax { - guard let firstElement = macro.argumentList.first, + guard let firstElement = node.argumentList.first, let stringLiteral = firstElement.expression .as(StringLiteralExprSyntax.self), stringLiteral.segments.count == 1, @@ -20,10 +32,10 @@ public struct WarningMacro: ExpressionMacro { context.diagnose( Diagnostic( - node: Syntax(macro), + node: Syntax(node), message: SimpleDiagnosticMessage( message: messageString.content.description, - diagnosticID: MessageID(domain: "test", id: "error"), + diagnosticID: MessageID(domain: "test123", id: "error"), severity: .warning ) ) diff --git a/Tests/MacroTestingTests/MacroExamples/WrapStoredPropertiesMacro.swift b/Tests/MacroTestingTests/MacroExamples/WrapStoredPropertiesMacro.swift index b036015..d885331 100644 --- a/Tests/MacroTestingTests/MacroExamples/WrapStoredPropertiesMacro.swift +++ b/Tests/MacroTestingTests/MacroExamples/WrapStoredPropertiesMacro.swift @@ -1,3 +1,15 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 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 import SwiftSyntaxBuilder import SwiftSyntaxMacros @@ -31,8 +43,7 @@ public struct WrapStoredPropertiesMacro: MemberAttributeMacro { stringLiteral.segments.count == 1, case let .stringSegment(wrapperName)? = stringLiteral.segments.first else { - throw CustomError.message( - "macro requires a string literal containing the name of an attribute") + throw CustomError.message("macro requires a string literal containing the name of an attribute") } return [ diff --git a/Tests/MacroTestingTests/MemberDeprecatedMacroTests.swift b/Tests/MacroTestingTests/MemberDeprecatedMacroTests.swift new file mode 100644 index 0000000..cc98740 --- /dev/null +++ b/Tests/MacroTestingTests/MemberDeprecatedMacroTests.swift @@ -0,0 +1,42 @@ +import MacroTesting +import XCTest + +final class MemberDepreacatedMacroTests: XCTestCase { + override func invokeTest() { + withMacroTesting(macros: ["memberDeprecated": MemberDeprecatedMacro.self]) { + super.invokeTest() + } + } + + func testExpansionMarksMembersAsDeprecated() { + assertMacro { + """ + @memberDeprecated + public struct SomeStruct { + typealias MacroName = String + + public var oldProperty: Int = 420 + + func oldMethod() { + print("This is an old method.") + } + } + """ + } expansion: { + """ + public struct SomeStruct { + @available(*, deprecated) + typealias MacroName = String + @available(*, deprecated) + + public var oldProperty: Int = 420 + @available(*, deprecated) + + func oldMethod() { + print("This is an old method.") + } + } + """ + } + } +} diff --git a/Tests/MacroTestingTests/MetaEnumMacroTests.swift b/Tests/MacroTestingTests/MetaEnumMacroTests.swift index d9f76cd..f12c0d4 100644 --- a/Tests/MacroTestingTests/MetaEnumMacroTests.swift +++ b/Tests/MacroTestingTests/MetaEnumMacroTests.swift @@ -8,19 +8,19 @@ final class MetaEnumMacroTests: BaseTestCase { } } - func testMetaEnum() { + func testExpansionAddsNestedMetaEnum() { assertMacro { - #""" - @MetaEnum enum Value { + """ + @MetaEnum enum Cell { case integer(Int) case text(String) case boolean(Bool) case null } - """# + """ } expansion: { """ - enum Value { + enum Cell { case integer(Int) case text(String) case boolean(Bool) @@ -31,7 +31,7 @@ final class MetaEnumMacroTests: BaseTestCase { case text case boolean case null - init(_ __macro_local_6parentfMu_: Value) { + init(_ __macro_local_6parentfMu_: Cell) { switch __macro_local_6parentfMu_ { case .integer: self = .integer @@ -49,7 +49,7 @@ final class MetaEnumMacroTests: BaseTestCase { } } - func testAccess() { + func testExpansionAddsPublicNestedMetaEnum() { assertMacro { """ @MetaEnum public enum Cell { @@ -85,7 +85,7 @@ final class MetaEnumMacroTests: BaseTestCase { } } - func testNonEnum() { + func testExpansionOnStructEmitsError() { assertMacro { """ @MetaEnum struct Cell { diff --git a/Tests/MacroTestingTests/NewTypeMacroTests.swift b/Tests/MacroTestingTests/NewTypeMacroTests.swift index fa0370d..cf9c0e2 100644 --- a/Tests/MacroTestingTests/NewTypeMacroTests.swift +++ b/Tests/MacroTestingTests/NewTypeMacroTests.swift @@ -8,7 +8,30 @@ final class NewTypeMacroTests: BaseTestCase { } } - func testNewType() { + func testExpansionAddsStringRawType() { + assertMacro { + """ + @NewType(String.self) + struct Username { + } + """ + } expansion: { + """ + struct Username { + + typealias RawValue = String + + var rawValue: RawValue + + init(_ rawValue: RawValue) { + self.rawValue = rawValue + } + } + """ + } + } + + func testExpansionWithPublicAddsPublicStringRawType() { assertMacro { """ @NewType(String.self) @@ -30,4 +53,40 @@ final class NewTypeMacroTests: BaseTestCase { """ } } + + func testExpansionOnClassEmitsError() { + assertMacro { + """ + @NewType(Int.self) + class NotAUsername { + } + """ + } diagnostics: { + """ + @NewType(Int.self) + ┬───────────────── + ╰─ 🛑 @NewType can only be applied to a struct declarations. + class NotAUsername { + } + """ + } + } + + func testExpansionWithMissingRawTypeEmitsError() { + assertMacro { + """ + @NewType + struct NoRawType { + } + """ + } diagnostics: { + """ + @NewType + ┬─────── + ╰─ 🛑 @NewType requires the raw type as an argument, in the form "RawType.self". + struct NoRawType { + } + """ + } + } } diff --git a/Tests/MacroTestingTests/OptionSetMacroTests.swift b/Tests/MacroTestingTests/OptionSetMacroTests.swift index ab34c39..05cac84 100644 --- a/Tests/MacroTestingTests/OptionSetMacroTests.swift +++ b/Tests/MacroTestingTests/OptionSetMacroTests.swift @@ -8,7 +8,7 @@ final class OptionSetMacroTests: BaseTestCase { } } - func testOptionSet() { + func testExpansionOnStructWithNestedEnumAndStatics() { assertMacro { """ @MyOptionSet @@ -67,4 +67,112 @@ final class OptionSetMacroTests: BaseTestCase { """ } } + + func testExpansionOnPublicStructWithExplicitOptionSetConformance() { + assertMacro { + """ + @MyOptionSet + public struct ShippingOptions: OptionSet { + private enum Options: Int { + case nextDay + case standard + } + } + """ + } expansion: { + """ + public struct ShippingOptions: OptionSet { + private enum Options: Int { + case nextDay + case standard + } + + public typealias RawValue = UInt8 + + public var rawValue: RawValue + + public init() { + self.rawValue = 0 + } + + public init(rawValue: RawValue) { + self.rawValue = rawValue + } + + public static let nextDay: Self = + Self (rawValue: 1 << Options.nextDay.rawValue) + + public static let standard: Self = + Self (rawValue: 1 << Options.standard.rawValue) + } + """ + } + } + + func testExpansionFailsOnEnumType() { + assertMacro { + """ + @MyOptionSet + enum Animal { + case dog + } + """ + } diagnostics: { + """ + @MyOptionSet + ├─ 🛑 'OptionSet' macro can only be applied to a struct + ╰─ 🛑 'OptionSet' macro can only be applied to a struct + enum Animal { + case dog + } + """ + } + } + + func testExpansionFailsWithoutNestedOptionsEnum() { + assertMacro { + """ + @MyOptionSet + struct ShippingOptions { + static let express: ShippingOptions = [.nextDay, .secondDay] + static let all: ShippingOptions = [.express, .priority, .standard] + } + """ + } diagnostics: { + """ + @MyOptionSet + ├─ 🛑 'OptionSet' macro requires nested options enum 'Options' + ╰─ 🛑 'OptionSet' macro requires nested options enum 'Options' + struct ShippingOptions { + static let express: ShippingOptions = [.nextDay, .secondDay] + static let all: ShippingOptions = [.express, .priority, .standard] + } + """ + } + } + + func testExpansionFailsWithoutSpecifiedRawType() { + assertMacro { + """ + @MyOptionSet + struct ShippingOptions { + private enum Options: Int { + case nextDay + } + } + """ + } diagnostics: { + """ + @MyOptionSet + ┬─────────── + ├─ 🛑 'OptionSet' macro requires a raw type + ╰─ 🛑 'OptionSet' macro requires a raw type + struct ShippingOptions { + private enum Options: Int { + case nextDay + } + } + """ + } + } } diff --git a/Tests/MacroTestingTests/PeerValueWithSuffixMacroTests.swift b/Tests/MacroTestingTests/PeerValueWithSuffixMacroTests.swift new file mode 100644 index 0000000..15039cb --- /dev/null +++ b/Tests/MacroTestingTests/PeerValueWithSuffixMacroTests.swift @@ -0,0 +1,61 @@ +import MacroTesting +import XCTest + +final class PeerValueWithSuffixNameMacroTests: XCTestCase { + override func invokeTest() { + withMacroTesting(macros: [PeerValueWithSuffixNameMacro.self]) { + super.invokeTest() + } + } + + func testExpansionAddsPeerValueToPrivateActor() { + assertMacro { + """ + @PeerValueWithSuffixName + private actor Counter { + var value = 0 + } + """ + } expansion: { + """ + private actor Counter { + var value = 0 + } + + var Counter_peer: Int { + 1 + } + """ + } + } + + func testExpansionAddsPeerValueToFunction() { + assertMacro { + """ + @PeerValueWithSuffixName + func someFunction() {} + """ + } expansion: { + """ + func someFunction() {} + + var someFunction_peer: Int { + 1 + } + """ + } + } + + func testExpansionIgnoresVariables() { + assertMacro { + """ + @PeerValueWithSuffixName + var someVariable: Int + """ + } expansion: { + """ + var someVariable: Int + """ + } + } +} diff --git a/Tests/MacroTestingTests/StringifyMacroTests.swift b/Tests/MacroTestingTests/StringifyMacroTests.swift index f82e87f..834890c 100644 --- a/Tests/MacroTestingTests/StringifyMacroTests.swift +++ b/Tests/MacroTestingTests/StringifyMacroTests.swift @@ -8,19 +8,27 @@ final class StringifyMacroTests: BaseTestCase { } } - func testStringify() { + func testExpansionWithBasicArithmeticExpression() { assertMacro { - #""" - let x = 1 - let y = 2 - print(#stringify(x + y)) - """# + """ + let a = #stringify(x + y) + """ } expansion: { """ - let x = 1 - let y = 2 - print((x + y, "x + y")) + let a = (x + y, "x + y") """ } } + + func testExpansionWithStringInterpolation() { + assertMacro { + #""" + let b = #stringify("Hello, \(name)") + """# + } expansion: { + #""" + let b = ("Hello, \(name)", #""Hello, \(name)""#) + """# + } + } } diff --git a/Tests/MacroTestingTests/URLMacroTests.swift b/Tests/MacroTestingTests/URLMacroTests.swift index c5ff289..5a1bcf0 100644 --- a/Tests/MacroTestingTests/URLMacroTests.swift +++ b/Tests/MacroTestingTests/URLMacroTests.swift @@ -8,44 +8,42 @@ final class URLMacroTests: BaseTestCase { } } - func testURL() { + func testExpansionWithMalformedURLEmitsError() { assertMacro { - #""" - print(#URL("https://swift.org/")) - """# - } expansion: { """ - print(URL(string: "https://swift.org/")!) + let invalid = #URL("https://not a url.com") + """ + } diagnostics: { + """ + let invalid = #URL("https://not a url.com") + ┬──────────────────────────── + ╰─ 🛑 malformed url: "https://not a url.com" """ } } - func testNonStaticURL() { + func testExpansionWithStringInterpolationEmitsError() { assertMacro { #""" - let domain = "domain.com" - print(#URL("https://\(domain)/api/path")) + #URL("https://\(domain)/api/path") """# } diagnostics: { #""" - let domain = "domain.com" - print(#URL("https://\(domain)/api/path")) - ┬───────────────────────────────── - ╰─ 🛑 #URL requires a static string literal + #URL("https://\(domain)/api/path") + ┬───────────────────────────────── + ╰─ 🛑 #URL requires a static string literal """# } } - func testMalformedURL() { + func testExpansionWithValidURL() { assertMacro { - #""" - print(#URL("https://not a url.com")) - """# - } diagnostics: { """ - print(#URL("https://not a url.com")) - ┬──────────────────────────── - ╰─ 🛑 malformed url: "https://not a url.com" + let valid = #URL("https://swift.org/") + """ + } expansion: { + """ + let valid = URL(string: "https://swift.org/")! """ } } diff --git a/Tests/MacroTestingTests/WarningMacroTests.swift b/Tests/MacroTestingTests/WarningMacroTests.swift index dea3088..525a8f1 100644 --- a/Tests/MacroTestingTests/WarningMacroTests.swift +++ b/Tests/MacroTestingTests/WarningMacroTests.swift @@ -8,16 +8,16 @@ final class WarningMacroTests: BaseTestCase { } } - func testWarning() { + func testExpansionWithValidStringLiteralEmitsWarning() { assertMacro { - #""" - #myWarning("remember to pass a string literal here") - """# + """ + #myWarning("This is a warning") + """ } diagnostics: { """ - #myWarning("remember to pass a string literal here") - ┬─────────────────────────────────────────────────── - ╰─ ⚠️ remember to pass a string literal here + #myWarning("This is a warning") + ┬────────────────────────────── + ╰─ ⚠️ This is a warning """ } expansion: { """ @@ -26,19 +26,31 @@ final class WarningMacroTests: BaseTestCase { } } - func testNonLiteral() { + func testExpansionWithInvalidExpressionEmitsError() { assertMacro { """ - let text = "oops" - #myWarning(text) + #myWarning(42) """ } diagnostics: { """ - let text = "oops" - #myWarning(text) - ┬─────────────── + #myWarning(42) + ┬───────────── ╰─ 🛑 #myWarning macro requires a string literal """ } } + + func testExpansionWithStringInterpolationEmitsError() { + assertMacro { + #""" + #myWarning("Say hello \(number) times!") + """# + } diagnostics: { + #""" + #myWarning("Say hello \(number) times!") + ┬─────────────────────────────────────── + ╰─ 🛑 #myWarning macro requires a string literal + """# + } + } } diff --git a/Tests/MacroTestingTests/WrapStoredPropertiesMacroTests.swift b/Tests/MacroTestingTests/WrapStoredPropertiesMacroTests.swift index 2fde6b0..d21a17f 100644 --- a/Tests/MacroTestingTests/WrapStoredPropertiesMacroTests.swift +++ b/Tests/MacroTestingTests/WrapStoredPropertiesMacroTests.swift @@ -8,19 +8,80 @@ final class WrapStoredPropertiesMacroTests: BaseTestCase { } } - func testWrapStoredProperties() { + func testExpansionAddsPublished() { + assertMacro { + """ + @wrapStoredProperties("Published") + struct Test { + var value: Int + } + """ + } expansion: { + """ + struct Test { + @Published + var value: Int + } + """ + } + } + + func testExpansionAddsDeprecationAttribute() { assertMacro { """ @wrapStoredProperties(#"available(*, deprecated, message: "hands off my data")"#) - struct OldStorage { - var x: Int + struct Test { + var value: Int } """ } expansion: { """ - struct OldStorage { + struct Test { @available(*, deprecated, message: "hands off my data") - var x: Int + var value: Int + } + """ + } + } + + func testExpansionIgnoresComputedProperty() { + assertMacro { + """ + @wrapStoredProperties("Published") + struct Test { + var value: Int { + get { return 0 } + set {} + } + } + """ + } expansion: { + """ + struct Test { + var value: Int { + get { return 0 } + set {} + } + } + """ + } + } + + func testExpansionWithInvalidAttributeEmitsError() { + assertMacro { + """ + @wrapStoredProperties(12) + struct Test { + var value: Int + } + """ + } diagnostics: { + """ + @wrapStoredProperties(12) + ┬──────────────────────── + ╰─ 🛑 macro requires a string literal containing the name of an attribute + struct Test { + var value: Int } """ }