diff --git a/Package.resolved b/Package.resolved index d527ffb..a508748 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "300b63bc52fc36d4e0a555a932e9715417628f92b25454b2233d22a4d5c6a0f1", + "originHash" : "f7222f8d74cbdc2dcf2a590f0c4dec1650949abd03d2a9a80da83fa4e4950ccb", "pins" : [ { "identity" : "swift-syntax", diff --git a/Package.swift b/Package.swift index f1efe00..fd2ada5 100644 --- a/Package.swift +++ b/Package.swift @@ -22,7 +22,7 @@ let package = Package( dependencies: [ .package( url: "https://github.com/swiftlang/swift-syntax", - "600.0.0" ..< "602.0.0" + "600.0.0" ..< "604.0.0" ) ], targets: [ diff --git a/Sources/PrincipleMacros/Builders/Declarations/Common/DeclBuilder.swift b/Sources/PrincipleMacros/Builders/Declarations/Common/DeclBuilder.swift index c62f4b0..79d66ba 100644 --- a/Sources/PrincipleMacros/Builders/Declarations/Common/DeclBuilder.swift +++ b/Sources/PrincipleMacros/Builders/Declarations/Common/DeclBuilder.swift @@ -20,9 +20,40 @@ extension DeclBuilder { public var inheritedAccessControlLevel: TokenSyntax? { let settings = settings.accessControlLevel - return basicDeclaration.accessControlLevel( - inheritedBy: settings.inheritingDeclaration, - maxAllowed: settings.maxAllowed - ) + guard let accessControlLevel = basicDeclaration.accessControlLevel, + let index = TokenKind.accessControlLevels.firstIndex(of: accessControlLevel.tokenKind), + let maxAllowedIndex = Keyword.accessControlLevels.firstIndex(of: settings.maxAllowed) + else { + return nil + } + + guard index <= maxAllowedIndex else { + let tokenKind = TokenKind.accessControlLevels[maxAllowedIndex] + return TokenSyntax(tokenKind, presence: .present).withTrailingSpace + } + + switch settings.inheritingDeclaration { + case .member: + if let internalIndex = Keyword.accessControlLevels.firstIndex(of: .internal), + index <= internalIndex { + return nil + } + case .peer: + break + } + + return accessControlLevel.trimmed.withTrailingSpace + } + + public var inheritedGlobalActorIsolation: AttributeSyntax? { + let globalActor: AttributeSyntax? = switch settings.globalActorIsolationPreference { + case .nonisolated: + nil + case let .isolated(globalActor): + "@\(globalActor)" + case .none: + basicDeclaration.globalActor?.trimmed + } + return globalActor?.withTrailingSpace } } diff --git a/Sources/PrincipleMacros/Builders/Declarations/Common/DeclBuilderSettings.swift b/Sources/PrincipleMacros/Builders/Declarations/Common/DeclBuilderSettings.swift index 167bba8..c157be7 100644 --- a/Sources/PrincipleMacros/Builders/Declarations/Common/DeclBuilderSettings.swift +++ b/Sources/PrincipleMacros/Builders/Declarations/Common/DeclBuilderSettings.swift @@ -11,9 +11,14 @@ import SwiftSyntax public struct DeclBuilderSettings { public var accessControlLevel: AccessControlLevel + public var globalActorIsolationPreference: GlobalActorIsolationPreference? - public init(accessControlLevel: AccessControlLevel) { + public init( + accessControlLevel: AccessControlLevel, + globalActorIsolationPreference: GlobalActorIsolationPreference? = nil + ) { self.accessControlLevel = accessControlLevel + self.globalActorIsolationPreference = globalActorIsolationPreference } } diff --git a/Sources/PrincipleMacros/Builders/Declarations/Types/ActorDeclBuilder.swift b/Sources/PrincipleMacros/Builders/Declarations/Types/ActorDeclBuilder.swift new file mode 100644 index 0000000..b0eed62 --- /dev/null +++ b/Sources/PrincipleMacros/Builders/Declarations/Types/ActorDeclBuilder.swift @@ -0,0 +1,21 @@ +// +// ActorDeclBuilder.swift +// PrincipleMacros +// +// Created by Kamil Strzelecki on 18/08/2025. +// Copyright © 2025 Kamil Strzelecki. All rights reserved. +// + +import SwiftSyntax + +public protocol ActorDeclBuilder: TypeDeclBuilder { + + var declaration: ActorDeclSyntax { get } +} + +extension ActorDeclBuilder { + + public var typeDeclaration: any TypeDeclSyntax { + declaration + } +} diff --git a/Sources/PrincipleMacros/Parameters/ParameterExtractor.swift b/Sources/PrincipleMacros/Parameters/ParameterExtractor.swift index 9047c1c..f86a826 100644 --- a/Sources/PrincipleMacros/Parameters/ParameterExtractor.swift +++ b/Sources/PrincipleMacros/Parameters/ParameterExtractor.swift @@ -10,7 +10,7 @@ import SwiftSyntax public struct ParameterExtractor { - private let arguments: LabeledExprListSyntax + private let arguments: LabeledExprListSyntax? private let trailingClosure: ClosureExprSyntax? public init(from node: some FreestandingMacroExpansionSyntax) { @@ -18,20 +18,42 @@ public struct ParameterExtractor { self.trailingClosure = node.trailingClosure } - public func expression(withLabel label: TokenSyntax?) throws -> ExprSyntax { - let match = arguments.first { element in - element.label?.trimmedDescription == label?.trimmedDescription + public init(from node: AttributeSyntax) { + self.arguments = switch node.arguments { + case let .argumentList(arguments): + arguments + default: + nil } + self.trailingClosure = nil + } - guard let match else { - throw ParameterExtractionError.notFound + public func expression( + withLabel label: TokenSyntax? + ) -> ExprSyntax? { + let match = arguments?.first { element in + element.label?.trimmedDescription == label?.trimmedDescription } + return match?.expression.trimmed + } - return match.expression.trimmed + public func trailingClosure( + withLabel label: TokenSyntax? + ) throws -> ExprSyntax? { + if let trailingClosure { + return ExprSyntax(trailingClosure) + } + return expression(withLabel: label) } - public func rawString(withLabel label: TokenSyntax?) throws -> String { - let rawString = try expression(withLabel: label) + public func rawString( + withLabel label: TokenSyntax? + ) throws -> String? { + guard let expression = expression(withLabel: label) else { + return nil + } + + let rawString = expression .as(StringLiteralExprSyntax.self)? .representedLiteralValue @@ -42,10 +64,23 @@ public struct ParameterExtractor { return rawString } - public func trailingClosure(withLabel label: TokenSyntax?) throws -> ExprSyntax { - if let trailingClosure { - return ExprSyntax(trailingClosure) + public func globalActorIsolationPreference( + withLabel label: TokenSyntax? + ) throws -> GlobalActorIsolationPreference? { + guard let expression = expression(withLabel: label) else { + return nil + } + + if NilLiteralExprSyntax(expression) != nil { + return .nonisolated + } + + if let memberAccessExpression = MemberAccessExprSyntax(expression), + memberAccessExpression.declName.baseName.tokenKind == .keyword(.self), + let baseType = memberAccessExpression.base { + return .isolated("\(baseType)") } - return try expression(withLabel: label) + + throw ParameterExtractionError.unexpectedSyntaxType } } diff --git a/Sources/PrincipleMacros/Syntax/Custom/BasicDeclSyntax.swift b/Sources/PrincipleMacros/Syntax/Custom/BasicDeclSyntax.swift index ff5cc58..6f9dee2 100644 --- a/Sources/PrincipleMacros/Syntax/Custom/BasicDeclSyntax.swift +++ b/Sources/PrincipleMacros/Syntax/Custom/BasicDeclSyntax.swift @@ -9,4 +9,5 @@ import SwiftSyntax public typealias BasicDeclSyntax = DeclSyntaxProtocol + & WithAttributesSyntax & WithModifiersSyntax diff --git a/Sources/PrincipleMacros/Syntax/Custom/TypeDeclSyntax.swift b/Sources/PrincipleMacros/Syntax/Custom/TypeDeclSyntax.swift index 95153d1..c3b7e45 100644 --- a/Sources/PrincipleMacros/Syntax/Custom/TypeDeclSyntax.swift +++ b/Sources/PrincipleMacros/Syntax/Custom/TypeDeclSyntax.swift @@ -8,7 +8,7 @@ import SwiftSyntax -public protocol TypeDeclSyntax: DeclGroupSyntax, NamedDeclSyntax, WithModifiersSyntax { +public protocol TypeDeclSyntax: DeclGroupSyntax, NamedDeclSyntax, BasicDeclSyntax { var isFinal: Bool { get } } diff --git a/Sources/PrincipleMacros/Syntax/Extensions/WithModifiersSyntax.swift b/Sources/PrincipleMacros/Syntax/Extensions/WithModifiersSyntax.swift index 66ac5f1..8a1e020 100644 --- a/Sources/PrincipleMacros/Syntax/Extensions/WithModifiersSyntax.swift +++ b/Sources/PrincipleMacros/Syntax/Extensions/WithModifiersSyntax.swift @@ -41,55 +41,23 @@ extension WithModifiersSyntax { } } -extension WithModifiersSyntax { - - public func accessControlLevel( - inheritedBy inheritingDeclaration: InheritingDeclaration, - maxAllowed: Keyword - ) -> TokenSyntax? { - guard let accessControlLevel, - let index = TokenKind.accessControlLevels.firstIndex(of: accessControlLevel.tokenKind), - let maxAllowedIndex = Keyword.accessControlLevels.firstIndex(of: maxAllowed) - else { - return nil - } - - guard index <= maxAllowedIndex else { - let tokenKind = TokenKind.accessControlLevels[maxAllowedIndex] - return TokenSyntax(tokenKind, presence: .present) - } - - switch inheritingDeclaration { - case .member: - if let internalIndex = Keyword.accessControlLevels.firstIndex(of: .internal), - index <= internalIndex { - return nil - } - case .peer: - break - } - - return accessControlLevel.trimmed.withTrailingSpace - } -} - extension TokenKind { - fileprivate static let typeScopeSpecifiers = Keyword.typeScopeSpecifiers + static let typeScopeSpecifiers = Keyword.typeScopeSpecifiers .map(TokenKind.keyword) - fileprivate static let accessControlLevels = Keyword.accessControlLevels + static let accessControlLevels = Keyword.accessControlLevels .map(TokenKind.keyword) } extension Keyword { - fileprivate static let typeScopeSpecifiers: [Keyword] = [ + static let typeScopeSpecifiers: [Keyword] = [ .static, .class ] - fileprivate static let accessControlLevels: [Keyword] = [ + static let accessControlLevels: [Keyword] = [ .private, .fileprivate, .internal, diff --git a/Sources/PrincipleMacros/Syntax/Helpers/GlobalActorIsolationPreference.swift b/Sources/PrincipleMacros/Syntax/Helpers/GlobalActorIsolationPreference.swift new file mode 100644 index 0000000..d5ea051 --- /dev/null +++ b/Sources/PrincipleMacros/Syntax/Helpers/GlobalActorIsolationPreference.swift @@ -0,0 +1,15 @@ +// +// GlobalActorIsolationPreference.swift +// PrincipleMacros +// +// Created by Kamil Strzelecki on 18/08/2025. +// Copyright © 2025 Kamil Strzelecki. All rights reserved. +// + +import SwiftSyntax + +public enum GlobalActorIsolationPreference: Hashable { + + case nonisolated + case isolated(TypeSyntax) +} diff --git a/Sources/PrincipleMacros/Syntax/Helpers/InheritingDeclaration.swift b/Sources/PrincipleMacros/Syntax/Helpers/InheritingDeclaration.swift index 493e368..e9679ae 100644 --- a/Sources/PrincipleMacros/Syntax/Helpers/InheritingDeclaration.swift +++ b/Sources/PrincipleMacros/Syntax/Helpers/InheritingDeclaration.swift @@ -6,7 +6,7 @@ // Copyright © 2025 Kamil Strzelecki. All rights reserved. // -public enum InheritingDeclaration { +public enum InheritingDeclaration: Hashable { case member case peer diff --git a/Tests/PrincipleMacrosTests/Parameters/ParameterExtractorTests.swift b/Tests/PrincipleMacrosTests/Parameters/ParameterExtractorTests.swift index 4f05175..db266df 100644 --- a/Tests/PrincipleMacrosTests/Parameters/ParameterExtractorTests.swift +++ b/Tests/PrincipleMacrosTests/Parameters/ParameterExtractorTests.swift @@ -19,25 +19,35 @@ internal struct ParameterExtractorTests { @Test func testExpressionExtraction() throws { let extractor = try makeExtractor(from: "#MyMacro(value: Type.make())") - let extracted = try extractor.expression(withLabel: "value") + let extracted = extractor.expression(withLabel: "value") let expected: ExprSyntax = "Type.make()" - #expect(extracted.description == expected.description) + #expect(extracted?.description == expected.description) } @Test func testUnnamedExpressionExtraction() throws { let extractor = try makeExtractor(from: "#MyMacro(value: Type.make(), 123)") - let extracted = try extractor.expression(withLabel: nil) + let extracted = extractor.expression(withLabel: nil) let expected: ExprSyntax = "123" - #expect(extracted.description == expected.description) + #expect(extracted?.description == expected.description) } + @Test + func testMissingExpressionExtraction() throws { + let extractor = try makeExtractor(from: #"#MyMacro(arg: Type.make())"#) + let extracted = extractor.expression(withLabel: "value") + #expect(extracted == nil) + } +} + +extension ParameterExtractorTests { + @Test func testTrailingClosureExtraction() throws { let extractor = try makeExtractor(from: "#MyMacro { _ in }") let extracted = try extractor.trailingClosure(withLabel: "operation") let expected: ExprSyntax = "{ _ in }" - #expect(extracted.description == expected.description) + #expect(extracted?.description == expected.description) } @Test @@ -45,8 +55,11 @@ internal struct ParameterExtractorTests { let extractor = try makeExtractor(from: "#MyMacro(operation: perform)") let extracted = try extractor.trailingClosure(withLabel: "operation") let expected: ExprSyntax = "perform" - #expect(extracted.description == expected.description) + #expect(extracted?.description == expected.description) } +} + +extension ParameterExtractorTests { @Test func testRawStringExtraction() throws { @@ -56,18 +69,53 @@ internal struct ParameterExtractorTests { } @Test - func testUnexpectedSyntaxTypeError() throws { + func testUnexpectedSyntaxWhenPerformingRawStringExtraction() throws { let extractor = try makeExtractor(from: #"#MyMacro(string: reference.arg)"#) #expect(throws: ParameterExtractionError.unexpectedSyntaxType) { try extractor.rawString(withLabel: "string") } } +} + +extension ParameterExtractorTests { @Test - func testNotFoundError() throws { - let extractor = try makeExtractor(from: #"#MyMacro(string: "arg")"#) - #expect(throws: ParameterExtractionError.notFound) { - try extractor.rawString(withLabel: "value") + func testMissingGlobalActorPreferenceExtraction() throws { + let extractor = try makeExtractor(from: "#MyMacro()") + let extracted = try extractor.globalActorIsolationPreference(withLabel: "isolation") + #expect(extracted == nil) + } + + @Test + func testNonisolatedGlobalActorPreferenceExtraction() throws { + let extractor = try makeExtractor(from: "#MyMacro(isolation: nil)") + let extracted = try extractor.globalActorIsolationPreference(withLabel: "isolation") + #expect(extracted == .nonisolated) + } + + @Test( + arguments: [ + "MainActor", + "SomeType.SomeActor" + ] + ) + func testIsolatedGlobalActorPreferenceExtraction(isolation: String) throws { + let extractor = try makeExtractor(from: "#MyMacro(isolation: \(raw: isolation).self)") + let extracted = try extractor.globalActorIsolationPreference(withLabel: "isolation") + + switch extracted { + case let .isolated(globalActor): + #expect(globalActor.description == isolation) + default: + Issue.record() + } + } + + @Test + func testUnexpectedSyntaxWhenPerformingGlobalActorPreferenceExtraction() throws { + let extractor = try makeExtractor(from: #"#MyMacro(isolation: MainActor.Type)"#) + #expect(throws: ParameterExtractionError.unexpectedSyntaxType) { + try extractor.globalActorIsolationPreference(withLabel: "isolation") } } }