From aad58a01eb7de9c4d5c8885be93d0f6f1ed02957 Mon Sep 17 00:00:00 2001 From: kyrylokhlopko Date: Sun, 25 Jun 2023 11:57:37 +0300 Subject: [PATCH 1/2] Handle missing type on property declaration & computed properties --- Sources/SweetDeclarationsClient/main.swift | 1 + .../DeclarationProperty.swift | 11 ++++ .../PublicInitMacro.swift | 1 + .../PublicInitMacroTests.swift | 61 +++++++++++++++++++ 4 files changed, 74 insertions(+) diff --git a/Sources/SweetDeclarationsClient/main.swift b/Sources/SweetDeclarationsClient/main.swift index b7d209f..b1f9116 100644 --- a/Sources/SweetDeclarationsClient/main.swift +++ b/Sources/SweetDeclarationsClient/main.swift @@ -11,6 +11,7 @@ public typealias GetConnections = () -> [User] @PublicInit(escaping: [GetConnections.self]) @GranularUpdate public struct User { + var desc: String { "\(id)+\(name)" } public let id: String public let name: Name public let getConnections: GetConnections diff --git a/Sources/SweetDeclarationsPlugin/DeclarationProperty.swift b/Sources/SweetDeclarationsPlugin/DeclarationProperty.swift index 67359c3..5e9d4cf 100644 --- a/Sources/SweetDeclarationsPlugin/DeclarationProperty.swift +++ b/Sources/SweetDeclarationsPlugin/DeclarationProperty.swift @@ -9,6 +9,12 @@ import SwiftDiagnostics internal struct DeclarationProperty { + struct TypeNotFoundMessage: DiagnosticMessage { + let message = "Property type not found or not supported. Specify type explicitly if its missing to fix this error" + let diagnosticID = SwiftDiagnostics.MessageID(domain: "PublicInitMacro", id: "TypeNotFound") + let severity: SwiftDiagnostics.DiagnosticSeverity = .error + } + internal static func gather( from declaration: some SwiftSyntax.DeclGroupSyntax, in context: some SwiftSyntaxMacros.MacroExpansionContext @@ -24,6 +30,10 @@ internal struct DeclarationProperty { else { return nil } + let isComputed = varDecl.bindings.contains { $0.accessor?.is(CodeBlockSyntax.self) == true } + guard !isComputed else { + return nil + } let typeSyntax = patternBindingSyntax?.typeAnnotation?.type let propertyType: String let isClosure: Bool @@ -39,6 +49,7 @@ internal struct DeclarationProperty { propertyType = closureType.description isClosure = true } else { + context.diagnose(Diagnostic(node: varDecl._syntaxNode, message: TypeNotFoundMessage())) return nil } return DeclarationProperty( diff --git a/Sources/SweetDeclarationsPlugin/PublicInitMacro.swift b/Sources/SweetDeclarationsPlugin/PublicInitMacro.swift index 32143d9..0755db5 100644 --- a/Sources/SweetDeclarationsPlugin/PublicInitMacro.swift +++ b/Sources/SweetDeclarationsPlugin/PublicInitMacro.swift @@ -5,6 +5,7 @@ import SwiftSyntax import SwiftSyntaxBuilder import SwiftSyntaxMacros +import SwiftDiagnostics public struct PublicInitMacro: MemberMacro { diff --git a/Tests/SweetDeclarationsLibTests/PublicInitMacroTests.swift b/Tests/SweetDeclarationsLibTests/PublicInitMacroTests.swift index 682f235..ab251e0 100644 --- a/Tests/SweetDeclarationsLibTests/PublicInitMacroTests.swift +++ b/Tests/SweetDeclarationsLibTests/PublicInitMacroTests.swift @@ -177,4 +177,65 @@ internal final class PublicInitMacroTests: XCTestCase { ) } + func test_explicitTypeIsRequired() { + assertMacroExpansion( + #""" + @PublicInit + public struct User { + public var id = "default_id" + public let name: String + } + """#, + expandedSource: #""" + public struct User { + public var id = "default_id" + public let name: String + public init( + name: String + ) { + self.name = name + } + } + """#, + diagnostics: [ + DiagnosticSpec( + message: "Property type not found or not supported. Specify type explicitly if its missing to fix this error", + line: 3, + column: 5 + ) + ], + macros: testMacros + ) + } + + func test_ignoresComputedProperties() { + assertMacroExpansion( + #""" + @PublicInit + public struct User { + public var descr: String { "\(id)+\(name)" } + public let id: String + public let name: String + } + """#, + expandedSource: #""" + public struct User { + public var descr: String { + "\(id)+\(name)" + } + public let id: String + public let name: String + public init( + id: String, + name: String + ) { + self.id = id + self.name = name + } + } + """#, + macros: testMacros + ) + } + } From 463672c8dda4cf9ff4c7c285460592597b94804c Mon Sep 17 00:00:00 2001 From: kyrylokhlopko Date: Tue, 6 Aug 2024 20:38:17 +0200 Subject: [PATCH 2/2] Update with Swift 6 --- .github/workflows/swift.yml | 27 +- Package.resolved | 7 +- Package.swift | 45 ++-- README.md | 2 +- Sources/SweetDeclarationsLib/Macros.swift | 15 -- Sources/SwiftUtilMacros/Macros.swift | 20 ++ .../main.swift | 9 +- ...ributeSyntax+MacrosAttributesParsing.swift | 13 +- .../DebugMsg.swift | 0 .../DeclarationProperty.swift | 125 +++++---- .../GranularUpdateMacro.swift | 6 +- .../Plugin.swift} | 2 +- .../PublicInitMacro.swift | 23 +- .../TestStubMacro.swift | 8 +- .../GranularUpdateMacroTests.swift | 105 -------- .../PublicInitMacroTests.swift | 241 ------------------ .../GranularUpdateMacroTests.swift | 109 ++++++++ .../PublicInitMacroTests.swift | 221 ++++++++++++++++ .../TestStubMacroTests.swift | 30 ++- 19 files changed, 516 insertions(+), 492 deletions(-) delete mode 100644 Sources/SweetDeclarationsLib/Macros.swift create mode 100644 Sources/SwiftUtilMacros/Macros.swift rename Sources/{SweetDeclarationsClient => SwiftUtilMacrosClient}/main.swift (86%) rename Sources/{SweetDeclarationsPlugin => SwiftUtilMacrosPlugin}/AttributeSyntax+MacrosAttributesParsing.swift (57%) rename Sources/{SweetDeclarationsPlugin => SwiftUtilMacrosPlugin}/DebugMsg.swift (100%) rename Sources/{SweetDeclarationsPlugin => SwiftUtilMacrosPlugin}/DeclarationProperty.swift (67%) rename Sources/{SweetDeclarationsPlugin => SwiftUtilMacrosPlugin}/GranularUpdateMacro.swift (87%) rename Sources/{SweetDeclarationsPlugin/SweetDeclarationsPlugin.swift => SwiftUtilMacrosPlugin/Plugin.swift} (90%) rename Sources/{SweetDeclarationsPlugin => SwiftUtilMacrosPlugin}/PublicInitMacro.swift (59%) rename Sources/{SweetDeclarationsPlugin => SwiftUtilMacrosPlugin}/TestStubMacro.swift (91%) delete mode 100644 Tests/SweetDeclarationsLibTests/GranularUpdateMacroTests.swift delete mode 100644 Tests/SweetDeclarationsLibTests/PublicInitMacroTests.swift create mode 100644 Tests/SwiftUtilMacrosPluginTests/GranularUpdateMacroTests.swift create mode 100644 Tests/SwiftUtilMacrosPluginTests/PublicInitMacroTests.swift rename Tests/{SweetDeclarationsLibTests => SwiftUtilMacrosPluginTests}/TestStubMacroTests.swift (84%) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 605f652..1ef38cb 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -1,7 +1,4 @@ -# This workflow will build a Swift project -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift - -name: Swift +name: test on: push: @@ -10,13 +7,17 @@ on: branches: [ "main" ] jobs: - build: - runs-on: macos-latest + run-tests: + runs-on: ${{ matrix.runner }} + strategy: + matrix: + runner: [ ubuntu-22.04, macos-latest ] steps: - - uses: swift-actions/setup-swift@v1 - with: - swift-version: "5.9" - - name: Build - run: swift build -v - - name: Run tests - run: swift test -v + - uses: actions/checkout@v4 + - uses: khlopko/setup-swift@bfd61cbd14eeef55a27afc45138b61ced7174839 + with: + swift-version: "main-snapshot" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - run: swift test + diff --git a/Package.resolved b/Package.resolved index ab64405..a53bdd5 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,14 +1,15 @@ { + "originHash" : "ac7a8b3c89189306fafff79842be1c9addc0a3faf5b97bab411a81122122615a", "pins" : [ { "identity" : "swift-syntax", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-syntax.git", "state" : { - "revision" : "f1e9245226002bb134884345d4809b9543da3666", - "version" : "509.0.0-swift-DEVELOPMENT-SNAPSHOT-2023-06-17-a" + "revision" : "06b5cdc432e93b60e3bdf53aff2857c6b312991a", + "version" : "600.0.0-prerelease-2024-07-30" } } ], - "version" : 2 + "version" : 3 } diff --git a/Package.swift b/Package.swift index db3f3ee..8efc9e8 100644 --- a/Package.swift +++ b/Package.swift @@ -1,49 +1,56 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 6.0 -import PackageDescription import CompilerPluginSupport +import PackageDescription let package = Package( - name: "SweetDeclarations", + name: "swift-util-macros", platforms: [ .macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), - .macCatalyst(.v13) + .macCatalyst(.v13), ], products: [ .library( - name: "SweetDeclarationsLib", - targets: ["SweetDeclarationsLib"] + name: "SwiftUtilMacros", + targets: ["SwiftUtilMacros"] ), .executable( - name: "SweetDeclarationsClient", - targets: ["SweetDeclarationsClient"] + name: "SwiftUtilMacrosClient", + targets: ["SwiftUtilMacrosClient"] ), ], dependencies: [ - .package( - url: "https://github.com/apple/swift-syntax.git", - from: "509.0.0-swift-5.9-DEVELOPMENT-SNAPSHOT-2023-04-25-b" - ), + .package(url: "https://github.com/apple/swift-syntax.git", from: "600.0.0-latest") ], targets: [ .macro( - name: "SweetDeclarationsPlugin", + name: "SwiftUtilMacrosPlugin", dependencies: [ .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), - .product(name: "SwiftCompilerPlugin", package: "swift-syntax") + .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), + ] + ), + .target( + name: "SwiftUtilMacros", + dependencies: [ + .target(name: "SwiftUtilMacrosPlugin") + ] + ), + .executableTarget( + name: "SwiftUtilMacrosClient", + dependencies: [ + .target(name: "SwiftUtilMacros") ] ), - .target(name: "SweetDeclarationsLib", dependencies: ["SweetDeclarationsPlugin"]), - .executableTarget(name: "SweetDeclarationsClient", dependencies: ["SweetDeclarationsLib"]), .testTarget( - name: "SweetDeclarationsLibTests", + name: "SwiftUtilMacrosPluginTests", dependencies: [ - "SweetDeclarationsLib", + .target(name: "SwiftUtilMacrosPlugin"), .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), ] - ) + ), ] ) diff --git a/README.md b/README.md index c4d2cdf..ea9cdf6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# SweetDeclarations +# swift-util-macros A set of Swift macros to provide convenient initialization / modification over structs and classes declarations. diff --git a/Sources/SweetDeclarationsLib/Macros.swift b/Sources/SweetDeclarationsLib/Macros.swift deleted file mode 100644 index 543bca2..0000000 --- a/Sources/SweetDeclarationsLib/Macros.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// Macros.swift -// - -@attached(member, names: arbitrary) -public macro PublicInit() = #externalMacro(module: "SweetDeclarationsPlugin", type: "PublicInitMacro") - -@attached(member, names: arbitrary) -public macro PublicInit(escaping: [Any.Type]) = #externalMacro(module: "SweetDeclarationsPlugin", type: "PublicInitMacro") - -@attached(member, names: arbitrary) -public macro GranularUpdate() = #externalMacro(module: "SweetDeclarationsPlugin", type: "GranularUpdateMacro") - -@attached(member, names: arbitrary) -public macro TestStub() = #externalMacro(module: "SweetDeclarationsPlugin", type: "TestStubMacro") diff --git a/Sources/SwiftUtilMacros/Macros.swift b/Sources/SwiftUtilMacros/Macros.swift new file mode 100644 index 0000000..522ed03 --- /dev/null +++ b/Sources/SwiftUtilMacros/Macros.swift @@ -0,0 +1,20 @@ +// +// Macros.swift +// + +@attached(member, names: arbitrary) +public macro PublicInit() = #externalMacro(module: "SwiftUtilMacrosPlugin", type: "PublicInitMacro") + +@attached(member, names: arbitrary) +public macro PublicInit(escaping: [Any.Type]) = #externalMacro(module: "SwiftUtilMacrosPlugin", type: "PublicInitMacro") + +@attached(member, names: arbitrary) +public macro GranularUpdate() = #externalMacro(module: "SwiftUtilMacrosPlugin", type: "GranularUpdateMacro") + +@attached(member, names: arbitrary) +public macro TestStub() = #externalMacro(module: "SwiftUtilMacrosPlugin", type: "TestStubMacro") + +@attached(member) +@attached(extension, conformances: OptionSet) +public macro BitMask() = #externalMacro(module: "SwiftUtilMacrosPlugin", type: "BitMaskOptionSetMacro") + diff --git a/Sources/SweetDeclarationsClient/main.swift b/Sources/SwiftUtilMacrosClient/main.swift similarity index 86% rename from Sources/SweetDeclarationsClient/main.swift rename to Sources/SwiftUtilMacrosClient/main.swift index b1f9116..9709c77 100644 --- a/Sources/SweetDeclarationsClient/main.swift +++ b/Sources/SwiftUtilMacrosClient/main.swift @@ -4,17 +4,15 @@ import Foundation -import SweetDeclarationsLib +import SwiftUtilMacros -public typealias GetConnections = () -> [User] - -@PublicInit(escaping: [GetConnections.self]) +@PublicInit @GranularUpdate public struct User { var desc: String { "\(id)+\(name)" } public let id: String public let name: Name - public let getConnections: GetConnections + public let getConnections: () -> [User] public let getPublications: (_ startDate: Date) -> [String] } @@ -53,3 +51,4 @@ final class SomeProtocolStub { return method3Result! } } + diff --git a/Sources/SweetDeclarationsPlugin/AttributeSyntax+MacrosAttributesParsing.swift b/Sources/SwiftUtilMacrosPlugin/AttributeSyntax+MacrosAttributesParsing.swift similarity index 57% rename from Sources/SweetDeclarationsPlugin/AttributeSyntax+MacrosAttributesParsing.swift rename to Sources/SwiftUtilMacrosPlugin/AttributeSyntax+MacrosAttributesParsing.swift index 1355b52..6d6944f 100644 --- a/Sources/SweetDeclarationsPlugin/AttributeSyntax+MacrosAttributesParsing.swift +++ b/Sources/SwiftUtilMacrosPlugin/AttributeSyntax+MacrosAttributesParsing.swift @@ -5,20 +5,15 @@ import SwiftSyntax extension SwiftSyntax.AttributeSyntax { - internal func macrosEscapingArgs() -> [String] { - let tupleElement = argument? - .as(TupleExprElementListSyntax.self)?.first? - .as(TupleExprElementSyntax.self) + let tupleElement = arguments?.as(LabeledExprListSyntax.self)?.first guard tupleElement?.label?.text == "escaping" else { return [] } - return tupleElement?.expression.as(ArrayExprSyntax.self)?.elements.compactMap { - $0 - .as(ArrayElementSyntax.self)?.expression + return tupleElement?.expression.as(ArrayExprSyntax.self)?.elements.compactMap { element in + element.expression .as(MemberAccessExprSyntax.self)?.base? - .as(IdentifierExprSyntax.self)?.identifier.text + .as(DeclReferenceExprSyntax.self)?.baseName.text } ?? [] } - } diff --git a/Sources/SweetDeclarationsPlugin/DebugMsg.swift b/Sources/SwiftUtilMacrosPlugin/DebugMsg.swift similarity index 100% rename from Sources/SweetDeclarationsPlugin/DebugMsg.swift rename to Sources/SwiftUtilMacrosPlugin/DebugMsg.swift diff --git a/Sources/SweetDeclarationsPlugin/DeclarationProperty.swift b/Sources/SwiftUtilMacrosPlugin/DeclarationProperty.swift similarity index 67% rename from Sources/SweetDeclarationsPlugin/DeclarationProperty.swift rename to Sources/SwiftUtilMacrosPlugin/DeclarationProperty.swift index 5e9d4cf..6d21cc1 100644 --- a/Sources/SweetDeclarationsPlugin/DeclarationProperty.swift +++ b/Sources/SwiftUtilMacrosPlugin/DeclarationProperty.swift @@ -2,20 +2,50 @@ // DeclarationProperty.swift // +import SwiftDiagnostics import SwiftSyntax import SwiftSyntaxBuilder import SwiftSyntaxMacros -import SwiftDiagnostics internal struct DeclarationProperty { + let propertyName: String + let propertyType: String + let isClosure: Bool + let explicitlyEscaping: Bool +} - struct TypeNotFoundMessage: DiagnosticMessage { - let message = "Property type not found or not supported. Specify type explicitly if its missing to fix this error" - let diagnosticID = SwiftDiagnostics.MessageID(domain: "PublicInitMacro", id: "TypeNotFound") - let severity: SwiftDiagnostics.DiagnosticSeverity = .error +extension DeclarationProperty { + func asInitParam(nillable: Bool) -> String { + let propertyType = decoratedType( + for: self, + nillable: nillable, + isExplicitlyEscaping: { _ in explicitlyEscaping } + ) + return " \(propertyName): \(propertyType)" } - internal static func gather( + private func decoratedType( + for property: DeclarationProperty, + nillable: Bool, + isExplicitlyEscaping: (String) -> Bool + ) -> String { + var propertyType: String = property.propertyType + let isExplicitlyEscaping = isExplicitlyEscaping(propertyType) + if nillable { + if property.isClosure { + propertyType = "(\(propertyType))" + } + propertyType = "\(propertyType)? = nil" + } + if !nillable && isExplicitlyEscaping { + propertyType = "@escaping \(propertyType)" + } + return propertyType + } +} + +extension DeclarationProperty { + static func gather( from declaration: some SwiftSyntax.DeclGroupSyntax, in context: some SwiftSyntaxMacros.MacroExpansionContext ) -> [DeclarationProperty] { @@ -30,90 +60,73 @@ internal struct DeclarationProperty { else { return nil } - let isComputed = varDecl.bindings.contains { $0.accessor?.is(CodeBlockSyntax.self) == true } + let isComputed = varDecl.bindings.contains { $0.accessorBlock != nil } guard !isComputed else { return nil } let typeSyntax = patternBindingSyntax?.typeAnnotation?.type let propertyType: String let isClosure: Bool + let explicitlyEscaping: Bool if let optionalType = typeSyntax?.as(OptionalTypeSyntax.self) { propertyType = optionalType.description - isClosure = optionalType.wrappedType - .as(TupleTypeSyntax.self)?.elements.first? + isClosure = + optionalType.wrappedType + .as(TupleTypeSyntax.self)?.elements.first?.type .is(FunctionTypeSyntax.self) == true - } else if let simpleType = typeSyntax?.as(SimpleTypeIdentifierSyntax.self)?.name.text { + explicitlyEscaping = false + } else if let simpleType = typeSyntax?.as(IdentifierTypeSyntax.self)?.name.text { propertyType = simpleType isClosure = false + explicitlyEscaping = false } else if let closureType = typeSyntax?.as(FunctionTypeSyntax.self) { propertyType = closureType.description isClosure = true + explicitlyEscaping = true } else { - context.diagnose(Diagnostic(node: varDecl._syntaxNode, message: TypeNotFoundMessage())) + context.diagnose( + Diagnostic(node: varDecl._syntaxNode, message: TypeNotFoundMessage())) return nil } return DeclarationProperty( propertyName: propertyName, propertyType: propertyType, - isClosure: isClosure + isClosure: isClosure, + explicitlyEscaping: explicitlyEscaping ) } } - - let propertyName: String - let propertyType: String - let isClosure: Bool - - func asInitParam(nillable: Bool, escapingPropertyTypes: [String]) -> String { - let propertyType = decoratedType( - for: self, - nillable: nillable, - isExplicitlyEscaping: { escapingPropertyTypes.contains($0) } - ) - return " \(propertyName): \(propertyType)" - } - - private func decoratedType( - for property: DeclarationProperty, - nillable: Bool, - isExplicitlyEscaping: (String) -> Bool - ) -> String { - var propertyType: String = property.propertyType - let isExplicitlyEscaping = isExplicitlyEscaping(propertyType) - if nillable { - if property.isClosure { - propertyType = "(\(propertyType))" - } - propertyType = "\(propertyType)? = nil" - } - let isNotNilClosure = property.isClosure && !nillable - if isNotNilClosure || isExplicitlyEscaping { - propertyType = "@escaping \(propertyType)" - } - return propertyType - } - } extension [DeclarationProperty] { - internal func asInitParams( - escapingPropertyTypes: [String], nillable: Bool ) -> SwiftSyntax.DeclSyntax { - SwiftSyntax.DeclSyntax(stringLiteral: map { - $0.asInitParam(nillable: nillable, escapingPropertyTypes: escapingPropertyTypes) - } - .joined(separator: ",\n")) + SwiftSyntax.DeclSyntax( + stringLiteral: map { + $0.asInitParam(nillable: nillable) + } + .joined(separator: ",\n") + ) } internal func asInitBody( decorateAssignment: (_ propertyName: String) -> String = { $0 } ) -> SwiftSyntax.DeclSyntax { - SwiftSyntax.DeclSyntax(stringLiteral: map { - "self.\($0.propertyName) = \(decorateAssignment($0.propertyName))" - } - .joined(separator: "\n")) + SwiftSyntax.DeclSyntax( + stringLiteral: map { + "self.\($0.propertyName) = \(decorateAssignment($0.propertyName))" + } + .joined(separator: "\n") + ) } +} +extension DeclarationProperty { + struct TypeNotFoundMessage: DiagnosticMessage { + let message = + "Property type not found or not supported. Specify type explicitly if its missing to fix this error" + let diagnosticID = SwiftDiagnostics.MessageID(domain: "PublicInitMacro", id: "TypeNotFound") + let severity: SwiftDiagnostics.DiagnosticSeverity = .error + } } diff --git a/Sources/SweetDeclarationsPlugin/GranularUpdateMacro.swift b/Sources/SwiftUtilMacrosPlugin/GranularUpdateMacro.swift similarity index 87% rename from Sources/SweetDeclarationsPlugin/GranularUpdateMacro.swift rename to Sources/SwiftUtilMacrosPlugin/GranularUpdateMacro.swift index 0e8ef4a..663835a 100644 --- a/Sources/SweetDeclarationsPlugin/GranularUpdateMacro.swift +++ b/Sources/SwiftUtilMacrosPlugin/GranularUpdateMacro.swift @@ -24,7 +24,7 @@ public struct GranularUpdateMacro: MemberMacro { let result: SwiftSyntax.DeclSyntax = """ public init( from another: \(raw: typeName), - \(raw: properties.asInitParams(escapingPropertyTypes: [], nillable: true)) + \(raw: properties.asInitParams(nillable: true)) ) { \(raw: properties.asInitBody(decorateAssignment: { "\($0) ?? another.\($0)" })) } @@ -36,10 +36,10 @@ public struct GranularUpdateMacro: MemberMacro { from declaration: some SwiftSyntax.DeclGroupSyntax ) throws -> String { if let structDecl = declaration.as(StructDeclSyntax.self) { - return structDecl.identifier.text + return structDecl.name.text } if let classDecl = declaration.as(ClassDeclSyntax.self) { - return classDecl.identifier.text + return classDecl.name.text } throw GenerationError.unsupportedType } diff --git a/Sources/SweetDeclarationsPlugin/SweetDeclarationsPlugin.swift b/Sources/SwiftUtilMacrosPlugin/Plugin.swift similarity index 90% rename from Sources/SweetDeclarationsPlugin/SweetDeclarationsPlugin.swift rename to Sources/SwiftUtilMacrosPlugin/Plugin.swift index 95aaeaf..be97bf2 100644 --- a/Sources/SweetDeclarationsPlugin/SweetDeclarationsPlugin.swift +++ b/Sources/SwiftUtilMacrosPlugin/Plugin.swift @@ -10,6 +10,6 @@ struct SweetDeclarationsPlugin: CompilerPlugin { let providingMacros: [Macro.Type] = [ GranularUpdateMacro.self, PublicInitMacro.self, - TestStubMacro.self + TestStubMacro.self, ] } diff --git a/Sources/SweetDeclarationsPlugin/PublicInitMacro.swift b/Sources/SwiftUtilMacrosPlugin/PublicInitMacro.swift similarity index 59% rename from Sources/SweetDeclarationsPlugin/PublicInitMacro.swift rename to Sources/SwiftUtilMacrosPlugin/PublicInitMacro.swift index 0755db5..e9d12ee 100644 --- a/Sources/SweetDeclarationsPlugin/PublicInitMacro.swift +++ b/Sources/SwiftUtilMacrosPlugin/PublicInitMacro.swift @@ -2,28 +2,33 @@ // PublicInitMacro.swift // +import SwiftDiagnostics import SwiftSyntax import SwiftSyntaxBuilder import SwiftSyntaxMacros -import SwiftDiagnostics public struct PublicInitMacro: MemberMacro { - public static func expansion( of node: SwiftSyntax.AttributeSyntax, providingMembersOf declaration: some SwiftSyntax.DeclGroupSyntax, in context: some SwiftSyntaxMacros.MacroExpansionContext ) throws -> [SwiftSyntax.DeclSyntax] { - let escapingArgs = node.macrosEscapingArgs() let properties = DeclarationProperty.gather(from: declaration, in: context) let result: SwiftSyntax.DeclSyntax = """ - public init( - \(raw: properties.asInitParams(escapingPropertyTypes: escapingArgs, nillable: false)) - ) { - \(raw: properties.asInitBody()) - } - """ + public init( + \(raw: properties.asInitParams(nillable: false)) + ) { + \(raw: properties.asInitBody()) + } + """ return [result] } +} +extension PublicInitMacro { + struct Message: DiagnosticMessage { + var diagnosticID: MessageID { .init(domain: "pim", id: "def") } + var severity: DiagnosticSeverity { .warning } + var message: String + } } diff --git a/Sources/SweetDeclarationsPlugin/TestStubMacro.swift b/Sources/SwiftUtilMacrosPlugin/TestStubMacro.swift similarity index 91% rename from Sources/SweetDeclarationsPlugin/TestStubMacro.swift rename to Sources/SwiftUtilMacrosPlugin/TestStubMacro.swift index 20d2067..e9e835c 100644 --- a/Sources/SweetDeclarationsPlugin/TestStubMacro.swift +++ b/Sources/SwiftUtilMacrosPlugin/TestStubMacro.swift @@ -2,10 +2,10 @@ // TestStubMacro.swift // +import SwiftDiagnostics import SwiftSyntax import SwiftSyntaxBuilder import SwiftSyntaxMacros -import SwiftDiagnostics public struct TestStubMacro: MemberMacro { @@ -27,7 +27,8 @@ public struct TestStubMacro: MemberMacro { let parametersList = parameters.map { $0.type.description }.joined(separator: ", ") callTrackSuffix = "Args: [\(parametersList)] = []" } else { - let parametersList = parameters + let parametersList = + parameters .map { "\($0.firstName.text): \($0.type.description)" } .joined(separator: ", ") callTrackSuffix = "Args: [(\(parametersList))] = []" @@ -43,7 +44,8 @@ public struct TestStubMacro: MemberMacro { properties.append(.init(stringLiteral: "var \(nameBase)Delay: Double?")) } if let returnType = funcDecl.signature.output?.returnType { - let type = "\(returnType.description.trimmingCharacters(in: .whitespacesAndNewlines))?" + let type = + "\(returnType.description.trimmingCharacters(in: .whitespacesAndNewlines))?" properties.append(.init(stringLiteral: "var \(nameBase)Result: \(type)")) } return properties diff --git a/Tests/SweetDeclarationsLibTests/GranularUpdateMacroTests.swift b/Tests/SweetDeclarationsLibTests/GranularUpdateMacroTests.swift deleted file mode 100644 index 1605752..0000000 --- a/Tests/SweetDeclarationsLibTests/GranularUpdateMacroTests.swift +++ /dev/null @@ -1,105 +0,0 @@ -// -// GranularUpdateMacroTests.swift -// - -import XCTest - -import SweetDeclarationsPlugin - -import SwiftSyntaxMacros -import SwiftSyntaxMacrosTestSupport - -internal final class GranularUpdateMacroTests: XCTestCase { - - let testMacros: [String: Macro.Type] = [ - "GranularUpdate": GranularUpdateMacro.self - ] - - func test_forStruct() { - assertMacroExpansion( - #""" - @GranularUpdate - public struct User { - public let id: String - public let name: String - } - """#, - expandedSource: #""" - public struct User { - public let id: String - public let name: String - public init( - from another: User, - id: String? = nil, - name: String? = nil - ) { - self.id = id ?? another.id - self.name = name ?? another.name - } - } - """#, - macros: testMacros - ) - } - - func test_forClass() { - assertMacroExpansion( - #""" - @GranularUpdate - public class User { - public let id: String - public let name: String - } - """#, - expandedSource: #""" - public class User { - public let id: String - public let name: String - public init( - from another: User, - id: String? = nil, - name: String? = nil - ) { - self.id = id ?? another.id - self.name = name ?? another.name - } - } - """#, - macros: testMacros - ) - } - - func test_handlesClosures() { - // assume we have declaration eariler like following: - // public typealias GetPublications = () -> [String] - assertMacroExpansion( - #""" - @GranularUpdate - public class User { - public let id: String - public let getName: () -> String - public let getPublications: GetPublications - } - """#, - expandedSource: #""" - public class User { - public let id: String - public let getName: () -> String - public let getPublications: GetPublications - public init( - from another: User, - id: String? = nil, - getName: (() -> String)? = nil, - getPublications: GetPublications? = nil - ) { - self.id = id ?? another.id - self.getName = getName ?? another.getName - self.getPublications = getPublications ?? another.getPublications - } - } - """#, - macros: testMacros - ) - } - -} diff --git a/Tests/SweetDeclarationsLibTests/PublicInitMacroTests.swift b/Tests/SweetDeclarationsLibTests/PublicInitMacroTests.swift deleted file mode 100644 index ab251e0..0000000 --- a/Tests/SweetDeclarationsLibTests/PublicInitMacroTests.swift +++ /dev/null @@ -1,241 +0,0 @@ -// -// PublicInitMacroTests.swift -// - -import XCTest - -import SweetDeclarationsPlugin - -import SwiftSyntaxMacros -import SwiftSyntaxMacrosTestSupport - -internal final class PublicInitMacroTests: XCTestCase { - - let testMacros: [String: Macro.Type] = [ - "PublicInit": PublicInitMacro.self - ] - - func test_forStruct() { - assertMacroExpansion( - #""" - @PublicInit - public struct User { - public let id: String - public let name: String - } - """#, - expandedSource: #""" - public struct User { - public let id: String - public let name: String - public init( - id: String, - name: String - ) { - self.id = id - self.name = name - } - } - """#, - macros: testMacros - ) - } - - func test_forClass() { - assertMacroExpansion( - #""" - @PublicInit - public class User { - public let id: String - public let name: String - } - """#, - expandedSource: #""" - public class User { - public let id: String - public let name: String - public init( - id: String, - name: String - ) { - self.id = id - self.name = name - } - } - """#, - macros: testMacros - ) - } - - func test_optionals() { - assertMacroExpansion( - #""" - @PublicInit - public struct User { - public let id: String - public let name: String - public let publications: [String]? - } - """#, - expandedSource: #""" - public struct User { - public let id: String - public let name: String - public let publications: [String]? - public init( - id: String, - name: String, - publications: [String]? - ) { - self.id = id - self.name = name - self.publications = publications - } - } - """#, - macros: testMacros - ) - } - - func test_withClosures() { - assertMacroExpansion( - #""" - @PublicInit - public struct User { - public let id: String - public let getName: () -> String - } - """#, - expandedSource: #""" - public struct User { - public let id: String - public let getName: () -> String - public init( - id: String, - getName: @escaping () -> String - ) { - self.id = id - self.getName = getName - } - } - """#, - macros: testMacros - ) - } - - func test_withOptionalClosures() { - assertMacroExpansion( - #""" - @PublicInit - public struct User { - public let id: String - public let getName: (() -> String)? - } - """#, - expandedSource: #""" - public struct User { - public let id: String - public let getName: (() -> String)? - public init( - id: String, - getName: (() -> String)? - ) { - self.id = id - self.getName = getName - } - } - """#, - macros: testMacros - ) - } - - func test_withTypeAliasedClosures() { - // assume we have declaration as following: - // public typealias GetName = () -> String - assertMacroExpansion( - #""" - @PublicInit(escaping: [GetName.self]) - public struct User { - public let id: String - public let getName: GetName - } - """#, - expandedSource: #""" - public struct User { - public let id: String - public let getName: GetName - public init( - id: String, - getName: @escaping GetName - ) { - self.id = id - self.getName = getName - } - } - """#, - macros: testMacros - ) - } - - func test_explicitTypeIsRequired() { - assertMacroExpansion( - #""" - @PublicInit - public struct User { - public var id = "default_id" - public let name: String - } - """#, - expandedSource: #""" - public struct User { - public var id = "default_id" - public let name: String - public init( - name: String - ) { - self.name = name - } - } - """#, - diagnostics: [ - DiagnosticSpec( - message: "Property type not found or not supported. Specify type explicitly if its missing to fix this error", - line: 3, - column: 5 - ) - ], - macros: testMacros - ) - } - - func test_ignoresComputedProperties() { - assertMacroExpansion( - #""" - @PublicInit - public struct User { - public var descr: String { "\(id)+\(name)" } - public let id: String - public let name: String - } - """#, - expandedSource: #""" - public struct User { - public var descr: String { - "\(id)+\(name)" - } - public let id: String - public let name: String - public init( - id: String, - name: String - ) { - self.id = id - self.name = name - } - } - """#, - macros: testMacros - ) - } - -} diff --git a/Tests/SwiftUtilMacrosPluginTests/GranularUpdateMacroTests.swift b/Tests/SwiftUtilMacrosPluginTests/GranularUpdateMacroTests.swift new file mode 100644 index 0000000..939c7b1 --- /dev/null +++ b/Tests/SwiftUtilMacrosPluginTests/GranularUpdateMacroTests.swift @@ -0,0 +1,109 @@ +// +// GranularUpdateMacroTests.swift +// + +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +import SwiftUtilMacrosPlugin +import XCTest + +internal final class GranularUpdateMacroTests: XCTestCase { + + let testMacros: [String: Macro.Type] = [ + "GranularUpdate": GranularUpdateMacro.self + ] + + func test_forStruct() { + assertMacroExpansion( + #""" + @GranularUpdate + public struct User { + public let id: String + public let name: String + } + """#, + expandedSource: + #""" + public struct User { + public let id: String + public let name: String + + public init( + from another: User, + id: String? = nil, + name: String? = nil + ) { + self.id = id ?? another.id + self.name = name ?? another.name + } + } + """#, + macros: testMacros + ) + } + + func test_forClass() { + assertMacroExpansion( + #""" + @GranularUpdate + public class User { + public let id: String + public let name: String + } + """#, + expandedSource: + #""" + public class User { + public let id: String + public let name: String + + public init( + from another: User, + id: String? = nil, + name: String? = nil + ) { + self.id = id ?? another.id + self.name = name ?? another.name + } + } + """#, + macros: testMacros + ) + } + + func test_handlesClosures() { + // assume we have declaration eariler like following: + // public typealias GetPublications = () -> [String] + assertMacroExpansion( + #""" + @GranularUpdate + public class User { + public let id: String + public let getName: () -> String + public let getPublications: GetPublications + } + """#, + expandedSource: + #""" + public class User { + public let id: String + public let getName: () -> String + public let getPublications: GetPublications + + public init( + from another: User, + id: String? = nil, + getName: (() -> String)? = nil, + getPublications: GetPublications? = nil + ) { + self.id = id ?? another.id + self.getName = getName ?? another.getName + self.getPublications = getPublications ?? another.getPublications + } + } + """#, + macros: testMacros + ) + } + +} diff --git a/Tests/SwiftUtilMacrosPluginTests/PublicInitMacroTests.swift b/Tests/SwiftUtilMacrosPluginTests/PublicInitMacroTests.swift new file mode 100644 index 0000000..2a7c736 --- /dev/null +++ b/Tests/SwiftUtilMacrosPluginTests/PublicInitMacroTests.swift @@ -0,0 +1,221 @@ +// +// PublicInitMacroTests.swift +// + +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +import SwiftUtilMacrosPlugin +import XCTest + +internal final class PublicInitMacroTests: XCTestCase { + + let testMacros: [String: Macro.Type] = [ + "PublicInit": PublicInitMacro.self + ] + + func test_forStruct() { + assertMacroExpansion( + #""" + @PublicInit + public struct User { + public let id: String + public let name: String + } + """#, + expandedSource: + #""" + public struct User { + public let id: String + public let name: String + + public init( + id: String, + name: String + ) { + self.id = id + self.name = name + } + } + """#, + macros: testMacros + ) + } + + func test_forClass() { + assertMacroExpansion( + #""" + @PublicInit + public class User { + public let id: String + public let name: String + } + """#, + expandedSource: + #""" + public class User { + public let id: String + public let name: String + + public init( + id: String, + name: String + ) { + self.id = id + self.name = name + } + } + """#, + macros: testMacros + ) + } + + func test_optionals() { + assertMacroExpansion( + #""" + @PublicInit + public struct User { + public let id: String + public let name: String + public let publications: [String]? + } + """#, + expandedSource: #""" + public struct User { + public let id: String + public let name: String + public let publications: [String]? + + public init( + id: String, + name: String, + publications: [String]? + ) { + self.id = id + self.name = name + self.publications = publications + } + } + """#, + macros: testMacros + ) + } + + func test_withClosures() { + assertMacroExpansion( + #""" + @PublicInit + public struct User { + public let id: String + public let getName: () -> String + } + """#, + expandedSource: + #""" + public struct User { + public let id: String + public let getName: () -> String + + public init( + id: String, + getName: @escaping () -> String + ) { + self.id = id + self.getName = getName + } + } + """#, + macros: testMacros + ) + } + + func test_withOptionalClosures() { + assertMacroExpansion( + #""" + @PublicInit + public struct User { + public let id: String + public let getName: (() -> String)? + } + """#, + expandedSource: + #""" + public struct User { + public let id: String + public let getName: (() -> String)? + + public init( + id: String, + getName: (() -> String)? + ) { + self.id = id + self.getName = getName + } + } + """#, + macros: testMacros + ) + } + + func test_explicitTypeIsRequired() { + assertMacroExpansion( + #""" + @PublicInit + public struct User { + public var id = "default_id" + public let name: String + } + """#, + expandedSource: #""" + public struct User { + public var id = "default_id" + public let name: String + + public init( + name: String + ) { + self.name = name + } + } + """#, + diagnostics: [ + DiagnosticSpec( + message: + "Property type not found or not supported. Specify type explicitly if its missing to fix this error", + line: 3, + column: 5 + ) + ], + macros: testMacros + ) + } + + func test_ignoresComputedProperties() { + assertMacroExpansion( + #""" + @PublicInit + public struct User { + public var descr: String { "\(id)+\(name)" } + public let id: String + public let name: String + } + """#, + expandedSource: + #""" + public struct User { + public var descr: String { "\(id)+\(name)" } + public let id: String + public let name: String + + public init( + id: String, + name: String + ) { + self.id = id + self.name = name + } + } + """#, + macros: testMacros + ) + } +} diff --git a/Tests/SweetDeclarationsLibTests/TestStubMacroTests.swift b/Tests/SwiftUtilMacrosPluginTests/TestStubMacroTests.swift similarity index 84% rename from Tests/SweetDeclarationsLibTests/TestStubMacroTests.swift rename to Tests/SwiftUtilMacrosPluginTests/TestStubMacroTests.swift index abe3de8..4a22afa 100644 --- a/Tests/SweetDeclarationsLibTests/TestStubMacroTests.swift +++ b/Tests/SwiftUtilMacrosPluginTests/TestStubMacroTests.swift @@ -4,7 +4,7 @@ import XCTest -import SweetDeclarationsPlugin +import SwiftUtilMacrosPlugin import SwiftSyntaxMacros import SwiftSyntaxMacrosTestSupport @@ -46,10 +46,14 @@ internal final class TestStubMacroTests: XCTestCase { func method4(value0: Int, value1: [String]) { } - private (set) var method1Calls: Int = 0 - private (set) var method2Args: [Int] = [] - private (set) var method3Args: [[String]] = [] - private (set) var method4Args: [(value0: Int, value1: [String])] = [] + + private(set) var method1Calls: Int = 0 + + private(set) var method2Args: [Int] = [] + + private(set) var method3Args: [[String]] = [] + + private(set) var method4Args: [(value0: Int, value1: [String])] = [] } """#, macros: testMacros @@ -69,7 +73,9 @@ internal final class TestStubMacroTests: XCTestCase { final class SomeProtocolStub: SomeProtocol { func method1() throws { } - private (set) var method1Calls: Int = 0 + + private(set) var method1Calls: Int = 0 + var method1Error: (any Error)? } """#, @@ -90,7 +96,9 @@ internal final class TestStubMacroTests: XCTestCase { final class SomeProtocolStub: SomeProtocol { func method1() async { } - private (set) var method1Calls: Int = 0 + + private(set) var method1Calls: Int = 0 + var method1Delay: Double? } """#, @@ -117,9 +125,13 @@ internal final class TestStubMacroTests: XCTestCase { func method2() -> [String] { } - private (set) var method1Calls: Int = 0 + + private(set) var method1Calls: Int = 0 + var method1Result: Int? - private (set) var method2Calls: Int = 0 + + private(set) var method2Calls: Int = 0 + var method2Result: [String]? } """#,