diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index b6e8af2..ac7ec49 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -12,7 +12,7 @@ concurrency: cancel-in-progress: true env: - XCODE_VERSION: "26.1" + XCODE_VERSION: "26.0" jobs: prepare: @@ -88,16 +88,16 @@ jobs: destination="platform=macOS,variant=Mac Catalyst" ;; ios) - destination="platform=iOS Simulator,name=iPhone 17 Pro Max,OS=26.1" + destination="platform=iOS Simulator,name=iPhone 17 Pro Max,OS=26.0.1" ;; tvos) - destination="platform=tvOS Simulator,name=Apple TV 4K (3rd generation),OS=26.1" + destination="platform=tvOS Simulator,name=Apple TV 4K (3rd generation),OS=26.0" ;; watchos) - destination="platform=watchOS Simulator,name=Apple Watch Series 11 (46mm),OS=26.1" + destination="platform=watchOS Simulator,name=Apple Watch Series 11 (46mm),OS=26.0" ;; visionos) - destination="platform=visionOS Simulator,name=Apple Vision Pro,OS=26.1" + destination="platform=visionOS Simulator,name=Apple Vision Pro,OS=26.0" ;; *) echo "Unknown platform: ${{ matrix.platform }}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b076f96..80556d9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,7 +6,7 @@ on: - '*' env: - XCODE_VERSION: "26.1" + XCODE_VERSION: "26.0" jobs: release: diff --git a/.swiftlint.yml b/.swiftlint.yml index fcb78ed..a98bea7 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -27,7 +27,7 @@ opt_in_rules: - discouraged_assert - discouraged_none_name - discouraged_object_literal - - discouraged_optional_boolean + # discouraged_optional_boolean - discouraged_optional_collection - empty_collection_literal - empty_count @@ -67,7 +67,7 @@ opt_in_rules: - local_doc_comment - lower_acl_than_parent # missing_docs - - modifier_order + # modifier_order - multiline_arguments - multiline_arguments_brackets - multiline_function_chains diff --git a/Sources/PrincipleMacros/Builders/Declarations/Common/DeclBuilder.swift b/Sources/PrincipleMacros/Builders/Declarations/Common/DeclBuilder.swift index fc0dda1..b67b740 100644 --- a/Sources/PrincipleMacros/Builders/Declarations/Common/DeclBuilder.swift +++ b/Sources/PrincipleMacros/Builders/Declarations/Common/DeclBuilder.swift @@ -15,7 +15,6 @@ public protocol DeclBuilder { var preferredGlobalActorIsolation: GlobalActorIsolation? { get } var preferredAccessControlLevel: AccessControlLevel? { get } - var maxAllowedAccessControlLevel: AccessControlLevel { get } func build() throws -> [DeclSyntax] } @@ -34,7 +33,7 @@ extension DeclBuilder { nil } - public var maxAllowedAccessControlLevel: AccessControlLevel { - .public + public var inheritedAvailability: AttributeListSyntax? { + basicDeclaration.availability?.trimmed.withTrailingNewline } } diff --git a/Sources/PrincipleMacros/Builders/Declarations/Common/MemberBuilding.swift b/Sources/PrincipleMacros/Builders/Declarations/Common/MemberBuilding.swift index 2eae3db..ac72172 100644 --- a/Sources/PrincipleMacros/Builders/Declarations/Common/MemberBuilding.swift +++ b/Sources/PrincipleMacros/Builders/Declarations/Common/MemberBuilding.swift @@ -11,10 +11,17 @@ public protocol MemberBuilding {} extension MemberBuilding where Self: TypeDeclBuilder { public var inheritedAccessControlLevel: AccessControlLevel? { + .forMember( + of: typeDeclaration, + preferred: preferredAccessControlLevel + ) + } + + public var inheritedAccessControlLevelAllowingOpen: AccessControlLevel? { .forMember( of: typeDeclaration, preferred: preferredAccessControlLevel, - maxAllowed: maxAllowedAccessControlLevel + maxAllowed: .open ) } } diff --git a/Sources/PrincipleMacros/Builders/Declarations/Common/PeerBuilding.swift b/Sources/PrincipleMacros/Builders/Declarations/Common/PeerBuilding.swift index 17d6ece..9eb31d3 100644 --- a/Sources/PrincipleMacros/Builders/Declarations/Common/PeerBuilding.swift +++ b/Sources/PrincipleMacros/Builders/Declarations/Common/PeerBuilding.swift @@ -11,10 +11,17 @@ public protocol PeerBuilding {} extension PeerBuilding where Self: DeclBuilder { public var inheritedAccessControlLevel: AccessControlLevel? { + .forPeer( + of: basicDeclaration, + preferred: preferredAccessControlLevel + ) + } + + public var inheritedAccessControlLevelAllowingOpen: AccessControlLevel? { .forPeer( of: basicDeclaration, preferred: preferredAccessControlLevel, - maxAllowed: maxAllowedAccessControlLevel + maxAllowed: .open ) } } diff --git a/Sources/PrincipleMacros/Builders/Declarations/Members/PropertyDeclAccessorBuilder.swift b/Sources/PrincipleMacros/Builders/Declarations/Members/PropertyDeclAccessorBuilder.swift new file mode 100644 index 0000000..9bde488 --- /dev/null +++ b/Sources/PrincipleMacros/Builders/Declarations/Members/PropertyDeclAccessorBuilder.swift @@ -0,0 +1,21 @@ +// +// PropertyDeclAccessorBuilder.swift +// PrincipleMacros +// +// Created by Kamil Strzelecki on 28/11/2025. +// Copyright © 2025 Kamil Strzelecki. All rights reserved. +// + +import SwiftSyntaxMacros + +public protocol PropertyDeclAccessorBuilder: PropertyDeclBuilder { + + func buildAccessors() throws -> [AccessorDeclSyntax] +} + +extension PropertyDeclAccessorBuilder { + + public func build() throws -> [DeclSyntax] { + try buildAccessors().map(DeclSyntax.init) + } +} diff --git a/Sources/PrincipleMacros/Builders/Declarations/Types/ClassDeclBuilder.swift b/Sources/PrincipleMacros/Builders/Declarations/Types/ClassDeclBuilder.swift index e45daf4..dfd7787 100644 --- a/Sources/PrincipleMacros/Builders/Declarations/Types/ClassDeclBuilder.swift +++ b/Sources/PrincipleMacros/Builders/Declarations/Types/ClassDeclBuilder.swift @@ -11,6 +11,7 @@ import SwiftSyntaxMacros public protocol ClassDeclBuilder: TypeDeclBuilder { var declaration: ClassDeclSyntax { get } + var trimmedSuperclassType: TypeSyntax? { get } } extension ClassDeclBuilder { @@ -19,3 +20,20 @@ extension ClassDeclBuilder { declaration } } + +extension ClassDeclBuilder { + + public var trimmedSuperclassType: TypeSyntax? { + nil + } + + public var inheritedOverrideModifier: TokenSyntax? { + trimmedSuperclassType != nil + ? TokenSyntax(.keyword(.override), presence: .present).withTrailingSpace + : nil + } + + public var inheritedFinalModifier: TokenSyntax? { + declaration.finalSpecifier?.trimmed.withTrailingSpace + } +} diff --git a/Sources/PrincipleMacros/Diagnostics/DiagnosticsError.swift b/Sources/PrincipleMacros/Diagnostics/DiagnosticsError.swift new file mode 100644 index 0000000..d251f7d --- /dev/null +++ b/Sources/PrincipleMacros/Diagnostics/DiagnosticsError.swift @@ -0,0 +1,22 @@ +// +// DiagnosticsError.swift +// PrincipleMacros +// +// Created by Kamil Strzelecki on 21/11/2025. +// Copyright © 2025 Kamil Strzelecki. All rights reserved. +// + +import SwiftSyntaxMacros + +extension DiagnosticsError { + + public init( + node: some SyntaxProtocol, + message: String, + fixIts: [FixIt] = [] + ) { + let message = MacroExpansionErrorMessage(message) + let diagnostic = Diagnostic(node: node, message: message, fixIts: fixIts) + self.init(diagnostics: [diagnostic]) + } +} diff --git a/Sources/PrincipleMacros/ExpansionContext/MacroExpansionContext.swift b/Sources/PrincipleMacros/Diagnostics/MacroExpansionContext.swift similarity index 81% rename from Sources/PrincipleMacros/ExpansionContext/MacroExpansionContext.swift rename to Sources/PrincipleMacros/Diagnostics/MacroExpansionContext.swift index 3e3b5cc..1247afe 100644 --- a/Sources/PrincipleMacros/ExpansionContext/MacroExpansionContext.swift +++ b/Sources/PrincipleMacros/Diagnostics/MacroExpansionContext.swift @@ -12,19 +12,21 @@ extension MacroExpansionContext { public func diagnose( node: some SyntaxProtocol, - errorMessage: String + errorMessage: String, + fixIts: [FixIt] = [] ) { let message = MacroExpansionErrorMessage(errorMessage) - let diagnostic = Diagnostic(node: node, message: message) + let diagnostic = Diagnostic(node: node, message: message, fixIts: fixIts) diagnose(diagnostic) } public func diagnose( node: some SyntaxProtocol, - warningMessage: String + warningMessage: String, + fixIts: [FixIt] = [] ) { let message = MacroExpansionWarningMessage(warningMessage) - let diagnostic = Diagnostic(node: node, message: message) + let diagnostic = Diagnostic(node: node, message: message, fixIts: fixIts) diagnose(diagnostic) } } diff --git a/Sources/PrincipleMacros/Parameters/ParameterExtractor.swift b/Sources/PrincipleMacros/Parameters/ParameterExtractor.swift index 2b16262..b8896bc 100644 --- a/Sources/PrincipleMacros/Parameters/ParameterExtractor.swift +++ b/Sources/PrincipleMacros/Parameters/ParameterExtractor.swift @@ -121,6 +121,32 @@ extension ParameterExtractor { } } +extension ParameterExtractor { + + public func rawBool( + withLabel label: TokenSyntax? + ) throws -> Bool? { + guard let expression = expression(withLabel: label) else { + return nil + } + + guard let bool = expression.as(BooleanLiteralExprSyntax.self) else { + throw ParameterExtractionError.unexpectedSyntaxType + } + + return bool.literal.tokenKind == .keyword(.true) + } + + public func requiredRawBool( + withLabel label: TokenSyntax? + ) throws -> Bool { + guard let bool = try rawBool(withLabel: label) else { + throw ParameterExtractionError.missingRequirement + } + return bool + } +} + extension ParameterExtractor { public func rawString( @@ -160,18 +186,20 @@ extension ParameterExtractor { return nil } - if NilLiteralExprSyntax(expression) != nil { + if expression.is(NilLiteralExprSyntax.self) { let isolation = DeclModifierSyntax(name: .keyword(.nonisolated)) return .nonisolated(trimmedModifer: isolation) } - if let memberAccessExpression = MemberAccessExprSyntax(expression), - let explicitType = memberAccessExpression.base?.inferredType, - memberAccessExpression.referencesBaseType { - return .isolated(standardizedType: explicitType.standardized) + guard let memberAccessExpression = MemberAccessExprSyntax(expression), + let globalActorType = memberAccessExpression.baseTypeReference + else { + throw ParameterExtractionError.unexpectedSyntaxType } - throw ParameterExtractionError.unexpectedSyntaxType + return .isolated( + standardizedType: globalActorType.standardized + ) } public func requiredGlobalActorIsolation( @@ -183,3 +211,31 @@ extension ParameterExtractor { return isolation } } + +extension ParameterExtractor { + + public func type( + withLabel label: TokenSyntax? + ) throws -> TypeSyntax? { + guard let expression = expression(withLabel: label) else { + return nil + } + + guard let memberAccessExpression = MemberAccessExprSyntax(expression), + let type = memberAccessExpression.baseTypeReference + else { + throw ParameterExtractionError.unexpectedSyntaxType + } + + return type + } + + public func requiredType( + withLabel label: TokenSyntax? + ) throws -> TypeSyntax { + guard let type = try type(withLabel: label) else { + throw ParameterExtractionError.missingRequirement + } + return type + } +} diff --git a/Sources/PrincipleMacros/Parsers/Common/Parser.swift b/Sources/PrincipleMacros/Parsers/Common/Parser.swift index 5e450e5..d523dcc 100644 --- a/Sources/PrincipleMacros/Parsers/Common/Parser.swift +++ b/Sources/PrincipleMacros/Parsers/Common/Parser.swift @@ -13,12 +13,44 @@ public protocol Parser { associatedtype ResultsCollection: ParserResultsCollection static func parse( - declaration: some DeclSyntaxProtocol, - in context: some MacroExpansionContext - ) -> ResultsCollection + declaration: some DeclSyntaxProtocol + ) throws -> ResultsCollection +} - static func parse( - memberBlock: MemberBlockSyntax, - in context: some MacroExpansionContext - ) -> ResultsCollection +extension Parser { + + public static func parse( + ifConfig: IfConfigDeclSyntax + ) throws -> ResultsCollection { + try ResultsCollection( + ifConfig.clauses.flatMap { clause in + switch clause.elements { + case let .decls(members): + try parse(members: members) + default: + ResultsCollection() + } + } + ) + } + + public static func parse( + members: MemberBlockItemListSyntax + ) throws -> ResultsCollection { + try ResultsCollection( + members.flatMap { member in + if let ifConfig = member.decl.as(IfConfigDeclSyntax.self) { + try parse(ifConfig: ifConfig) + } else { + try parse(declaration: member.decl) + } + } + ) + } + + public static func parse( + memberBlock: MemberBlockSyntax + ) throws -> ResultsCollection { + try parse(members: memberBlock.members) + } } diff --git a/Sources/PrincipleMacros/Parsers/Common/ParserResultsCollection.swift b/Sources/PrincipleMacros/Parsers/Common/ParserResultsCollection.swift index 9b39016..bc64de4 100644 --- a/Sources/PrincipleMacros/Parsers/Common/ParserResultsCollection.swift +++ b/Sources/PrincipleMacros/Parsers/Common/ParserResultsCollection.swift @@ -12,6 +12,8 @@ where Element: ParserResult { associatedtype Element var all: [Element] { get } + + init(_ all: [Element]) } extension ParserResultsCollection { @@ -28,3 +30,16 @@ extension ParserResultsCollection { all[position] } } + +extension ParserResultsCollection { + + public init() { + self.init([]) + } + + public func filter( + _ isIncluded: (Element) throws -> Bool + ) rethrows -> Self { + try Self(filter(isIncluded)) + } +} diff --git a/Sources/PrincipleMacros/Parsers/Common/_Parser.swift b/Sources/PrincipleMacros/Parsers/Common/_Parser.swift deleted file mode 100644 index 4b16f81..0000000 --- a/Sources/PrincipleMacros/Parsers/Common/_Parser.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// _Parser.swift -// PrincipleMacros -// -// Created by Kamil Strzelecki on 22/01/2025. -// Copyright © 2025 Kamil Strzelecki. All rights reserved. -// - -import SwiftSyntaxMacros - -internal protocol _Parser: Parser -where ResultsCollection: _ParserResultsCollection {} - -extension _Parser { - - public static func parse( - memberBlock: MemberBlockSyntax, - in context: some MacroExpansionContext - ) -> ResultsCollection { - ResultsCollection( - memberBlock.members.flatMap { member in - parse(declaration: member.decl, in: context) - } - ) - } -} diff --git a/Sources/PrincipleMacros/Parsers/Common/_ParserResultsCollection.swift b/Sources/PrincipleMacros/Parsers/Common/_ParserResultsCollection.swift deleted file mode 100644 index 571d1d6..0000000 --- a/Sources/PrincipleMacros/Parsers/Common/_ParserResultsCollection.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// _ParserResultsCollection.swift -// PrincipleMacros -// -// Created by Kamil Strzelecki on 22/01/2025. -// Copyright © 2025 Kamil Strzelecki. All rights reserved. -// - -internal protocol _ParserResultsCollection: ParserResultsCollection { - - init(_ all: [Element]) -} - -extension _ParserResultsCollection { - - init() { - self.init([]) - } -} diff --git a/Sources/PrincipleMacros/Parsers/EnumCases/EnumCasesList.swift b/Sources/PrincipleMacros/Parsers/EnumCases/EnumCasesList.swift index cb84d39..064e32f 100644 --- a/Sources/PrincipleMacros/Parsers/EnumCases/EnumCasesList.swift +++ b/Sources/PrincipleMacros/Parsers/EnumCases/EnumCasesList.swift @@ -8,11 +8,11 @@ import SwiftSyntaxMacros -public struct EnumCasesList: _ParserResultsCollection { +public struct EnumCasesList: ParserResultsCollection { public let all: [EnumCase] - init(_ all: [EnumCase]) { + public init(_ all: [EnumCase]) { self.all = all } } diff --git a/Sources/PrincipleMacros/Parsers/EnumCases/EnumCasesParser.swift b/Sources/PrincipleMacros/Parsers/EnumCases/EnumCasesParser.swift index b9cd9d3..09d896a 100644 --- a/Sources/PrincipleMacros/Parsers/EnumCases/EnumCasesParser.swift +++ b/Sources/PrincipleMacros/Parsers/EnumCases/EnumCasesParser.swift @@ -8,11 +8,10 @@ import SwiftSyntaxMacros -public enum EnumCasesParser: _Parser { +public enum EnumCasesParser: Parser { public static func parse( - declaration: some DeclSyntaxProtocol, - in _: some MacroExpansionContext + declaration: some DeclSyntaxProtocol ) -> EnumCasesList { guard let declaration = EnumCaseDeclSyntax(declaration) else { return .init() diff --git a/Sources/PrincipleMacros/Parsers/Properties/PropertiesList.swift b/Sources/PrincipleMacros/Parsers/Properties/PropertiesList.swift index 65d1a7b..25b713b 100644 --- a/Sources/PrincipleMacros/Parsers/Properties/PropertiesList.swift +++ b/Sources/PrincipleMacros/Parsers/Properties/PropertiesList.swift @@ -8,11 +8,11 @@ import SwiftSyntaxMacros -public struct PropertiesList: _ParserResultsCollection { +public struct PropertiesList: ParserResultsCollection { public let all: [Property] - init(_ all: [Property]) { + public init(_ all: [Property]) { self.all = all } } diff --git a/Sources/PrincipleMacros/Parsers/Properties/PropertiesParser.swift b/Sources/PrincipleMacros/Parsers/Properties/PropertiesParser.swift index 797ac3a..106cc98 100644 --- a/Sources/PrincipleMacros/Parsers/Properties/PropertiesParser.swift +++ b/Sources/PrincipleMacros/Parsers/Properties/PropertiesParser.swift @@ -8,32 +8,29 @@ import SwiftSyntaxMacros -public enum PropertiesParser: _Parser { +public enum PropertiesParser: Parser { public static func parse( - declaration: some DeclSyntaxProtocol, - in context: some MacroExpansionContext - ) -> PropertiesList { + declaration: some DeclSyntaxProtocol + ) throws -> PropertiesList { guard let declaration = VariableDeclSyntax(declaration) else { return .init() } - return PropertiesList( + return try PropertiesList( declaration.bindings.compactMap { binding -> Property? in guard let name = binding.name else { - context.diagnose( + throw DiagnosticsError( node: declaration, - errorMessage: "Property cannot be parsed" + message: "Property cannot be parsed" ) - return nil } guard let inferredType = binding.inferredType else { - context.diagnose( + throw DiagnosticsError( node: declaration, - errorMessage: "Type of property cannot be inferred - provide it explicitly" + message: "Type of property cannot be inferred - provide it explicitly" ) - return nil } return Property( diff --git a/Sources/PrincipleMacros/Syntax/Concepts/AccessControlLevel.swift b/Sources/PrincipleMacros/Syntax/Concepts/AccessControlLevel.swift index cdae34f..105254a 100644 --- a/Sources/PrincipleMacros/Syntax/Concepts/AccessControlLevel.swift +++ b/Sources/PrincipleMacros/Syntax/Concepts/AccessControlLevel.swift @@ -75,6 +75,37 @@ extension AccessControlLevel: Comparable { } } +extension AccessControlLevel { + + public func inheritedByMember( + maxAllowed: Self = .public + ) -> Self? { + switch self { + case .private: + nil + default: + min(self, maxAllowed) + } + } + + public func inheritedBySibling( + maxAllowed: Self = .public + ) -> Self { + switch self { + case .private: + min(.fileprivate, maxAllowed) + default: + min(self, maxAllowed) + } + } + + public func inheritedByPeer( + maxAllowed: Self = .public + ) -> Self { + min(self, maxAllowed) + } +} + extension AccessControlLevel { public static func forMember( @@ -86,7 +117,7 @@ extension AccessControlLevel { from: declaration.accessControlLevel, preferred: preferred, maxAllowed: maxAllowed, - transform: { $0 == .private ? nil : $0 } + transform: { $0.inheritedByMember(maxAllowed: maxAllowed) } ) } @@ -99,7 +130,7 @@ extension AccessControlLevel { from: syntax.accessControlLevel, preferred: preferred, maxAllowed: maxAllowed, - transform: { $0 == .private ? .fileprivate : $0 } + transform: { $0.inheritedBySibling(maxAllowed: maxAllowed) } ) } @@ -111,7 +142,8 @@ extension AccessControlLevel { _resolved( from: syntax.accessControlLevel, preferred: preferred, - maxAllowed: maxAllowed + maxAllowed: maxAllowed, + transform: { $0.inheritedByPeer(maxAllowed: maxAllowed) } ) } @@ -119,13 +151,12 @@ extension AccessControlLevel { from attached: Self?, preferred: Self?, maxAllowed: Self, - transform: (Self) -> Self? = \.self + transform: (Self) -> Self? ) -> Self? { if let preferred { return min(preferred, maxAllowed) } - if var attached { - attached = min(attached, maxAllowed) + if let attached { return transform(attached) } return nil diff --git a/Sources/PrincipleMacros/Syntax/Custom/ClosureTypeSyntax.swift b/Sources/PrincipleMacros/Syntax/Concepts/ClosureTypeSyntax.swift similarity index 100% rename from Sources/PrincipleMacros/Syntax/Custom/ClosureTypeSyntax.swift rename to Sources/PrincipleMacros/Syntax/Concepts/ClosureTypeSyntax.swift diff --git a/Sources/PrincipleMacros/Syntax/Concepts/GlobalActorIsolation.swift b/Sources/PrincipleMacros/Syntax/Concepts/GlobalActorIsolation.swift index ed5a70b..bd62dd0 100644 --- a/Sources/PrincipleMacros/Syntax/Concepts/GlobalActorIsolation.swift +++ b/Sources/PrincipleMacros/Syntax/Concepts/GlobalActorIsolation.swift @@ -32,10 +32,10 @@ public enum GlobalActorIsolation { } public var standardizedIsolationAttribute: AttributeSyntax? { - guard let standardizedIsolationType else { - return nil + if let standardizedIsolationType { + return AttributeSyntax(attributeName: standardizedIsolationType) } - return AttributeSyntax(attributeName: standardizedIsolationType) + return nil } } diff --git a/Sources/PrincipleMacros/Syntax/Extensions/AttributeListSyntax.swift b/Sources/PrincipleMacros/Syntax/Extensions/AttributeListSyntax.swift deleted file mode 100644 index b24ed65..0000000 --- a/Sources/PrincipleMacros/Syntax/Extensions/AttributeListSyntax.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// AttributeListSyntax.swift -// PrincipleMacros -// -// Created by Kamil Strzelecki on 14/11/2025. -// Copyright © 2025 Kamil Strzelecki. All rights reserved. -// - -import SwiftSyntaxMacros - -extension AttributeListSyntax { - - public var attributeElements: some Collection { - lazy.compactMap { element in - switch element { - case let .attribute(attribute): - attribute - default: - nil - } - } - } - - public func first(like other: AttributeSyntax) -> AttributeSyntax? { - attributeElements.first { $0.isLike(other) } - } - - public func contains(like other: AttributeSyntax) -> Bool { - first(like: other) != nil - } - - public func contains(likeOneOf other: AttributeSyntax...) -> Bool { - attributeElements.contains { attribute in - other.contains { attribute.isLike($0) } - } - } -} diff --git a/Sources/PrincipleMacros/Syntax/Extensions/AttributeSyntax.swift b/Sources/PrincipleMacros/Syntax/Extensions/AttributeSyntax.swift deleted file mode 100644 index abddf7d..0000000 --- a/Sources/PrincipleMacros/Syntax/Extensions/AttributeSyntax.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// AttributeSyntax.swift -// PrincipleMacros -// -// Created by Kamil Strzelecki on 15/11/2025. -// Copyright © 2025 Kamil Strzelecki. All rights reserved. -// - -import SwiftSyntaxMacros - -extension AttributeSyntax { - - public func isLike(_ other: AttributeSyntax) -> Bool { - attributeName.isLike(other.attributeName) - } -} diff --git a/Sources/PrincipleMacros/Syntax/Extensions/ClassDeclSyntax.swift b/Sources/PrincipleMacros/Syntax/Extensions/ClassDeclSyntax.swift new file mode 100644 index 0000000..b988f8f --- /dev/null +++ b/Sources/PrincipleMacros/Syntax/Extensions/ClassDeclSyntax.swift @@ -0,0 +1,58 @@ +// +// ClassDeclSyntax.swift +// PrincipleMacros +// +// Created by Kamil Strzelecki on 21/11/2025. +// Copyright © 2025 Kamil Strzelecki. All rights reserved. +// + +import SwiftSyntaxMacros + +extension ClassDeclSyntax { + + public var possibleSuperclassType: TypeSyntax? { + inheritanceClause?.inheritedTypes.first?.type.trimmed + } + + public func inferredSuperclassType() -> TypeSyntax? { + let inferrer = SuperclassTypeInferrer(for: self) + return inferrer.infer() + } +} + +extension ClassDeclSyntax { + + private final class SuperclassTypeInferrer: SyntaxVisitor { + + private let classDecl: ClassDeclSyntax + private var didFind = false + + init(for classDecl: ClassDeclSyntax) { + self.classDecl = classDecl + super.init(viewMode: .sourceAccurate) + } + + func infer() -> TypeSyntax? { + if let superclassType = classDecl.possibleSuperclassType { + walk(classDecl) + return didFind ? superclassType : nil + } else { + return nil + } + } + + override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind { + node == classDecl ? .visitChildren : .skipChildren + } + + override func visit(_ node: DeclModifierSyntax) -> SyntaxVisitorContinueKind { + didFind = didFind || node.overrideSpecifier != nil + return .visitChildren + } + + override func visit(_: SuperExprSyntax) -> SyntaxVisitorContinueKind { + didFind = true + return .visitChildren + } + } +} diff --git a/Sources/PrincipleMacros/Syntax/Extensions/ExprSyntax.swift b/Sources/PrincipleMacros/Syntax/Extensions/ExprSyntax.swift index 0153a3d..c6b0190 100644 --- a/Sources/PrincipleMacros/Syntax/Extensions/ExprSyntax.swift +++ b/Sources/PrincipleMacros/Syntax/Extensions/ExprSyntax.swift @@ -66,20 +66,24 @@ extension StringLiteralExprSyntax { extension OptionalChainingExprSyntax { public var inferredType: TypeSyntax? { - guard let inferredType = expression.inferredType else { - return nil + if let inferredWrappedType { + return "Optional<\(inferredWrappedType)>" } - return "Optional<\(inferredType)>" + return nil + } + + public var inferredWrappedType: TypeSyntax? { + expression.inferredType } } extension ArrayExprSyntax { public var inferredType: TypeSyntax? { - guard let inferredElementType else { - return nil + if let inferredElementType { + return "Array<\(inferredElementType)>" } - return "Array<\(inferredElementType)>" + return nil } public var inferredElementType: TypeSyntax? { @@ -90,10 +94,10 @@ extension ArrayExprSyntax { extension DictionaryExprSyntax { public var inferredType: TypeSyntax? { - guard let inferredKeyType, let inferredValueType else { - return nil + if let inferredKeyType, let inferredValueType { + return "Dictionary<\(inferredKeyType), \(inferredValueType)>" } - return "Dictionary<\(inferredKeyType), \(inferredValueType)>" + return nil } public var inferredKeyType: TypeSyntax? { @@ -125,19 +129,15 @@ extension FunctionCallExprSyntax { extension GenericSpecializationExprSyntax { public var inferredType: TypeSyntax? { - guard let inferredType = expression.inferredType else { - return nil + if let inferredType = expression.inferredType { + return "\(inferredType)\(genericArgumentClause.standardized)" } - return "\(inferredType)\(genericArgumentClause.standardized)" + return nil } } extension MemberAccessExprSyntax { - public var referencesBaseType: Bool { - declName.baseName.tokenKind == .keyword(.self) - } - public var inferredType: TypeSyntax? { guard let first = base?.inferredType else { return nil @@ -147,6 +147,13 @@ extension MemberAccessExprSyntax { } return first } + + public var baseTypeReference: TypeSyntax? { + if let base, declName.baseName.tokenKind == .keyword(.self) { + return "\(base.trimmed)" + } + return nil + } } extension DeclReferenceExprSyntax { diff --git a/Sources/PrincipleMacros/Syntax/Extensions/IfConfigDeclSyntax+Availability.swift b/Sources/PrincipleMacros/Syntax/Extensions/IfConfigDeclSyntax+Availability.swift new file mode 100644 index 0000000..a071284 --- /dev/null +++ b/Sources/PrincipleMacros/Syntax/Extensions/IfConfigDeclSyntax+Availability.swift @@ -0,0 +1,59 @@ +// +// IfConfigDeclSyntax+Availability.swift +// PrincipleMacros +// +// Created by Kamil Strzelecki on 21/11/2025. +// Copyright © 2025 Kamil Strzelecki. All rights reserved. +// + +import SwiftSyntaxMacros + +extension IfConfigDeclSyntax { + + public var availability: Self? { + var elements = [IfConfigClauseSyntax]() + var isEmpty = true + + for clause in clauses { + if let availability = clause.availability { + elements.append(availability) + isEmpty = false + } else { + elements.append(clause.with(\.elements, .attributes([]))) + } + } + + guard !isEmpty else { + return nil + } + + let clauses = IfConfigClauseListSyntax(elements) + return with(\.clauses, clauses) + } +} + +extension IfConfigClauseSyntax { + + public var availability: Self? { + if let availability = elements?.availability { + with(\.elements, availability) + } else { + nil + } + } +} + +extension IfConfigClauseSyntax.Elements { + + public var availability: Self? { + switch self { + case let .attributes(attributes): + if let availability = attributes.availability { + return .attributes(availability) + } + default: + break + } + return nil + } +} diff --git a/Sources/PrincipleMacros/Syntax/Extensions/IfConfigDeclSyntax+EnclosingIfConfig.swift b/Sources/PrincipleMacros/Syntax/Extensions/IfConfigDeclSyntax+EnclosingIfConfig.swift new file mode 100644 index 0000000..13bbba5 --- /dev/null +++ b/Sources/PrincipleMacros/Syntax/Extensions/IfConfigDeclSyntax+EnclosingIfConfig.swift @@ -0,0 +1,117 @@ +// +// IfConfigDeclSyntax+EnclosingIfConfig.swift +// PrincipleMacros +// +// Created by Kamil Strzelecki on 21/11/2025. +// Copyright © 2025 Kamil Strzelecki. All rights reserved. +// + +import SwiftSyntax + +extension IfConfigDeclSyntax { + + fileprivate var aligned: Self { + with(\.poundEndif, .poundEndifToken(leadingTrivia: .newline)) + } + + public var enclosingIfConfig: IfConfigDeclSyntax? { + if let parent = aligned.parent?.as(MemberBlockItemSyntax.self) { + return parent.enclosingIfConfig + } + return nil + } +} + +extension IfConfigClauseListSyntax { + + public var enclosingIfConfig: IfConfigDeclSyntax? { + if let parent = parent?.as(IfConfigDeclSyntax.self) { + return parent.enclosingIfConfig ?? parent.aligned + } + return nil + } +} + +extension IfConfigClauseSyntax { + + private var aligned: Self { + with(\.poundKeyword, poundKeyword.trimmed.withTrailingSpace) + } + + public var enclosingIfConfig: IfConfigDeclSyntax? { + guard var parent = parent?.as(IfConfigClauseListSyntax.self) else { + return nil + } + + for (index, clause) in zip(parent.indices, parent) { + parent[index] = if clause == self { + clause.aligned + } else { + clause.aligned + .with(\.elements, .decls([])) + .withTrailingNewline + } + } + + return parent.enclosingIfConfig + } +} + +extension MemberBlockItemListSyntax { + + public var enclosingIfConfig: IfConfigDeclSyntax? { + if let parent = parent?.as(IfConfigClauseSyntax.self) { + return parent.enclosingIfConfig + } + return nil + } +} + +extension MemberBlockItemSyntax { + + public var enclosingIfConfig: IfConfigDeclSyntax? { + guard var parent = parent?.as(MemberBlockItemListSyntax.self) else { + return nil + } + + parent.replaceSubrange( + parent.startIndex ..< parent.endIndex, + with: CollectionOfOne(trimmed.withLeadingNewline) + ) + + return parent.enclosingIfConfig + } +} + +extension DeclSyntaxProtocol { + + public var enclosingIfConfig: IfConfigDeclSyntax? { + if let parent = parent?.as(MemberBlockItemSyntax.self) { + return parent.enclosingIfConfig + } + return nil + } + + public func applyingEnclosingIfConfig( + to members: MemberBlockItemListSyntax + ) -> IfConfigDeclSyntax? { + applyingEnclosingIfConfig(to: .decls(members.withLeadingNewline)) + } + + public func applyingEnclosingIfConfig( + to statements: CodeBlockItemListSyntax + ) -> IfConfigDeclSyntax? { + applyingEnclosingIfConfig(to: .statements(statements.withLeadingNewline)) + } + + private func applyingEnclosingIfConfig( + to elements: IfConfigClauseSyntax.Elements + ) -> IfConfigDeclSyntax? { + if var ancestor = parent?.parent?.parent?.as(IfConfigClauseSyntax.self) { + ancestor = ancestor.with(\.elements, elements) + return ancestor.enclosingIfConfig + } else { + return nil + } + } +} diff --git a/Sources/PrincipleMacros/Syntax/Extensions/SyntaxProtocol.swift b/Sources/PrincipleMacros/Syntax/Extensions/SyntaxProtocol.swift index 86e6fdd..ac95b7c 100644 --- a/Sources/PrincipleMacros/Syntax/Extensions/SyntaxProtocol.swift +++ b/Sources/PrincipleMacros/Syntax/Extensions/SyntaxProtocol.swift @@ -33,4 +33,9 @@ extension SyntaxProtocol { public func withLeadingNewlines(_ count: Int = 2) -> Self { with(\.leadingTrivia, .newlines(count)) } + + public func withTrivia(from other: some SyntaxProtocol) -> Self { + with(\.leadingTrivia, other.leadingTrivia) + .with(\.trailingTrivia, other.trailingTrivia) + } } diff --git a/Sources/PrincipleMacros/Syntax/Extensions/TypeSyntax.swift b/Sources/PrincipleMacros/Syntax/Extensions/TypeSyntax.swift index cfdcc3e..52c2930 100644 --- a/Sources/PrincipleMacros/Syntax/Extensions/TypeSyntax.swift +++ b/Sources/PrincipleMacros/Syntax/Extensions/TypeSyntax.swift @@ -15,6 +15,26 @@ extension TypeSyntax { } } +extension AttributeSyntax { + + public func isLike(_ other: AttributeSyntax) -> Bool { + attributeName.isLike(other.attributeName) + } +} + +extension AttributeListSyntax { + + public func contains(like attribute: AttributeSyntax) -> Bool { + first(like: attribute) != nil + } + + public func first(like attribute: AttributeSyntax) -> AttributeSyntax? { + lazy.compactMap(\.attribute).first { element in + element.isLike(attribute) + } + } +} + extension TypeSyntax { public var standardized: TypeSyntax { diff --git a/Sources/PrincipleMacros/Syntax/Extensions/WithAttributesSyntax+Availability.swift b/Sources/PrincipleMacros/Syntax/Extensions/WithAttributesSyntax+Availability.swift new file mode 100644 index 0000000..91faea1 --- /dev/null +++ b/Sources/PrincipleMacros/Syntax/Extensions/WithAttributesSyntax+Availability.swift @@ -0,0 +1,48 @@ +// +// WithAttributesSyntax+Availability.swift +// PrincipleMacros +// +// Created by Kamil Strzelecki on 17/01/2025. +// Copyright © 2025 Kamil Strzelecki. All rights reserved. +// + +import SwiftSyntaxMacros + +extension WithAttributesSyntax { + + public var availability: AttributeListSyntax? { + attributes.availability + } +} + +extension AttributeListSyntax { + + public var availability: Self? { + let elements = compactMap(\.availability) + return elements.isEmpty ? nil : AttributeListSyntax(elements) + } +} + +extension AttributeListSyntax.Element { + + public var availability: Self? { + switch self { + case let .attribute(attribute): + if let availability = attribute.availability { + return .attribute(availability) + } + case let .ifConfigDecl(ifConfig): + if let availability = ifConfig.availability { + return .ifConfigDecl(availability) + } + } + return nil + } +} + +extension AttributeSyntax { + + public var availability: Self? { + arguments?.is(AvailabilityArgumentListSyntax.self) == true ? self : nil + } +} diff --git a/Sources/PrincipleMacros/Syntax/Extensions/WithAttributesSyntax+GlobalActorIsolation.swift b/Sources/PrincipleMacros/Syntax/Extensions/WithAttributesSyntax+GlobalActorIsolation.swift new file mode 100644 index 0000000..2c6d22a --- /dev/null +++ b/Sources/PrincipleMacros/Syntax/Extensions/WithAttributesSyntax+GlobalActorIsolation.swift @@ -0,0 +1,41 @@ +// +// WithAttributesSyntax+GlobalActorIsolation.swift +// PrincipleMacros +// +// Created by Kamil Strzelecki on 17/01/2025. +// Copyright © 2025 Kamil Strzelecki. All rights reserved. +// + +import SwiftSyntaxMacros + +extension WithAttributesSyntax { + + public var globalActorIsolation: GlobalActorIsolation? { + attributes.lazy + .compactMap(\.attribute?.globalActorIsolation) + .first + } +} + +extension AttributeSyntax { + + public var globalActorIsolation: GlobalActorIsolation? { + if attributeName.trimmedDescription.hasSuffix("Actor") { + let standardizedType = attributeName.standardized + return .isolated(standardizedType: standardizedType) + } + return nil + } +} + +extension AttributeListSyntax.Element { + + public var attribute: AttributeSyntax? { + switch self { + case let .attribute(attribute): + attribute + case .ifConfigDecl: + nil + } + } +} diff --git a/Sources/PrincipleMacros/Syntax/Extensions/WithAttributesSyntax.swift b/Sources/PrincipleMacros/Syntax/Extensions/WithAttributesSyntax.swift deleted file mode 100644 index e148e03..0000000 --- a/Sources/PrincipleMacros/Syntax/Extensions/WithAttributesSyntax.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// WithAttributesSyntax.swift -// PrincipleMacros -// -// Created by Kamil Strzelecki on 17/01/2025. -// Copyright © 2025 Kamil Strzelecki. All rights reserved. -// - -import SwiftSyntaxMacros - -extension WithAttributesSyntax { - - public var globalActorIsolation: GlobalActorIsolation? { - let attribute = attributes.attributeElements.first { attribute in - attribute.attributeName.trimmedDescription.hasSuffix("Actor") - } - if let attribute { - let standardizedType = attribute.attributeName.standardized - return .isolated(standardizedType: standardizedType) - } - return nil - } -} diff --git a/Sources/PrincipleMacros/Syntax/Extensions/WithModifiersSyntax+AccessControlLevel.swift b/Sources/PrincipleMacros/Syntax/Extensions/WithModifiersSyntax+AccessControlLevel.swift new file mode 100644 index 0000000..f9614f5 --- /dev/null +++ b/Sources/PrincipleMacros/Syntax/Extensions/WithModifiersSyntax+AccessControlLevel.swift @@ -0,0 +1,42 @@ +// +// WithModifiersSyntax+AccessControlLevel.swift +// PrincipleMacros +// +// Created by Kamil Strzelecki on 12/01/2025. +// Copyright © 2025 Kamil Strzelecki. All rights reserved. +// + +import SwiftSyntaxMacros + +extension WithModifiersSyntax { + + public var accessControlLevel: AccessControlLevel? { + modifiers.lazy + .compactMap(\.accessControlLevel) + .first + } + + public var setterAccessControlLevel: AccessControlLevel? { + modifiers.lazy + .compactMap(\.setterAccessControlLevel) + .first ?? accessControlLevel + } +} + +extension DeclModifierSyntax { + + public var accessControlLevel: AccessControlLevel? { + accessControlLevel(detail: nil) + } + + public var setterAccessControlLevel: AccessControlLevel? { + accessControlLevel(detail: .identifier("set")) + } + + private func accessControlLevel(detail detailTokenKind: TokenKind?) -> AccessControlLevel? { + if detail?.detail.tokenKind == detailTokenKind { + return AccessControlLevel(tokenSyntax: name) + } + return nil + } +} diff --git a/Sources/PrincipleMacros/Syntax/Extensions/WithModifiersSyntax+GlobalActorIsolation.swift b/Sources/PrincipleMacros/Syntax/Extensions/WithModifiersSyntax+GlobalActorIsolation.swift new file mode 100644 index 0000000..1582ab9 --- /dev/null +++ b/Sources/PrincipleMacros/Syntax/Extensions/WithModifiersSyntax+GlobalActorIsolation.swift @@ -0,0 +1,28 @@ +// +// WithModifiersSyntax+GlobalActorIsolation.swift +// PrincipleMacros +// +// Created by Kamil Strzelecki on 12/01/2025. +// Copyright © 2025 Kamil Strzelecki. All rights reserved. +// + +import SwiftSyntaxMacros + +extension WithModifiersSyntax { + + public var globalActorIsolation: GlobalActorIsolation? { + modifiers.lazy + .compactMap(\.globalActorIsolation) + .first + } +} + +extension DeclModifierSyntax { + + public var globalActorIsolation: GlobalActorIsolation? { + if name.tokenKind == .keyword(.nonisolated) { + return .nonisolated(trimmedModifer: trimmed) + } + return nil + } +} diff --git a/Sources/PrincipleMacros/Syntax/Extensions/WithModifiersSyntax+Specifiers.swift b/Sources/PrincipleMacros/Syntax/Extensions/WithModifiersSyntax+Specifiers.swift new file mode 100644 index 0000000..e3c1cea --- /dev/null +++ b/Sources/PrincipleMacros/Syntax/Extensions/WithModifiersSyntax+Specifiers.swift @@ -0,0 +1,50 @@ +// +// WithModifiersSyntax+Specifiers.swift +// PrincipleMacros +// +// Created by Kamil Strzelecki on 12/01/2025. +// Copyright © 2025 Kamil Strzelecki. All rights reserved. +// + +import SwiftSyntaxMacros + +extension WithModifiersSyntax { + + public var overrideSpecifier: TokenSyntax? { + modifiers.lazy + .compactMap(\.overrideSpecifier) + .first + } + + public var finalSpecifier: TokenSyntax? { + modifiers.lazy + .compactMap(\.finalSpecifier) + .first + } + + public var typeScopeSpecifier: TokenSyntax? { + modifiers.lazy + .compactMap(\.typeScopeSpecifier) + .first + } +} + +extension DeclModifierSyntax { + + public var overrideSpecifier: TokenSyntax? { + name.tokenKind == .keyword(.override) ? name : nil + } + + public var finalSpecifier: TokenSyntax? { + name.tokenKind == .keyword(.final) ? name : nil + } + + public var typeScopeSpecifier: TokenSyntax? { + switch name.tokenKind { + case .keyword(.class), .keyword(.static): + name + default: + nil + } + } +} diff --git a/Sources/PrincipleMacros/Syntax/Extensions/WithModifiersSyntax.swift b/Sources/PrincipleMacros/Syntax/Extensions/WithModifiersSyntax.swift deleted file mode 100644 index 5c81264..0000000 --- a/Sources/PrincipleMacros/Syntax/Extensions/WithModifiersSyntax.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// WithModifiersSyntax.swift -// PrincipleMacros -// -// Created by Kamil Strzelecki on 12/01/2025. -// Copyright © 2025 Kamil Strzelecki. All rights reserved. -// - -import SwiftSyntaxMacros - -extension WithModifiersSyntax { - - public var globalActorIsolation: GlobalActorIsolation? { - let modifier = modifiers.first { modifier in - modifier.name.tokenKind == .keyword(.nonisolated) - } - if let modifier { - return .nonisolated(trimmedModifer: modifier.trimmed) - } - return nil - } -} - -extension WithModifiersSyntax { - - public var accessControlLevel: AccessControlLevel? { - accessControlLevel(detail: nil) - } - - public var setterAccessControlLevel: AccessControlLevel? { - accessControlLevel(detail: .identifier("set")) - ?? accessControlLevel - } - - private func accessControlLevel(detail: TokenKind?) -> AccessControlLevel? { - modifiers.lazy - .filter { $0.detail?.detail.tokenKind == detail } - .compactMap { AccessControlLevel(tokenSyntax: $0.name) } - .first - } -} - -extension WithModifiersSyntax { - - public var finalSpecifier: TokenSyntax? { - modifiers.lazy.map(\.name).first { name in - name.tokenKind == .keyword(.final) - } - } - - public var typeScopeSpecifier: TokenSyntax? { - modifiers.lazy.map(\.name).first { name in - switch name.tokenKind { - case .keyword(.class), .keyword(.static): - true - default: - false - } - } - } -} diff --git a/Sources/PrincipleMacros/Syntax/Custom/BasicDeclSyntax.swift b/Sources/PrincipleMacros/Syntax/Protocols/BasicDeclSyntax.swift similarity index 100% rename from Sources/PrincipleMacros/Syntax/Custom/BasicDeclSyntax.swift rename to Sources/PrincipleMacros/Syntax/Protocols/BasicDeclSyntax.swift diff --git a/Sources/PrincipleMacros/Syntax/Custom/StatefulDeclSyntax.swift b/Sources/PrincipleMacros/Syntax/Protocols/StatefulDeclSyntax.swift similarity index 100% rename from Sources/PrincipleMacros/Syntax/Custom/StatefulDeclSyntax.swift rename to Sources/PrincipleMacros/Syntax/Protocols/StatefulDeclSyntax.swift diff --git a/Sources/PrincipleMacros/Syntax/Custom/TypeDeclSyntax.swift b/Sources/PrincipleMacros/Syntax/Protocols/TypeDeclSyntax.swift similarity index 100% rename from Sources/PrincipleMacros/Syntax/Custom/TypeDeclSyntax.swift rename to Sources/PrincipleMacros/Syntax/Protocols/TypeDeclSyntax.swift diff --git a/Tests/PrincipleMacrosTests/Builders/EnumCaseCallExprBuilderTests.swift b/Tests/PrincipleMacrosTests/Builders/EnumCaseCallExprBuilderTests.swift index da06d9b..b0ba636 100644 --- a/Tests/PrincipleMacrosTests/Builders/EnumCaseCallExprBuilderTests.swift +++ b/Tests/PrincipleMacrosTests/Builders/EnumCaseCallExprBuilderTests.swift @@ -11,14 +11,14 @@ import Testing internal struct EnumCaseCallExprBuilderTests { - func makeEnumCase(from decl: DeclSyntax) throws -> EnumCase { + private func makeEnumCase(from decl: DeclSyntax) throws -> EnumCase { let enumCaseDecl = try #require(EnumCaseDeclSyntax(decl)) let enumElement = try #require(enumCaseDecl.elements.first) return EnumCase(declaration: enumCaseDecl, element: enumElement) } @Test - func callWithoutAssociatedValues() throws { + func withoutAssociatedValues() throws { let enumCase = try makeEnumCase(from: "case first") let builder = EnumCaseCallExprBuilder(for: enumCase) { _ in Issue.record() @@ -28,7 +28,7 @@ internal struct EnumCaseCallExprBuilderTests { } @Test - func callWithUnnamedAssociatedValue() throws { + func withUnnamedAssociatedValue() throws { let enumCase = try makeEnumCase(from: "case second(Int)") let builder = EnumCaseCallExprBuilder(for: enumCase) { _ in "123" as ExprSyntax @@ -37,7 +37,7 @@ internal struct EnumCaseCallExprBuilderTests { } @Test - func callWithMultipleAssociatedValues() throws { + func withMultipleAssociatedValues() throws { let enumCase = try makeEnumCase(from: "case third(arg: String, Int)") let builder = EnumCaseCallExprBuilder(for: enumCase) { associatedValue in if associatedValue.standardizedName.description == "arg" { diff --git a/Tests/PrincipleMacrosTests/Builders/SwitchExprBuilderTests.swift b/Tests/PrincipleMacrosTests/Builders/SwitchExprBuilderTests.swift index a627982..4709c49 100644 --- a/Tests/PrincipleMacrosTests/Builders/SwitchExprBuilderTests.swift +++ b/Tests/PrincipleMacrosTests/Builders/SwitchExprBuilderTests.swift @@ -11,14 +11,14 @@ import Testing internal struct SwitchExprBuilderTests { - func makeEnumCase(from decl: DeclSyntax) throws -> EnumCase { + private func makeEnumCase(from decl: DeclSyntax) throws -> EnumCase { let enumCaseDecl = try #require(EnumCaseDeclSyntax(decl)) let enumElement = try #require(enumCaseDecl.elements.first) return EnumCase(declaration: enumCaseDecl, element: enumElement) } @Test - func switchExpression() throws { + func build() throws { let enumCases = try EnumCasesList([ makeEnumCase(from: "case first"), makeEnumCase(from: "case second(Int)"), diff --git a/Tests/PrincipleMacrosTests/Parameters/ParameterExtractorTests.swift b/Tests/PrincipleMacrosTests/Parameters/ParameterExtractorTests.swift index b7cda1b..0ef1be2 100644 --- a/Tests/PrincipleMacrosTests/Parameters/ParameterExtractorTests.swift +++ b/Tests/PrincipleMacrosTests/Parameters/ParameterExtractorTests.swift @@ -9,144 +9,207 @@ @testable import PrincipleMacros import Testing -internal struct ParameterExtractorTests { +internal class ParameterExtractorTests { private func makeExtractor(from expr: ExprSyntax) throws -> ParameterExtractor { let macro = try #require(MacroExpansionExprSyntax(expr)) return ParameterExtractor(from: macro) } +} - @Test - func expressionExtraction() throws { - let extractor = try makeExtractor(from: "#MyMacro(value: Type.make())") - let extracted = extractor.expression(withLabel: "value") - let expected: ExprSyntax = "Type.make()" - #expect(extracted?.description == expected.description) - } +extension ParameterExtractorTests { - @Test - func unnamedExpressionExtraction() throws { - let extractor = try makeExtractor(from: "#MyMacro(value: Type.make(), 123)") - let extracted = extractor.expression(withLabel: nil) - let expected: ExprSyntax = "123" - #expect(extracted?.description == expected.description) - } + final class Expression: ParameterExtractorTests { - @Test - func overlappingExpressionExtraction() throws { - let extractor = try makeExtractor(from: "#MyMacro(Type.make(), 123)") - let firstExtracted = extractor.expression(withLabel: nil) - let firstExpected: ExprSyntax = "Type.make()" - #expect(firstExtracted?.description == firstExpected.description) + @Test + func extraction() throws { + let extractor = try makeExtractor(from: "#MyMacro(value: Type.make())") + let extracted = extractor.expression(withLabel: "value") + let expected: ExprSyntax = "Type.make()" + #expect(extracted?.description == expected.description) + } - let secondExtracted = extractor.expression(withLabel: nil) - let secondExpected: ExprSyntax = "123" - #expect(secondExtracted?.description == secondExpected.description) - } + @Test + func unnamedExtraction() throws { + let extractor = try makeExtractor(from: "#MyMacro(value: Type.make(), 123)") + let extracted = extractor.expression(withLabel: nil) + let expected: ExprSyntax = "123" + #expect(extracted?.description == expected.description) + } + + @Test + func overlappingExtraction() throws { + let extractor = try makeExtractor(from: "#MyMacro(Type.make(), 123)") + let firstExtracted = extractor.expression(withLabel: nil) + let firstExpected: ExprSyntax = "Type.make()" + #expect(firstExtracted?.description == firstExpected.description) + + let secondExtracted = extractor.expression(withLabel: nil) + let secondExpected: ExprSyntax = "123" + #expect(secondExtracted?.description == secondExpected.description) + } - @Test - func missingExpressionExtraction() throws { - let extractor = try makeExtractor(from: #"#MyMacro(arg: Type.make())"#) - let extracted = extractor.expression(withLabel: "value") - #expect(extracted == nil) + @Test + func missingExtraction() throws { + let extractor = try makeExtractor(from: #"#MyMacro(arg: Type.make())"#) + let extracted = extractor.expression(withLabel: "value") + #expect(extracted == nil) + } } } extension ParameterExtractorTests { - @Test - func trailingClosureExtraction() throws { - let extractor = try makeExtractor(from: "#MyMacro { _ in }") - let extracted = extractor.trailingClosure(withLabel: "operation") - #expect(extracted?.description == "{ _ in }") - } + final class TrailingClosure: ParameterExtractorTests { + + @Test + func extraction() throws { + let extractor = try makeExtractor(from: "#MyMacro { _ in }") + let extracted = extractor.trailingClosure(withLabel: "operation") + #expect(extracted?.description == "{ _ in }") + } - @Test - func trailingClosureReferenceExtraction() throws { - let extractor = try makeExtractor(from: "#MyMacro(operation: perform)") - let extracted = extractor.trailingClosure(withLabel: "operation") - #expect(extracted?.description == "perform") + @Test + func referenceExtraction() throws { + let extractor = try makeExtractor(from: "#MyMacro(operation: perform)") + let extracted = extractor.trailingClosure(withLabel: "operation") + #expect(extracted?.description == "perform") + } } } extension ParameterExtractorTests { - @Test( - arguments: [ - "private", - "fileprivate", - "internal", - "package", - "public", - "open" - ] - ) - func accessControlLevelExtraction(_ level: String) throws { - let extractor = try makeExtractor(from: "#MyMacro(.\(raw: level))") - let extracted = try #require(try extractor.accessControlLevel(withLabel: nil)) - #expect(String(describing: extracted).hasSuffix(level)) - } + final class AccessControlLevel: ParameterExtractorTests { + + @Test( + arguments: [ + "private", + "fileprivate", + "internal", + "package", + "public", + "open" + ] + ) + func extraction(_ level: String) throws { + let extractor = try makeExtractor(from: "#MyMacro(.\(raw: level))") + let extracted = try #require(try extractor.accessControlLevel(withLabel: nil)) + #expect(String(describing: extracted).hasSuffix(level)) + } - @Test - func unexpectedSyntaxWhenPerformingAccessControlLevelExtraction() throws { - let extractor = try makeExtractor(from: "#MyMacro(.didSet)") - #expect(throws: ParameterExtractionError.unexpectedSyntaxType) { - try extractor.accessControlLevel(withLabel: nil) + @Test + func unexpectedSyntax() throws { + let extractor = try makeExtractor(from: "#MyMacro(.didSet)") + #expect(throws: ParameterExtractionError.unexpectedSyntaxType) { + try extractor.accessControlLevel(withLabel: nil) + } } } } extension ParameterExtractorTests { - @Test - func rawStringExtraction() throws { - let extractor = try makeExtractor(from: #"#MyMacro(string: "arg")"#) - let extracted = try extractor.rawString(withLabel: "string") - #expect(extracted == "arg") - } + final class RawBool: ParameterExtractorTests { + + @Test( + arguments: [ + true, + false + ] + ) + func extraction(_ bool: Bool) throws { + let extractor = try makeExtractor(from: "#MyMacro(boolean: \(raw: bool))") + let extracted = try extractor.rawBool(withLabel: "boolean") + #expect(extracted == bool) + } - @Test - func unexpectedSyntaxWhenPerformingRawStringExtraction() throws { - let extractor = try makeExtractor(from: #"#MyMacro(string: reference.arg)"#) - #expect(throws: ParameterExtractionError.unexpectedSyntaxType) { - try extractor.rawString(withLabel: "string") + @Test + func unexpectedSyntax() throws { + let extractor = try makeExtractor(from: #"#MyMacro(boolean: value)"#) + #expect(throws: ParameterExtractionError.unexpectedSyntaxType) { + try extractor.rawBool(withLabel: "boolean") + } } } } extension ParameterExtractorTests { - @Test( - arguments: [ - "MainActor", - "SomeType.SomeActor" - ] - ) - func globalActorExtraction(_ isolation: String) throws { - let extractor = try makeExtractor(from: "#MyMacro(isolation: \(raw: isolation).self)") - let extracted = try extractor.globalActorIsolation(withLabel: "isolation") - #expect(extracted?.standardizedIsolationType?.trimmedDescription == isolation) - } + final class RawString: ParameterExtractorTests { - @Test - func missingGlobalActorExtraction() throws { - let extractor = try makeExtractor(from: "#MyMacro()") - let extracted = try extractor.globalActorIsolation(withLabel: "isolation") - #expect(extracted == nil) + @Test + func extraction() throws { + let extractor = try makeExtractor(from: #"#MyMacro(string: "arg")"#) + let extracted = try extractor.rawString(withLabel: "string") + #expect(extracted == "arg") + } + + @Test + func unexpectedSyntax() throws { + let extractor = try makeExtractor(from: #"#MyMacro(string: reference.arg)"#) + #expect(throws: ParameterExtractionError.unexpectedSyntaxType) { + try extractor.rawString(withLabel: "string") + } + } } +} + +extension ParameterExtractorTests { + + final class GlobalActorIsolation: ParameterExtractorTests { + + @Test( + arguments: [ + "MainActor", + "SomeType.SomeActor" + ] + ) + func extraction(_ isolation: String) throws { + let extractor = try makeExtractor(from: "#MyMacro(isolation: \(raw: isolation).self)") + let extracted = try extractor.globalActorIsolation(withLabel: "isolation") + #expect(extracted?.standardizedIsolationType?.trimmedDescription == isolation) + } + + @Test + func nonisolatedExtraction() throws { + let extractor = try makeExtractor(from: "#MyMacro(isolation: nil)") + let extracted = try extractor.globalActorIsolation(withLabel: "isolation") + #expect(extracted?.trimmedNonisolatedModifier?.trimmedDescription == "nonisolated") + } - @Test - func explicitNilGlobalActorExtraction() throws { - let extractor = try makeExtractor(from: "#MyMacro(isolation: nil)") - let extracted = try extractor.globalActorIsolation(withLabel: "isolation") - #expect(extracted?.trimmedNonisolatedModifier?.trimmedDescription == "nonisolated") + @Test + func unexpectedSyntax() throws { + let extractor = try makeExtractor(from: #"#MyMacro(isolation: MainActor.Type)"#) + #expect(throws: ParameterExtractionError.unexpectedSyntaxType) { + try extractor.globalActorIsolation(withLabel: "isolation") + } + } } +} + +extension ParameterExtractorTests { + + final class TypeReference: ParameterExtractorTests { + + @Test( + arguments: [ + "MyType", + "SomeType.MyType" + ] + ) + func extraction(_ type: String) throws { + let extractor = try makeExtractor(from: "#MyMacro(type: \(raw: type).self)") + let extracted = try extractor.type(withLabel: "type") + #expect(extracted?.trimmedDescription == type) + } - @Test - func unexpectedSyntaxWhenPerformingGlobalActorExtraction() throws { - let extractor = try makeExtractor(from: #"#MyMacro(isolation: MainActor.Type)"#) - #expect(throws: ParameterExtractionError.unexpectedSyntaxType) { - try extractor.globalActorIsolation(withLabel: "isolation") + @Test + func unexpectedSyntax() throws { + let extractor = try makeExtractor(from: #"#MyMacro(type: MainActor.Type)"#) + #expect(throws: ParameterExtractionError.unexpectedSyntaxType) { + try extractor.type(withLabel: "type") + } } } } diff --git a/Tests/PrincipleMacrosTests/Parsers/EnumCasesParserTests.swift b/Tests/PrincipleMacrosTests/Parsers/EnumCasesParserTests.swift index 98a46a1..c19d039 100644 --- a/Tests/PrincipleMacrosTests/Parsers/EnumCasesParserTests.swift +++ b/Tests/PrincipleMacrosTests/Parsers/EnumCasesParserTests.swift @@ -12,24 +12,22 @@ import Testing internal struct EnumCasesParserTests { - private let context = BasicMacroExpansionContext() - @Test - func enumCase() throws { + func withoutAssociatedValue() throws { let decl: DeclSyntax = """ case myCase """ - let enumCase = try #require(EnumCasesParser.parse(declaration: decl, in: context).first) + let enumCase = try #require(EnumCasesParser.parse(declaration: decl).first) #expect(enumCase.trimmedName.description == "myCase") #expect(enumCase.associatedValues.isEmpty) } @Test - func enumCaseWithUnnamedAssociatedValue() throws { + func withUnnamedAssociatedValue() throws { let decl: DeclSyntax = """ case myCase(Int?) """ - let enumCase = try #require(EnumCasesParser.parse(declaration: decl, in: context).first) + let enumCase = try #require(EnumCasesParser.parse(declaration: decl).first) #expect(enumCase.trimmedName.description == "myCase") let associatedValue0 = try #require(enumCase.associatedValues.first) @@ -38,11 +36,11 @@ internal struct EnumCasesParserTests { } @Test - func enumCaseWithNamedAssociatedValue() throws { + func withNamedAssociatedValue() throws { let decl: DeclSyntax = """ case myCase(values: [String]) """ - let enumCase = try #require(EnumCasesParser.parse(declaration: decl, in: context).first) + let enumCase = try #require(EnumCasesParser.parse(declaration: decl).first) #expect(enumCase.trimmedName.description == "myCase") let associatedValue0 = try #require(enumCase.associatedValues.first) @@ -51,11 +49,11 @@ internal struct EnumCasesParserTests { } @Test - func enumCaseWithManyAssociatedValues() throws { + func withMultipleAssociatedValues() throws { let decl: DeclSyntax = """ case myCase(value: Int?, [String]) """ - let enumCase = try #require(EnumCasesParser.parse(declaration: decl, in: context).first) + let enumCase = try #require(EnumCasesParser.parse(declaration: decl).first) #expect(enumCase.trimmedName.description == "myCase") let associatedValue0 = try #require(enumCase.associatedValues.first) diff --git a/Tests/PrincipleMacrosTests/Parsers/PropertiesListTests.swift b/Tests/PrincipleMacrosTests/Parsers/PropertiesListTests.swift index a30ad93..4214982 100644 --- a/Tests/PrincipleMacrosTests/Parsers/PropertiesListTests.swift +++ b/Tests/PrincipleMacrosTests/Parsers/PropertiesListTests.swift @@ -7,7 +7,6 @@ // @testable import PrincipleMacros -import SwiftSyntaxMacroExpansion import Testing internal struct PropertiesListTests { @@ -34,9 +33,8 @@ internal struct PropertiesListTests { } """ - let classDecl = try #require(ClassDeclSyntax(decl)) - let context = BasicMacroExpansionContext() - self.list = PropertiesParser.parse(memberBlock: classDecl.memberBlock, in: context) + let classDecl = try #require(decl.as(ClassDeclSyntax.self)) + self.list = try PropertiesParser.parse(memberBlock: classDecl.memberBlock) } @Test diff --git a/Tests/PrincipleMacrosTests/Parsers/PropertiesParserTests.swift b/Tests/PrincipleMacrosTests/Parsers/PropertiesParserTests.swift index b5e0b85..95f2e2a 100644 --- a/Tests/PrincipleMacrosTests/Parsers/PropertiesParserTests.swift +++ b/Tests/PrincipleMacrosTests/Parsers/PropertiesParserTests.swift @@ -7,19 +7,16 @@ // @testable import PrincipleMacros -import SwiftSyntaxMacroExpansion import Testing internal struct PropertiesParserTests { - private let context = BasicMacroExpansionContext() - @Test func storedLet() throws { let decl: DeclSyntax = """ public internal(set) static let myLet: Int? """ - let property = try #require(PropertiesParser.parse(declaration: decl, in: context).first) + let property = try #require(PropertiesParser.parse(declaration: decl).first) #expect(property.kind == .stored) #expect(property.mutability == .immutable) #expect(property.accessControlLevel == .public) @@ -36,7 +33,7 @@ internal struct PropertiesParserTests { let decl: DeclSyntax = """ private(set) var myVar = "Hello, world!" """ - let property = try #require(PropertiesParser.parse(declaration: decl, in: context).first) + let property = try #require(PropertiesParser.parse(declaration: decl).first) #expect(property.kind == .stored) #expect(property.mutability == .mutable) #expect(property.accessControlLevel == nil) @@ -56,7 +53,7 @@ internal struct PropertiesParserTests { didSet { print("didSet", oldValue) } } """ - let property = try #require(PropertiesParser.parse(declaration: decl, in: context).first) + let property = try #require(PropertiesParser.parse(declaration: decl).first) #expect(property.kind == .stored) #expect(property.mutability == .mutable) #expect(property.accessControlLevel == nil) @@ -76,7 +73,7 @@ internal struct PropertiesParserTests { [1, 2, 3] } """ - let property = try #require(PropertiesParser.parse(declaration: decl, in: context).first) + let property = try #require(PropertiesParser.parse(declaration: decl).first) #expect(property.kind == .computed) #expect(property.mutability == .immutable) #expect(property.accessControlLevel == .fileprivate) @@ -96,7 +93,7 @@ internal struct PropertiesParserTests { set { _storage = newValue } } """ - let property = try #require(PropertiesParser.parse(declaration: decl, in: context).first) + let property = try #require(PropertiesParser.parse(declaration: decl).first) #expect(property.kind == .computed) #expect(property.mutability == .mutable) #expect(property.accessControlLevel == nil) diff --git a/Tests/PrincipleMacrosTests/Syntax/ClosureExprSyntaxTests.swift b/Tests/PrincipleMacrosTests/Syntax/ClosureExprSyntaxTests.swift deleted file mode 100644 index 14d84ff..0000000 --- a/Tests/PrincipleMacrosTests/Syntax/ClosureExprSyntaxTests.swift +++ /dev/null @@ -1,127 +0,0 @@ -// -// ClosureExprSyntaxTests.swift -// PrincipleMacros -// -// Created by Kamil Strzelecki on 03/02/2025. -// Copyright © 2025 Kamil Strzelecki. All rights reserved. -// - -@testable import PrincipleMacros -import Testing - -internal enum ClosureExprSyntaxTests { - - enum SingleLine { - - struct WithoutSignature { - - private let expr: ExprSyntax = """ - { Date.now } - """ - - @Test - func expansion() { - let closure = expr.expanded(nestingLevel: 2) - let interpolation: ExprSyntax = """ - .init( - parameter: .init( - closure: \(closure), - value: "Foo" - ) - ) - """ - - let expectation = """ - .init( - parameter: .init( - closure: { - Date.now - }, - value: "Foo" - ) - ) - """ - - #expect(interpolation.description == expectation) - } - } - - struct WithSignature { - - private let expr: ExprSyntax = """ - { [weak self] arg0, _ -> String in arg0 } - """ - - @Test - func expansion() { - let closure = expr.expanded(nestingLevel: 2) - let interpolation: ExprSyntax = """ - .init( - parameter: .init( - closure: \(closure), - value: "Foo" - ) - ) - """ - - let expectation = """ - .init( - parameter: .init( - closure: { [weak self] arg0, _ -> String in - arg0 - }, - value: "Foo" - ) - ) - """ - - #expect(interpolation.description == expectation) - } - } - } - - struct MultiLine { - - private let expr: ExprSyntax = """ - { [weak self] arg0, arg1 -> String in - if arg0 > 0 { - return String(arg0) - } else if arg1 { - return "Test" - } - return "" - } - """ - - @Test - func expansion() { - let closure = expr.expanded(nestingLevel: 2) - let interpolation: ExprSyntax = """ - .init( - parameter: .init( - closure: \(closure), - value: "Foo" - ) - ) - """ - - let expectation = """ - .init( - parameter: .init( - closure: { [weak self] arg0, arg1 -> String in - if arg0 > 0 { - return String(arg0) - } else if arg1 { - return "Test" - } - return "" - }, - value: "Foo" - ) - ) - """ - - #expect(interpolation.description == expectation) - } - } -} diff --git a/Tests/PrincipleMacrosTests/Syntax/CamelCaseNotationTests.swift b/Tests/PrincipleMacrosTests/Syntax/Concepts/CamelCaseNotationTests.swift similarity index 100% rename from Tests/PrincipleMacrosTests/Syntax/CamelCaseNotationTests.swift rename to Tests/PrincipleMacrosTests/Syntax/Concepts/CamelCaseNotationTests.swift diff --git a/Tests/PrincipleMacrosTests/Syntax/ClosureTypeSyntaxTests.swift b/Tests/PrincipleMacrosTests/Syntax/Concepts/ClosureTypeSyntaxTests.swift similarity index 95% rename from Tests/PrincipleMacrosTests/Syntax/ClosureTypeSyntaxTests.swift rename to Tests/PrincipleMacrosTests/Syntax/Concepts/ClosureTypeSyntaxTests.swift index a36dac7..0ed3231 100644 --- a/Tests/PrincipleMacrosTests/Syntax/ClosureTypeSyntaxTests.swift +++ b/Tests/PrincipleMacrosTests/Syntax/Concepts/ClosureTypeSyntaxTests.swift @@ -12,7 +12,7 @@ import Testing internal struct ClosureTypeSyntaxTests { @Test - func unnamedClosure() throws { + func unnamed() throws { let type: TypeSyntax = "(Int?, [Bool]) async throws -> ()" let closure = try #require(ClosureTypeSyntax(type)) @@ -30,7 +30,7 @@ internal struct ClosureTypeSyntaxTests { } @Test - func namedClosure() throws { + func named() throws { let type: TypeSyntax = "(_ value: Int!, _ argument: (first: String, [Int])) throws(SomeError) -> String?" let closure = try #require(ClosureTypeSyntax(type)) @@ -48,7 +48,7 @@ internal struct ClosureTypeSyntaxTests { } @Test - func mixedClosure() throws { + func mixed() throws { let type: TypeSyntax = "(_ value: Bool, String.Key) -> Void" let closure = try #require(ClosureTypeSyntax(type)) @@ -66,7 +66,7 @@ internal struct ClosureTypeSyntaxTests { } @Test - func attributedClosure() throws { + func attributed() throws { let type: TypeSyntax = "@Sendable () -> Void" let closure = try #require(ClosureTypeSyntax(type)) let attribute = try #require(closure.attributes.first) diff --git a/Tests/PrincipleMacrosTests/Syntax/GlobalActorIsolationTests.swift b/Tests/PrincipleMacrosTests/Syntax/Concepts/GlobalActorIsolationTests.swift similarity index 90% rename from Tests/PrincipleMacrosTests/Syntax/GlobalActorIsolationTests.swift rename to Tests/PrincipleMacrosTests/Syntax/Concepts/GlobalActorIsolationTests.swift index f0feeb6..7ef8485 100644 --- a/Tests/PrincipleMacrosTests/Syntax/GlobalActorIsolationTests.swift +++ b/Tests/PrincipleMacrosTests/Syntax/Concepts/GlobalActorIsolationTests.swift @@ -11,18 +11,7 @@ import Testing internal enum GlobalActorIsolationTests { - internal struct Function { - - @Test - func withNonisolatedNonsendingModifier() throws { - let decl: DeclSyntax = "nonisolated(nonsending) func test() {}" - let functionDecl = try #require(decl.as(FunctionDeclSyntax.self)) - let isolation = GlobalActorIsolation.resolved(for: functionDecl, in: []) - #expect(isolation?.trimmedNonisolatedModifier?.trimmedDescription == "nonisolated(nonsending)") - } - } - - internal struct Property { + internal struct MemberDeclaration { @Test func withGlobalActor() throws { @@ -48,6 +37,14 @@ internal enum GlobalActorIsolationTests { #expect(isolation?.trimmedNonisolatedModifier?.trimmedDescription == "nonisolated") } + @Test + func withNonisolatedNonsendingModifier() throws { + let decl: DeclSyntax = "nonisolated(nonsending) func test() {}" + let functionDecl = try #require(decl.as(FunctionDeclSyntax.self)) + let isolation = GlobalActorIsolation.resolved(for: functionDecl, in: []) + #expect(isolation?.trimmedNonisolatedModifier?.trimmedDescription == "nonisolated(nonsending)") + } + @Test func withoutGlobalActorInStructWithGlobalActor() throws { let decl: DeclSyntax = "var test = 123" @@ -78,6 +75,16 @@ internal enum GlobalActorIsolationTests { #expect(isolation?.trimmedNonisolatedModifier?.trimmedDescription == "nonisolated") } + @Test + func withNonisolatedNonsendingModifierInStructWithGlobalActor() throws { + let decl: DeclSyntax = "nonisolated(nonsending) func test() {}" + let functionDecl = try #require(decl.as(FunctionDeclSyntax.self)) + let structDecl: DeclSyntax = "@MyActor struct MyStruct {}" + let lexicalContext = [Syntax(structDecl)] + let isolation = GlobalActorIsolation.resolved(for: functionDecl, in: lexicalContext) + #expect(isolation?.trimmedNonisolatedModifier?.trimmedDescription == "nonisolated(nonsending)") + } + @Test func inNestedStructWithGlobalActor() throws { let decl: DeclSyntax = "var test = 123" @@ -101,7 +108,7 @@ internal enum GlobalActorIsolationTests { } } - internal struct Class { + internal struct TypeDeclaration { @Test func withGlobalActor() throws { diff --git a/Tests/PrincipleMacrosTests/Syntax/Extensions/AvailabilityTests.swift b/Tests/PrincipleMacrosTests/Syntax/Extensions/AvailabilityTests.swift new file mode 100644 index 0000000..50b2236 --- /dev/null +++ b/Tests/PrincipleMacrosTests/Syntax/Extensions/AvailabilityTests.swift @@ -0,0 +1,61 @@ +// +// AvailabilityTests.swift +// PrincipleMacros +// +// Created by Kamil Strzelecki on 22/11/2025. +// Copyright © 2025 Kamil Strzelecki. All rights reserved. +// + +@testable import PrincipleMacros +import Testing + +internal struct AvailabilityTests { + + @Test + func withoutAvailability() throws { + let decl: DeclSyntax = """ + @MainActor @Observable + class MyClass {} + """ + + let classDecl = try #require(decl.as(ClassDeclSyntax.self)) + #expect(classDecl.availability == nil) + } + + @Test + func withAvailability() throws { + let decl: DeclSyntax = """ + @MainActor @Observable + @available(iOS 26, *) + class MyClass {} + """ + + let classDecl = try #require(decl.as(ClassDeclSyntax.self)) + #expect(classDecl.availability?.trimmedDescription == "@available(iOS 26, *)") + } + + @Test + func withIfConfig() throws { + let decl: DeclSyntax = """ + #if os(macOS) + @MainActor + @available(macOS 26, *) + #else + @Observable + @available(iOS 26, *) + #endif + class MyClass {} + """ + + let expectation = """ + #if os(macOS) + @available(macOS 26, *) + #else + @available(iOS 26, *) + #endif + """ + + let classDecl = try #require(decl.as(ClassDeclSyntax.self)) + #expect(classDecl.availability?.trimmedDescription == expectation) + } +} diff --git a/Tests/PrincipleMacrosTests/Syntax/Extensions/ClassDeclSyntaxTests.swift b/Tests/PrincipleMacrosTests/Syntax/Extensions/ClassDeclSyntaxTests.swift new file mode 100644 index 0000000..b327f31 --- /dev/null +++ b/Tests/PrincipleMacrosTests/Syntax/Extensions/ClassDeclSyntaxTests.swift @@ -0,0 +1,79 @@ +// +// ClassDeclSyntaxTests.swift +// PrincipleMacros +// +// Created by Kamil Strzelecki on 21/11/2025. +// Copyright © 2025 Kamil Strzelecki. All rights reserved. +// + +@testable import PrincipleMacros +import Testing + +internal enum ClassDeclSyntaxTests { + + struct InferredSuperclassType { + + @Test + func withoutInheritanceClause() throws { + let decl: DeclSyntax = "class MyClass {}" + let classDecl = try #require(decl.as(ClassDeclSyntax.self)) + let inferredSuperclass = classDecl.inferredSuperclassType() + #expect(inferredSuperclass == nil) + } + + @Test + func withProtocolConformance() throws { + let decl: DeclSyntax = "class MyClass: Equatable, Hashable {}" + let classDecl = try #require(decl.as(ClassDeclSyntax.self)) + let inferredSuperclass = classDecl.inferredSuperclassType() + #expect(inferredSuperclass == nil) + } + + // swiftlint:disable empty_line_after_type_declaration + + @Test + func withOverrideModifier() throws { + let decl: DeclSyntax = """ + class MyClass: BaseClass, Hashable { + override func test() {} + } + """ + + let classDecl = try #require(decl.as(ClassDeclSyntax.self)) + let inferredSuperclass = classDecl.inferredSuperclassType() + #expect(inferredSuperclass?.description == "BaseClass") + } + + @Test + func withSuperExpression() throws { + let decl: DeclSyntax = """ + class MyClass: BaseClass, Hashable { + init(value: Int) { + super.init() + } + } + """ + + let classDecl = try #require(decl.as(ClassDeclSyntax.self)) + let inferredSuperclass = classDecl.inferredSuperclassType() + #expect(inferredSuperclass?.description == "BaseClass") + } + + @Test + func withNestedClass() throws { + let decl: DeclSyntax = """ + class MyClass: Equatable, Hashable { + class NestedClass: BaseClass { + override func test() {} + } + } + """ + + let classDecl = try #require(decl.as(ClassDeclSyntax.self)) + let inferredSuperclass = classDecl.inferredSuperclassType() + #expect(inferredSuperclass == nil) + } + + // swiftlint:enable empty_line_after_type_declaration + } +} diff --git a/Tests/PrincipleMacrosTests/Syntax/Extensions/ClosureExprSyntaxTests.swift b/Tests/PrincipleMacrosTests/Syntax/Extensions/ClosureExprSyntaxTests.swift new file mode 100644 index 0000000..dfffbcf --- /dev/null +++ b/Tests/PrincipleMacrosTests/Syntax/Extensions/ClosureExprSyntaxTests.swift @@ -0,0 +1,115 @@ +// +// ClosureExprSyntaxTests.swift +// PrincipleMacros +// +// Created by Kamil Strzelecki on 03/02/2025. +// Copyright © 2025 Kamil Strzelecki. All rights reserved. +// + +@testable import PrincipleMacros +import Testing + +internal enum ClosureExprSyntaxTests { + + struct Expansion { + + @Test + func withoutSignature() { + let expr: ExprSyntax = """ + { Date.now } + """ + + let interpolation: ExprSyntax = """ + .init( + parameter: .init( + closure: \(expr.expanded(nestingLevel: 2)), + value: "Foo" + ) + ) + """ + + let expectation = """ + .init( + parameter: .init( + closure: { + Date.now + }, + value: "Foo" + ) + ) + """ + + #expect(interpolation.description == expectation) + } + + @Test + func withSignature() { + let expr: ExprSyntax = """ + { [weak self] arg0, _ -> String in arg0 } + """ + + let interpolation: ExprSyntax = """ + .init( + parameter: .init( + closure: \(expr.expanded(nestingLevel: 2)), + value: "Foo" + ) + ) + """ + + let expectation = """ + .init( + parameter: .init( + closure: { [weak self] arg0, _ -> String in + arg0 + }, + value: "Foo" + ) + ) + """ + + #expect(interpolation.description == expectation) + } + + @Test + func multiline() { + let expr: ExprSyntax = """ + { [weak self] arg0, arg1 -> String in + if arg0 > 0 { + return String(arg0) + } else if arg1 { + return "Test" + } + return "" + } + """ + + let interpolation: ExprSyntax = """ + .init( + parameter: .init( + closure: \(expr.expanded(nestingLevel: 2)), + value: "Foo" + ) + ) + """ + + let expectation = """ + .init( + parameter: .init( + closure: { [weak self] arg0, arg1 -> String in + if arg0 > 0 { + return String(arg0) + } else if arg1 { + return "Test" + } + return "" + }, + value: "Foo" + ) + ) + """ + + #expect(interpolation.description == expectation) + } + } +} diff --git a/Tests/PrincipleMacrosTests/Syntax/ExprSyntaxTests.swift b/Tests/PrincipleMacrosTests/Syntax/Extensions/ExprSyntaxTests.swift similarity index 98% rename from Tests/PrincipleMacrosTests/Syntax/ExprSyntaxTests.swift rename to Tests/PrincipleMacrosTests/Syntax/Extensions/ExprSyntaxTests.swift index f26a851..7af0ef3 100644 --- a/Tests/PrincipleMacrosTests/Syntax/ExprSyntaxTests.swift +++ b/Tests/PrincipleMacrosTests/Syntax/Extensions/ExprSyntaxTests.swift @@ -11,7 +11,7 @@ import Testing internal enum ExprSyntaxTests { - struct Basic { + struct InferredType { @Test func optionalLiteral() { @@ -90,9 +90,6 @@ internal enum ExprSyntaxTests { let expr: ExprSyntax = "Model.self" #expect(expr.inferredType?.description == "Model.Type") } - } - - struct Complex { @Test( arguments: [ diff --git a/Tests/PrincipleMacrosTests/Syntax/Extensions/IfConfigDeclSyntaxTests.swift b/Tests/PrincipleMacrosTests/Syntax/Extensions/IfConfigDeclSyntaxTests.swift new file mode 100644 index 0000000..679d4a9 --- /dev/null +++ b/Tests/PrincipleMacrosTests/Syntax/Extensions/IfConfigDeclSyntaxTests.swift @@ -0,0 +1,202 @@ +// +// IfConfigDeclSyntaxTests.swift +// PrincipleMacros +// +// Created by Kamil Strzelecki on 21/11/2025. +// Copyright © 2025 Kamil Strzelecki. All rights reserved. +// + +@testable import PrincipleMacros +import Testing + +internal enum IfConfigDeclSyntaxTests { + + struct EnclosingIfConfig { + + private func parseLastProperty(in decl: DeclSyntax) throws -> Property { + let classDecl = try #require(decl.as(ClassDeclSyntax.self)) + let properties = try PropertiesParser.parse(memberBlock: classDecl.memberBlock) + return try #require(properties.last) + } + + // swiftlint:disable empty_line_after_type_declaration + + @Test + func withoutIfConfig() throws { + let decl: DeclSyntax = """ + class MyClass { + var test = 123 + } + """ + + let ifConfig = try parseLastProperty(in: decl).enclosingIfConfig + #expect(ifConfig == nil) + } + + @Test + func withIfConfig() throws { + let decl: DeclSyntax = """ + class MyClass { + #if os(macOS) + var other = "hello" + var test = 123 + #endif + } + """ + + let expectation = """ + #if os(macOS) + var test = 123 + #endif + """ + + let ifConfig = try parseLastProperty(in: decl).enclosingIfConfig + #expect(ifConfig?.description == expectation) + } + + @Test + func withElseIfConfig() throws { + let decl: DeclSyntax = """ + class MyClass { + #if os(iOS) + var other = "hello" + #elseif os(macOS) + var test = 123 + #endif + } + """ + + let expectation = """ + #if os(iOS) + #elseif os(macOS) + var test = 123 + #endif + """ + + let ifConfig = try parseLastProperty(in: decl).enclosingIfConfig + #expect(ifConfig?.description == expectation) + } + + @Test + func withNestedIfConfig() throws { + let decl: DeclSyntax = """ + class MyClass { + #if DEBUG + var other = "hello" + #if os(macOS) + var test = 123 + #endif + #endif + } + """ + + let expectation = """ + #if DEBUG + #if os(macOS) + var test = 123 + #endif + #endif + """ + + let ifConfig = try parseLastProperty(in: decl).enclosingIfConfig + #expect(ifConfig?.description == expectation) + } + + @Test + func withNestedElseConfig() throws { + let decl: DeclSyntax = """ + class MyClass { + #if DEBUG + var other = "hello" + #else + #if os(macOS) + var test = 123 + #endif + #endif + } + """ + + let expectation = """ + #if DEBUG + #else + #if os(macOS) + var test = 123 + #endif + #endif + """ + + let ifConfig = try parseLastProperty(in: decl).enclosingIfConfig + #expect(ifConfig?.description == expectation) + } + + @Test + func applyToNewMembers() throws { + let decl: DeclSyntax = """ + class MyClass { + #if DEBUG + var other = "hello" + #else + #if os(macOS) + var test = 123 + #endif + #endif + } + """ + + let newMembers: MemberBlockItemListSyntax = """ + var replacement = "Hello" + func test() {} + """ + + let expectation = """ + #if DEBUG + #else + #if os(macOS) + var replacement = "Hello" + func test() {} + #endif + #endif + """ + + let property = try parseLastProperty(in: decl) + let ifConfig = property.underlying.applyingEnclosingIfConfig(to: newMembers) + #expect(ifConfig?.description == expectation) + } + + @Test + func applyToNewStatements() throws { + let decl: DeclSyntax = """ + class MyClass { + #if DEBUG + var other = "hello" + #else + #if os(macOS) + var test = 123 + #endif + #endif + } + """ + + let newStatements: CodeBlockItemListSyntax = """ + var replacement = "Hello" + func test() {} + """ + + let expectation = """ + #if DEBUG + #else + #if os(macOS) + var replacement = "Hello" + func test() {} + #endif + #endif + """ + + let property = try parseLastProperty(in: decl) + let ifConfig = property.underlying.applyingEnclosingIfConfig(to: newStatements) + #expect(ifConfig?.description == expectation) + } + + // swiftlint:enable empty_line_after_type_declaration + } +} diff --git a/Tests/PrincipleMacrosTests/Syntax/TypeSyntaxTests.swift b/Tests/PrincipleMacrosTests/Syntax/Extensions/TypeSyntaxTests.swift similarity index 98% rename from Tests/PrincipleMacrosTests/Syntax/TypeSyntaxTests.swift rename to Tests/PrincipleMacrosTests/Syntax/Extensions/TypeSyntaxTests.swift index a2ab13c..8316c29 100644 --- a/Tests/PrincipleMacrosTests/Syntax/TypeSyntaxTests.swift +++ b/Tests/PrincipleMacrosTests/Syntax/Extensions/TypeSyntaxTests.swift @@ -11,7 +11,7 @@ import Testing internal enum TypeSyntaxTests { - struct Basic { + struct Standardization { @Test func optionalLiteral() { @@ -66,9 +66,6 @@ internal enum TypeSyntaxTests { let type: TypeSyntax = "(_ first: String, second: Int, Bool)" #expect(type.standardized.description == "(_ first: String, second: Int, Bool)") } - } - - struct Complex { @Test( arguments: [