diff --git a/.swiftlint.tests.yml b/.swiftlint.tests.yml index ee516cf..504622d 100644 --- a/.swiftlint.tests.yml +++ b/.swiftlint.tests.yml @@ -1,3 +1,4 @@ disabled_rules: - function_body_length + - type_body_length - no_magic_numbers \ No newline at end of file diff --git a/.swiftlint.yml b/.swiftlint.yml index a98bea7..54988a0 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -8,7 +8,7 @@ opt_in_rules: - anonymous_argument_in_multiline_closure - array_init - async_without_await - # attributes + # attributes (swiftformat) # balanced_xctest_lifecycle # closure_body_length - closure_end_indentation @@ -55,9 +55,9 @@ opt_in_rules: - ibinspectable_in_extension - identical_operands - implicit_return - # implicitly_unwrapped_optional + - implicitly_unwrapped_optional # incompatible_concurrency_annotation - # indentation_width + # indentation_width (swiftformat) - joined_default_parameter - last_where - legacy_multiple @@ -67,7 +67,7 @@ opt_in_rules: - local_doc_comment - lower_acl_than_parent # missing_docs - # modifier_order + # modifier_order (swiftformat) - multiline_arguments - multiline_arguments_brackets - multiline_function_chains @@ -144,7 +144,7 @@ opt_in_rules: # vertical_whitespace_opening_braces - weak_delegate - xct_specific_matcher - # yoda_condition + # yoda_condition (swiftformat) analyzer_rules: - capture_variable @@ -173,6 +173,7 @@ identifier_name: excluded: [id, ui, x, y, z, dx, dy, dz] line_length: + ignores_multiline_strings: true ignores_comments: true nesting: @@ -189,10 +190,6 @@ type_contents_order: order: [[case], [type_alias, associated_type], [subtype], [type_property], [instance_property], [ib_inspectable], [ib_outlet], [initializer], [deinitializer], [type_method], [view_life_cycle_method], [ib_action, ib_segue_action], [other_method], [subscript]] custom_rules: - global_actor_attribute_order: - name: "Global actor attribute order" - message: "Global actor should be the first attribute." - regex: "(?-s)(@.+[^,\\s]\\s+@.*Actor\\s)" sendable_attribute_order: name: "Sendable attribute order" message: "Sendable should be the first attribute." @@ -204,4 +201,4 @@ custom_rules: empty_line_after_type_declaration: name: "Empty line after type declaration" message: "Type declaration should start with an empty line." - regex: "( |^)(actor|class|struct|enum|protocol|extension) (?!var)[^\\{]*? \\{(?!\\s*\\}) *\\n? *\\S" + regex: "( |^)(actor|class|struct|enum|protocol|extension) (?!var)[^\\n\\{]*? \\{(?!\\s*\\}) *\\n? *\\S" diff --git a/Sources/PrincipleMacros/Diagnostics/FixIt.swift b/Sources/PrincipleMacros/Diagnostics/FixIt.swift new file mode 100644 index 0000000..55a6c8d --- /dev/null +++ b/Sources/PrincipleMacros/Diagnostics/FixIt.swift @@ -0,0 +1,35 @@ +// +// FixIt.swift +// PrincipleMacros +// +// Created by Kamil Strzelecki on 30/11/2025. +// Copyright © 2025 Kamil Strzelecki. All rights reserved. +// + +import SwiftSyntaxMacros + +extension FixIt { + + public static func remove( + message: String, + oldNode: some SyntaxProtocol + ) -> Self { + .replace( + message: MacroExpansionFixItMessage(message), + oldNode: oldNode, + newNode: "\(oldNode.leadingTrivia)" as TokenSyntax + ) + } + + public static func replace( + message: String, + oldNode: some SyntaxProtocol, + newNode: some SyntaxProtocol + ) -> Self { + .replace( + message: MacroExpansionFixItMessage(message), + oldNode: oldNode, + newNode: newNode.withTrivia(from: oldNode) + ) + } +} diff --git a/Sources/PrincipleMacros/Parameters/ParameterExtractor.swift b/Sources/PrincipleMacros/Parameters/ParameterExtractor.swift index b8896bc..6dad151 100644 --- a/Sources/PrincipleMacros/Parameters/ParameterExtractor.swift +++ b/Sources/PrincipleMacros/Parameters/ParameterExtractor.swift @@ -187,8 +187,7 @@ extension ParameterExtractor { } if expression.is(NilLiteralExprSyntax.self) { - let isolation = DeclModifierSyntax(name: .keyword(.nonisolated)) - return .nonisolated(trimmedModifer: isolation) + return .nonisolated } guard let memberAccessExpression = MemberAccessExprSyntax(expression), diff --git a/Sources/PrincipleMacros/Parsers/Common/Parser.swift b/Sources/PrincipleMacros/Parsers/Common/Parser.swift index d523dcc..2ede4ab 100644 --- a/Sources/PrincipleMacros/Parsers/Common/Parser.swift +++ b/Sources/PrincipleMacros/Parsers/Common/Parser.swift @@ -20,37 +20,19 @@ public protocol Parser { extension Parser { public static func parse( - ifConfig: IfConfigDeclSyntax + declarationGroup: some DeclGroupSyntax ) throws -> ResultsCollection { - try ResultsCollection( - ifConfig.clauses.flatMap { clause in - switch clause.elements { - case let .decls(members): - try parse(members: members) - default: - ResultsCollection() - } - } - ) + let members = declarationGroup.memberBlock.members.flattened + return try parse(members: members) } public static func parse( - members: MemberBlockItemListSyntax + members: some Sequence ) 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) - } + try parse(declaration: member.decl) } ) } - - public static func parse( - memberBlock: MemberBlockSyntax - ) throws -> ResultsCollection { - try parse(members: memberBlock.members) - } } diff --git a/Sources/PrincipleMacros/Parsers/EnumCases/EnumCasesParser.swift b/Sources/PrincipleMacros/Parsers/EnumCases/EnumCasesParser.swift index 09d896a..0172c24 100644 --- a/Sources/PrincipleMacros/Parsers/EnumCases/EnumCasesParser.swift +++ b/Sources/PrincipleMacros/Parsers/EnumCases/EnumCasesParser.swift @@ -14,7 +14,7 @@ public enum EnumCasesParser: Parser { declaration: some DeclSyntaxProtocol ) -> EnumCasesList { guard let declaration = EnumCaseDeclSyntax(declaration) else { - return .init() + return EnumCasesList() } return EnumCasesList( diff --git a/Sources/PrincipleMacros/Parsers/Properties/PropertiesParser.swift b/Sources/PrincipleMacros/Parsers/Properties/PropertiesParser.swift index 106cc98..2c0c13b 100644 --- a/Sources/PrincipleMacros/Parsers/Properties/PropertiesParser.swift +++ b/Sources/PrincipleMacros/Parsers/Properties/PropertiesParser.swift @@ -14,7 +14,7 @@ public enum PropertiesParser: Parser { declaration: some DeclSyntaxProtocol ) throws -> PropertiesList { guard let declaration = VariableDeclSyntax(declaration) else { - return .init() + return PropertiesList() } return try PropertiesList( @@ -42,4 +42,22 @@ public enum PropertiesParser: Parser { } ) } + + public static func parseStandalone( + declaration: some DeclSyntaxProtocol + ) throws -> Property? { + let properties = try parse(declaration: declaration) + guard let first = properties.first else { + return nil + } + + guard properties.count == 1 else { + throw DiagnosticsError( + node: declaration, + message: "Property must have only one binding" + ) + } + + return first + } } diff --git a/Sources/PrincipleMacros/Syntax/Concepts/AccessControlLevel.swift b/Sources/PrincipleMacros/Syntax/Concepts/AccessControlLevel.swift index 105254a..dd6c50e 100644 --- a/Sources/PrincipleMacros/Syntax/Concepts/AccessControlLevel.swift +++ b/Sources/PrincipleMacros/Syntax/Concepts/AccessControlLevel.swift @@ -6,7 +6,7 @@ // Copyright © 2025 Kamil Strzelecki. All rights reserved. // -public enum AccessControlLevel: Int, Hashable, CaseIterable { +public enum AccessControlLevel: Int, Hashable, CaseIterable, Sendable { case `private` case `fileprivate` diff --git a/Sources/PrincipleMacros/Syntax/Concepts/GlobalActorIsolation.swift b/Sources/PrincipleMacros/Syntax/Concepts/GlobalActorIsolation.swift index bd62dd0..3fefc30 100644 --- a/Sources/PrincipleMacros/Syntax/Concepts/GlobalActorIsolation.swift +++ b/Sources/PrincipleMacros/Syntax/Concepts/GlobalActorIsolation.swift @@ -13,6 +13,14 @@ public enum GlobalActorIsolation { case nonisolated(trimmedModifer: DeclModifierSyntax) case isolated(standardizedType: TypeSyntax) + public static var nonisolated: Self { + let modifier = DeclModifierSyntax(name: .keyword(.nonisolated)) + return .nonisolated(trimmedModifer: modifier) + } +} + +extension GlobalActorIsolation { + public var trimmedNonisolatedModifier: DeclModifierSyntax? { switch self { case let .nonisolated(trimmedModifer): @@ -63,7 +71,7 @@ extension GlobalActorIsolation { } private static func _resolved( - in fullContext: some Collection, + in fullContext: some Sequence, preferred: Self? ) -> Self? { if let preferred { diff --git a/Sources/PrincipleMacros/Syntax/Extensions/AttributedTypeSyntax.swift b/Sources/PrincipleMacros/Syntax/Extensions/AttributedTypeSyntax.swift index b2593da..fc53468 100644 --- a/Sources/PrincipleMacros/Syntax/Extensions/AttributedTypeSyntax.swift +++ b/Sources/PrincipleMacros/Syntax/Extensions/AttributedTypeSyntax.swift @@ -12,7 +12,7 @@ extension AttributedTypeSyntax { public init( globalActorIsolation: GlobalActorIsolation?, - baseType: some TypeSyntaxProtocol + baseType: TypeSyntax ) { let specifiers: TypeSpecifierListSyntax = switch globalActorIsolation { @@ -35,4 +35,14 @@ extension AttributedTypeSyntax { baseType: baseType ) } + + public init( + globalActorIsolation: GlobalActorIsolation?, + baseType: some TypeSyntaxProtocol + ) { + self.init( + globalActorIsolation: globalActorIsolation, + baseType: TypeSyntax(baseType) + ) + } } diff --git a/Sources/PrincipleMacros/Syntax/Extensions/IfConfigDeclSyntax+EnclosingIfConfig.swift b/Sources/PrincipleMacros/Syntax/Extensions/IfConfigDeclSyntax+EnclosingIfConfig.swift index 13bbba5..eac96c4 100644 --- a/Sources/PrincipleMacros/Syntax/Extensions/IfConfigDeclSyntax+EnclosingIfConfig.swift +++ b/Sources/PrincipleMacros/Syntax/Extensions/IfConfigDeclSyntax+EnclosingIfConfig.swift @@ -6,7 +6,7 @@ // Copyright © 2025 Kamil Strzelecki. All rights reserved. // -import SwiftSyntax +import SwiftSyntaxMacros extension IfConfigDeclSyntax { @@ -65,6 +65,17 @@ extension MemberBlockItemListSyntax { } return nil } + + @MemberBlockItemListBuilder + public func withIfConfigIfPresent( + from declaration: some DeclSyntaxProtocol + ) -> Self { + if let ifConfig = declaration.applyEnclosingIfConfig(to: .decls(self)) { + ifConfig + } else { + self + } + } } extension MemberBlockItemSyntax { @@ -83,6 +94,20 @@ extension MemberBlockItemSyntax { } } +extension CodeBlockItemListSyntax { + + @CodeBlockItemListBuilder + public func withIfConfigIfPresent( + from declaration: some DeclSyntaxProtocol + ) -> Self { + if let ifConfig = declaration.applyEnclosingIfConfig(to: .statements(self)) { + ifConfig + } else { + self + } + } +} + extension DeclSyntaxProtocol { public var enclosingIfConfig: IfConfigDeclSyntax? { @@ -92,23 +117,11 @@ extension DeclSyntaxProtocol { 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( + fileprivate func applyEnclosingIfConfig( to elements: IfConfigClauseSyntax.Elements ) -> IfConfigDeclSyntax? { if var ancestor = parent?.parent?.parent?.as(IfConfigClauseSyntax.self) { - ancestor = ancestor.with(\.elements, elements) + ancestor = ancestor.with(\.elements, elements.withLeadingNewline) return ancestor.enclosingIfConfig } else { return nil diff --git a/Sources/PrincipleMacros/Syntax/Extensions/MemberBlockItemListSyntax.swift b/Sources/PrincipleMacros/Syntax/Extensions/MemberBlockItemListSyntax.swift new file mode 100644 index 0000000..83f1059 --- /dev/null +++ b/Sources/PrincipleMacros/Syntax/Extensions/MemberBlockItemListSyntax.swift @@ -0,0 +1,41 @@ +// +// MemberBlockItemListSyntax.swift +// PrincipleMacros +// +// Created by Kamil Strzelecki on 30/11/2025. +// Copyright © 2025 Kamil Strzelecki. All rights reserved. +// + +import SwiftSyntaxMacros + +extension MemberBlockItemListSyntax { + + public var flattened: some Sequence { + lazy.flatMap { member in + if let ifConfig = member.decl.as(IfConfigDeclSyntax.self) { + AnySequence(ifConfig.flattenedMembers) + } else { + AnySequence(CollectionOfOne(member)) + } + } + } +} + +extension IfConfigDeclSyntax { + + public var flattenedMembers: some Sequence { + clauses.lazy.flatMap(\.flattenedMembers) + } +} + +extension IfConfigClauseSyntax { + + public var flattenedMembers: some Sequence { + switch elements { + case let .decls(members): + AnySequence(members.flattened) + default: + AnySequence(EmptyCollection()) + } + } +} diff --git a/Sources/PrincipleMacros/Syntax/Extensions/WithModifiersSyntax+AccessControlLevel.swift b/Sources/PrincipleMacros/Syntax/Extensions/WithModifiersSyntax+AccessControlLevel.swift index f9614f5..4db229c 100644 --- a/Sources/PrincipleMacros/Syntax/Extensions/WithModifiersSyntax+AccessControlLevel.swift +++ b/Sources/PrincipleMacros/Syntax/Extensions/WithModifiersSyntax+AccessControlLevel.swift @@ -23,6 +23,23 @@ extension WithModifiersSyntax { } } +extension DeclModifierListSyntax { + + public func withAccessControlLevel(_ level: AccessControlLevel?) -> Self { + var modifiers = filter { modifier in + modifier.accessControlLevel == nil + && modifier.setterAccessControlLevel == nil + } + + if let level { + let modifier = DeclModifierSyntax(name: level.tokenSyntax) + modifiers.insert(modifier, at: modifiers.startIndex) + } + + return modifiers + } +} + extension DeclModifierSyntax { public var accessControlLevel: AccessControlLevel? { diff --git a/Tests/PrincipleMacrosTests/Parsers/PropertiesListTests.swift b/Tests/PrincipleMacrosTests/Parsers/PropertiesListTests.swift index 4214982..4c98af4 100644 --- a/Tests/PrincipleMacrosTests/Parsers/PropertiesListTests.swift +++ b/Tests/PrincipleMacrosTests/Parsers/PropertiesListTests.swift @@ -34,7 +34,7 @@ internal struct PropertiesListTests { """ let classDecl = try #require(decl.as(ClassDeclSyntax.self)) - self.list = try PropertiesParser.parse(memberBlock: classDecl.memberBlock) + self.list = try PropertiesParser.parse(declarationGroup: classDecl) } @Test diff --git a/Tests/PrincipleMacrosTests/Syntax/Concepts/AccessControlLevelTests.swift b/Tests/PrincipleMacrosTests/Syntax/Concepts/AccessControlLevelTests.swift new file mode 100644 index 0000000..c01ac35 --- /dev/null +++ b/Tests/PrincipleMacrosTests/Syntax/Concepts/AccessControlLevelTests.swift @@ -0,0 +1,127 @@ +// +// AccessControlLevelTests.swift +// PrincipleMacros +// +// Created by Kamil Strzelecki on 01/12/2025. +// Copyright © 2025 Kamil Strzelecki. All rights reserved. +// + +@testable import PrincipleMacros +import Testing + +internal struct AccessControlLevelTests { + + @Test( + arguments: [ + Keyword.private, + Keyword.fileprivate, + Keyword.internal, + Keyword.package, + Keyword.public, + Keyword.open + ] + ) + func conversion(_ keyword: Keyword) { + let tokenSyntax = TokenSyntax(.keyword(keyword), presence: .present) + let level = AccessControlLevel(tokenSyntax: tokenSyntax) + #expect(tokenSyntax.description == level?.tokenSyntax.description) + } + + @Test + func comparison() { + #expect(AccessControlLevel.private < .fileprivate) + #expect(AccessControlLevel.fileprivate < .internal) + #expect(AccessControlLevel.internal < .package) + #expect(AccessControlLevel.package < .public) + #expect(AccessControlLevel.public < .open) + } +} + +extension AccessControlLevelTests { + + struct MemberInheritance { + + private func makeDecl(with level: AccessControlLevel) throws -> ClassDeclSyntax { + let decl: DeclSyntax = "\(level)class MyClass {}" + return try #require(decl.as(ClassDeclSyntax.self)) + } + + @Test + func shouldRemovePrivate() throws { + let decl = try makeDecl(with: .private) + let inherited = AccessControlLevel.forMember(of: decl) + #expect(inherited == nil) + } + + @Test + func shouldChangeOpenToPublicByDefault() throws { + let decl = try makeDecl(with: .open) + let inherited = AccessControlLevel.forMember(of: decl) + #expect(inherited == .public) + } + + @Test(arguments: AccessControlLevel.allCases.dropFirst().dropLast()) + func shouldKeep(level: AccessControlLevel) throws { + let decl = try makeDecl(with: level) + let inherited = AccessControlLevel.forMember(of: decl) + #expect(inherited == level) + } + } +} + +extension AccessControlLevelTests { + + struct SiblingInheritance { + + private func makeDecl(with level: AccessControlLevel) throws -> VariableDeclSyntax { + let decl: DeclSyntax = "\(level)var myVar = 123" + return try #require(decl.as(VariableDeclSyntax.self)) + } + + @Test + func shouldChangePrivateToFileprivate() throws { + let decl = try makeDecl(with: .private) + let inherited = AccessControlLevel.forSibling(of: decl) + #expect(inherited == .fileprivate) + } + + @Test + func shouldChangeOpenToPublicByDefault() throws { + let decl = try makeDecl(with: .open) + let inherited = AccessControlLevel.forSibling(of: decl) + #expect(inherited == .public) + } + + @Test(arguments: AccessControlLevel.allCases.dropFirst().dropLast()) + func shouldKeep(level: AccessControlLevel) throws { + let decl = try makeDecl(with: level) + let inherited = AccessControlLevel.forSibling(of: decl) + #expect(inherited == level) + } + } +} + +extension AccessControlLevelTests { + + struct PeerInheritance { + + private func makeDecl(with level: AccessControlLevel) throws -> VariableDeclSyntax { + let decl: DeclSyntax = "\(level)var myVar = 123" + return try #require(decl.as(VariableDeclSyntax.self)) + } + + @Test + func shouldChangeOpenToPublicByDefault() throws { + let decl = try makeDecl(with: .open) + let inherited = AccessControlLevel.forPeer(of: decl) + #expect(inherited == .public) + } + + @Test(arguments: AccessControlLevel.allCases.dropLast()) + func shouldKeep(level: AccessControlLevel) throws { + let decl = try makeDecl(with: level) + let inherited = AccessControlLevel.forPeer(of: decl) + #expect(inherited == level) + } + } +} diff --git a/Tests/PrincipleMacrosTests/Syntax/Extensions/IfConfigDeclSyntaxTests.swift b/Tests/PrincipleMacrosTests/Syntax/Extensions/IfConfigDeclSyntaxTests.swift index 679d4a9..2f848e9 100644 --- a/Tests/PrincipleMacrosTests/Syntax/Extensions/IfConfigDeclSyntaxTests.swift +++ b/Tests/PrincipleMacrosTests/Syntax/Extensions/IfConfigDeclSyntaxTests.swift @@ -15,7 +15,7 @@ internal enum IfConfigDeclSyntaxTests { private func parseLastProperty(in decl: DeclSyntax) throws -> Property { let classDecl = try #require(decl.as(ClassDeclSyntax.self)) - let properties = try PropertiesParser.parse(memberBlock: classDecl.memberBlock) + let properties = try PropertiesParser.parse(declarationGroup: classDecl) return try #require(properties.last) } @@ -130,7 +130,7 @@ internal enum IfConfigDeclSyntaxTests { } @Test - func applyToNewMembers() throws { + func applyToMemberBlock() throws { let decl: DeclSyntax = """ class MyClass { #if DEBUG @@ -159,12 +159,12 @@ internal enum IfConfigDeclSyntaxTests { """ let property = try parseLastProperty(in: decl) - let ifConfig = property.underlying.applyingEnclosingIfConfig(to: newMembers) - #expect(ifConfig?.description == expectation) + let ifConfig = newMembers.withIfConfigIfPresent(from: property.underlying) + #expect(ifConfig.description == expectation) } @Test - func applyToNewStatements() throws { + func applyToCodeBlock() throws { let decl: DeclSyntax = """ class MyClass { #if DEBUG @@ -193,8 +193,8 @@ internal enum IfConfigDeclSyntaxTests { """ let property = try parseLastProperty(in: decl) - let ifConfig = property.underlying.applyingEnclosingIfConfig(to: newStatements) - #expect(ifConfig?.description == expectation) + let ifConfig = newStatements.withIfConfigIfPresent(from: property.underlying) + #expect(ifConfig.description == expectation) } // swiftlint:enable empty_line_after_type_declaration diff --git a/Tests/PrincipleMacrosTests/Syntax/Extensions/MemberBlockItemListSyntaxTests.swift b/Tests/PrincipleMacrosTests/Syntax/Extensions/MemberBlockItemListSyntaxTests.swift new file mode 100644 index 0000000..061db8b --- /dev/null +++ b/Tests/PrincipleMacrosTests/Syntax/Extensions/MemberBlockItemListSyntaxTests.swift @@ -0,0 +1,64 @@ +// +// MemberBlockItemListSyntaxTests.swift +// PrincipleMacros +// +// Created by Kamil Strzelecki on 01/12/2025. +// Copyright © 2025 Kamil Strzelecki. All rights reserved. +// + +@testable import PrincipleMacros +import Testing + +internal enum MemberBlockItemListSyntaxTests { + + struct Flattening { + + @Test + func members() { + let members: MemberBlockItemListSyntax = """ + let a = "" + var b = 123 + func c() {} + """ + + let flattened = members.flattened + #expect(members.elementsEqual(flattened)) + } + + @Test + func ifConfig() { + let members: MemberBlockItemListSyntax = """ + let a = "" + #if os(macOS) + var b = 123 + #else + func c() {} + #endif + func d() {} + """ + + let flattened = Array(members.flattened) + #expect(flattened.count == 4) + } + + @Test + func nestedIfConfig() { + let members: MemberBlockItemListSyntax = """ + let a = "" + #if os(macOS) + var b = 123 + #else + #if os(iOS) + func c() {} + #else + func d() {} + #endif + #endif + func e() {} + """ + + let flattened = Array(members.flattened) + #expect(flattened.count == 5) + } + } +}