Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
61 changes: 48 additions & 13 deletions Sources/PrincipleMacros/Parameters/ParameterExtractor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,50 @@ import SwiftSyntax

public struct ParameterExtractor {

private let arguments: LabeledExprListSyntax
private let arguments: LabeledExprListSyntax?
private let trailingClosure: ClosureExprSyntax?

public init(from node: some FreestandingMacroExpansionSyntax) {
self.arguments = node.arguments
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

Expand All @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@
import SwiftSyntax

public typealias BasicDeclSyntax = DeclSyntaxProtocol
& WithAttributesSyntax
& WithModifiersSyntax
2 changes: 1 addition & 1 deletion Sources/PrincipleMacros/Syntax/Custom/TypeDeclSyntax.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import SwiftSyntax

public protocol TypeDeclSyntax: DeclGroupSyntax, NamedDeclSyntax, WithModifiersSyntax {
public protocol TypeDeclSyntax: DeclGroupSyntax, NamedDeclSyntax, BasicDeclSyntax {

var isFinal: Bool { get }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
Comment thread
NSFatalError marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
// Copyright © 2025 Kamil Strzelecki. All rights reserved.
//

public enum InheritingDeclaration {
public enum InheritingDeclaration: Hashable {

case member
case peer
Expand Down
70 changes: 59 additions & 11 deletions Tests/PrincipleMacrosTests/Parameters/ParameterExtractorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,34 +19,47 @@ 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
func testTrailingClosureReferenceExtraction() throws {
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 {
Expand All @@ -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()
}
}
Comment thread
NSFatalError marked this conversation as resolved.

@Test
func testUnexpectedSyntaxWhenPerformingGlobalActorPreferenceExtraction() throws {
let extractor = try makeExtractor(from: #"#MyMacro(isolation: MainActor.Type)"#)
#expect(throws: ParameterExtractionError.unexpectedSyntaxType) {
try extractor.globalActorIsolationPreference(withLabel: "isolation")
}
}
}
Loading