From 073bac53780e50279590228f68a473de998b4d9e Mon Sep 17 00:00:00 2001 From: plx Date: Tue, 29 Apr 2025 17:16:20 -0500 Subject: [PATCH 1/4] Addressed deprecation warnings for swift-syntax. --- .../MemberwiseInitMacros/Macros/MemberwiseInitMacro.swift | 3 ++- .../Macros/Support/DeprecationDiagnostics.swift | 2 +- .../MemberwiseInitMacros/Macros/Support/Diagnostics.swift | 5 ++--- .../Macros/Support/ExprTypeInference.swift | 4 ++-- .../Macros/Support/InlinabilityAttribute.swift | 0 .../Macros/UncheckedMemberwiseInitMacro.swift | 1 - Tests/MacroTestingTests/MacroExamples/AddAsyncMacro.swift | 2 +- 7 files changed, 8 insertions(+), 9 deletions(-) create mode 100644 Sources/MemberwiseInitMacros/Macros/Support/InlinabilityAttribute.swift diff --git a/Sources/MemberwiseInitMacros/Macros/MemberwiseInitMacro.swift b/Sources/MemberwiseInitMacros/Macros/MemberwiseInitMacro.swift index b60c1a4..bb1c9e0 100644 --- a/Sources/MemberwiseInitMacros/Macros/MemberwiseInitMacro.swift +++ b/Sources/MemberwiseInitMacros/Macros/MemberwiseInitMacro.swift @@ -2,9 +2,10 @@ import SwiftCompilerPlugin import SwiftDiagnostics import SwiftSyntax import SwiftSyntaxBuilder -import SwiftSyntaxMacroExpansion import SwiftSyntaxMacros + + public struct InitMacro: PeerMacro { public static func expansion( of node: SwiftSyntax.AttributeSyntax, diff --git a/Sources/MemberwiseInitMacros/Macros/Support/DeprecationDiagnostics.swift b/Sources/MemberwiseInitMacros/Macros/Support/DeprecationDiagnostics.swift index 4a7b95f..2a2b06a 100644 --- a/Sources/MemberwiseInitMacros/Macros/Support/DeprecationDiagnostics.swift +++ b/Sources/MemberwiseInitMacros/Macros/Support/DeprecationDiagnostics.swift @@ -1,6 +1,6 @@ import SwiftDiagnostics import SwiftSyntax -import SwiftSyntaxMacroExpansion +import SwiftSyntaxMacros func deprecationDiagnostics( node: AttributeSyntax, diff --git a/Sources/MemberwiseInitMacros/Macros/Support/Diagnostics.swift b/Sources/MemberwiseInitMacros/Macros/Support/Diagnostics.swift index f09df40..dd04bac 100644 --- a/Sources/MemberwiseInitMacros/Macros/Support/Diagnostics.swift +++ b/Sources/MemberwiseInitMacros/Macros/Support/Diagnostics.swift @@ -1,6 +1,6 @@ import SwiftDiagnostics import SwiftSyntax -import SwiftSyntaxMacroExpansion +import SwiftSyntaxMacros // MARK: - Diagnose VariableDeclSyntax @@ -496,8 +496,7 @@ extension VariableDeclSyntax { newFirstBinding.typeAnnotation = TypeAnnotationSyntax( colon: .colonToken(trailingTrivia: .space), type: inferredTypeSyntax - ?? MissingTypeSyntax(placeholder: TokenSyntax(stringLiteral: "\u{3C}#Type#\u{3E}")) - .as(TypeSyntax.self)! + ?? TypeSyntax.self(MissingTypeSyntax(placeholder: TokenSyntax(stringLiteral: "\u{3C}#Type#\u{3E}")))! ) newFirstBinding.pattern = newFirstBinding.pattern.trimmed } diff --git a/Sources/MemberwiseInitMacros/Macros/Support/ExprTypeInference.swift b/Sources/MemberwiseInitMacros/Macros/Support/ExprTypeInference.swift index d5aea19..fb60dba 100644 --- a/Sources/MemberwiseInitMacros/Macros/Support/ExprTypeInference.swift +++ b/Sources/MemberwiseInitMacros/Macros/Support/ExprTypeInference.swift @@ -221,8 +221,8 @@ extension ExprSyntax { case .infixOperatorExpr: guard let infixOperatorExpr = self.as(InfixOperatorExprSyntax.self), - let lhsType = infixOperatorExpr.leftOperand.as(ExprSyntax.self)?.inferredType, - let rhsType = infixOperatorExpr.rightOperand.as(ExprSyntax.self)?.inferredType, + let lhsType = ExprSyntax(infixOperatorExpr.leftOperand)?.inferredType, + let rhsType = ExprSyntax(infixOperatorExpr.rightOperand)?.inferredType, let operation = InfixOperator(rawValue: infixOperatorExpr.operator.trimmedDescription), let inferredType = resultTypeOfInfixOperation( lhs: lhsType, diff --git a/Sources/MemberwiseInitMacros/Macros/Support/InlinabilityAttribute.swift b/Sources/MemberwiseInitMacros/Macros/Support/InlinabilityAttribute.swift new file mode 100644 index 0000000..e69de29 diff --git a/Sources/MemberwiseInitMacros/Macros/UncheckedMemberwiseInitMacro.swift b/Sources/MemberwiseInitMacros/Macros/UncheckedMemberwiseInitMacro.swift index eca75f1..4eea802 100644 --- a/Sources/MemberwiseInitMacros/Macros/UncheckedMemberwiseInitMacro.swift +++ b/Sources/MemberwiseInitMacros/Macros/UncheckedMemberwiseInitMacro.swift @@ -2,7 +2,6 @@ import SwiftCompilerPlugin import SwiftDiagnostics import SwiftSyntax import SwiftSyntaxBuilder -import SwiftSyntaxMacroExpansion import SwiftSyntaxMacros public struct UncheckedMemberwiseInitMacro: MemberMacro { diff --git a/Tests/MacroTestingTests/MacroExamples/AddAsyncMacro.swift b/Tests/MacroTestingTests/MacroExamples/AddAsyncMacro.swift index 13d32d6..e52f1dc 100644 --- a/Tests/MacroTestingTests/MacroExamples/AddAsyncMacro.swift +++ b/Tests/MacroTestingTests/MacroExamples/AddAsyncMacro.swift @@ -135,7 +135,7 @@ public struct AddAsyncMacro: PeerMacro { funcDecl.signature.effectSpecifiers = FunctionEffectSpecifiersSyntax( leadingTrivia: .space, asyncSpecifier: .keyword(.async), - throwsSpecifier: isResultReturn ? .keyword(.throws) : nil + throwsClause: isResultReturn ? ThrowsClauseSyntax(throwsSpecifier: .keyword(.throws)) : nil ) // add result type From 5d39f170b9c3d9b010b2898cf22e2a5894eb6ff4 Mon Sep 17 00:00:00 2001 From: plx Date: Tue, 29 Apr 2025 17:16:40 -0500 Subject: [PATCH 2/4] Extracted unnamed-parameter extraction to extension method. --- .../Macros/Support/SyntaxHelpers.swift | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/Sources/MemberwiseInitMacros/Macros/Support/SyntaxHelpers.swift b/Sources/MemberwiseInitMacros/Macros/Support/SyntaxHelpers.swift index 7a317fe..a599d3e 100644 --- a/Sources/MemberwiseInitMacros/Macros/Support/SyntaxHelpers.swift +++ b/Sources/MemberwiseInitMacros/Macros/Support/SyntaxHelpers.swift @@ -1,5 +1,24 @@ import SwiftSyntax +extension AttributeSyntax { + + func firstUnlabeledValue(interpretableAs type: T.Type) -> T? where T: RawRepresentable { + guard let arguments = arguments?.as(LabeledExprListSyntax.self) + else { return nil } + + // NB: Search for the first argument whose name matches an access level name + for labeledExprSyntax in arguments { + if let identifier = labeledExprSyntax.expression.as(MemberAccessExprSyntax.self)?.declName, + let accessLevel = T(rawValue: identifier.baseName.trimmedDescription) + { + return accessLevel + } + } + + return nil + } +} + extension VariableDeclSyntax { func modifiersExclude(_ keywords: [Keyword]) -> Bool { return !self.modifiers.containsAny(of: keywords.map { TokenSyntax.keyword($0) }) @@ -63,7 +82,7 @@ extension VariableDeclSyntax { var isComputedProperty: Bool { guard self.bindings.count == 1, - let binding = self.bindings.first?.as(PatternBindingSyntax.self) + let binding = self.bindings.first else { return false } return self.bindingSpecifier.tokenKind == .keyword(.var) && binding.isComputedProperty From 9f95300d2177e446da131bc79271b1426176bb90 Mon Sep 17 00:00:00 2001 From: plx Date: Tue, 29 Apr 2025 17:16:57 -0500 Subject: [PATCH 3/4] Introduced inlinability concept. --- Sources/MemberwiseInit/MemberwiseInit.swift | 9 +++ .../Macros/MemberwiseInitMacro.swift | 59 ++++++++++++++----- .../Support/InlinabilityAttribute.swift | 7 +++ .../Support/MemberwiseInitFormatter.swift | 14 ++++- .../Macros/UncheckedMemberwiseInitMacro.swift | 3 + 5 files changed, 77 insertions(+), 15 deletions(-) diff --git a/Sources/MemberwiseInit/MemberwiseInit.swift b/Sources/MemberwiseInit/MemberwiseInit.swift index 73ce0e5..430e8d0 100644 --- a/Sources/MemberwiseInit/MemberwiseInit.swift +++ b/Sources/MemberwiseInit/MemberwiseInit.swift @@ -7,11 +7,17 @@ public enum AccessLevelConfig { case `open` } +public enum InlinabilityConfig { + case usableFromInline + case inlinable +} + // MARK: @MemberwiseInit macro @attached(member, names: named(init)) public macro MemberwiseInit( _ accessLevel: AccessLevelConfig, + _ inlinability: InlinabilityConfig? = nil, _deunderscoreParameters: Bool? = nil, _optionalsDefaultNil: Bool? = nil ) = @@ -22,6 +28,7 @@ public macro MemberwiseInit( @attached(member, names: named(init)) public macro MemberwiseInit( + _ inlinability: InlinabilityConfig? = nil, _deunderscoreParameters: Bool? = nil, _optionalsDefaultNil: Bool? = nil ) = @@ -32,6 +39,7 @@ public macro MemberwiseInit( @attached(member, names: named(init)) public macro _UncheckedMemberwiseInit( + _ inlinability: InlinabilityConfig? = nil, _deunderscoreParameters: Bool? = nil, _optionalsDefaultNil: Bool? = nil ) = @@ -43,6 +51,7 @@ public macro _UncheckedMemberwiseInit( @attached(member, names: named(init)) public macro _UncheckedMemberwiseInit( _ accessLevel: AccessLevelConfig, + _ inlinability: InlinabilityConfig? = nil, _deunderscoreParameters: Bool? = nil, _optionalsDefaultNil: Bool? = nil ) = diff --git a/Sources/MemberwiseInitMacros/Macros/MemberwiseInitMacro.swift b/Sources/MemberwiseInitMacros/Macros/MemberwiseInitMacro.swift index bb1c9e0..265b026 100644 --- a/Sources/MemberwiseInitMacros/Macros/MemberwiseInitMacro.swift +++ b/Sources/MemberwiseInitMacros/Macros/MemberwiseInitMacro.swift @@ -36,6 +36,8 @@ public struct MemberwiseInitMacro: MemberMacro { .forEach(context.diagnose) let configuredAccessLevel: AccessLevelModifier? = extractConfiguredAccessLevel(from: node) + let inlinability: InlinabilityAttribute? = extractInlinabilityAttribute(from: node) + let optionalsDefaultNil: Bool? = extractLabeledBoolArgument("_optionalsDefaultNil", from: node) let deunderscoreParameters: Bool = @@ -47,37 +49,66 @@ public struct MemberwiseInitMacro: MemberMacro { targetAccessLevel: accessLevel ) diagnostics.forEach { context.diagnose($0) } + if let incompatibilityDiagnostic = incompatibilityDiagnosticBetween( + accessLevel: accessLevel, + inlinability: inlinability, + in: node + ) { + context.diagnose(incompatibilityDiagnostic) + } return [ DeclSyntax( MemberwiseInitFormatter.formatInitializer( properties: properties, accessLevel: accessLevel, + inlinability: inlinability, deunderscoreParameters: deunderscoreParameters, optionalsDefaultNil: optionalsDefaultNil ) ) ] } - + static func extractConfiguredAccessLevel( from node: AttributeSyntax ) -> AccessLevelModifier? { - guard let arguments = node.arguments?.as(LabeledExprListSyntax.self) - else { return nil } - - // NB: Search for the first argument whose name matches an access level name - for labeledExprSyntax in arguments { - if let identifier = labeledExprSyntax.expression.as(MemberAccessExprSyntax.self)?.declName, - let accessLevel = AccessLevelModifier(rawValue: identifier.baseName.trimmedDescription) - { - return accessLevel - } + node.firstUnlabeledValue(interpretableAs: AccessLevelModifier.self) + } + + static func extractInlinabilityAttribute( + from node: AttributeSyntax + ) -> InlinabilityAttribute? { + node.firstUnlabeledValue(interpretableAs: InlinabilityAttribute.self) + } + + static func incompatibilityDiagnosticBetween( + accessLevel: AccessLevelModifier, + inlinability: InlinabilityAttribute?, + in node: AttributeSyntax + ) -> Diagnostic? { + guard let inlinability else { return nil } + switch (accessLevel, inlinability) { + case + (.open, .inlinable), + (.public, .inlinable), + (.package, .inlinable), + (.package, .usableFromInline), + (.internal, .inlinable), + (.internal, .usableFromInline): + return nil + default: + return Diagnostic( + node: node, + message: MacroExpansionErrorMessage( + """ + Inlinability `@\(inlinability.rawValue)` is incompatible-with access-level `\(accessLevel)`! + """ + ) + ) } - - return nil } - + static func extractLabeledBoolArgument( _ label: String, from node: AttributeSyntax diff --git a/Sources/MemberwiseInitMacros/Macros/Support/InlinabilityAttribute.swift b/Sources/MemberwiseInitMacros/Macros/Support/InlinabilityAttribute.swift index e69de29..0cdf9c0 100644 --- a/Sources/MemberwiseInitMacros/Macros/Support/InlinabilityAttribute.swift +++ b/Sources/MemberwiseInitMacros/Macros/Support/InlinabilityAttribute.swift @@ -0,0 +1,7 @@ +import SwiftSyntax + +enum InlinabilityAttribute: String, Hashable, CaseIterable, Sendable { + case usableFromInline + case inlinable + +} diff --git a/Sources/MemberwiseInitMacros/Macros/Support/MemberwiseInitFormatter.swift b/Sources/MemberwiseInitMacros/Macros/Support/MemberwiseInitFormatter.swift index 6113d00..ed69340 100644 --- a/Sources/MemberwiseInitMacros/Macros/Support/MemberwiseInitFormatter.swift +++ b/Sources/MemberwiseInitMacros/Macros/Support/MemberwiseInitFormatter.swift @@ -5,6 +5,7 @@ struct MemberwiseInitFormatter { static func formatInitializer( properties: [MemberProperty], accessLevel: AccessLevelModifier, + inlinability: InlinabilityAttribute?, deunderscoreParameters: Bool, optionalsDefaultNil: Bool? ) -> InitializerDeclSyntax { @@ -14,8 +15,19 @@ struct MemberwiseInitFormatter { optionalsDefaultNil: optionalsDefaultNil, accessLevel: accessLevel ) + + var modifierComponents: [String] = [] + if let inlinability = inlinability { + modifierComponents.append("@\(inlinability.rawValue)") + } + modifierComponents.append("\(accessLevel.rawValue)") + let modifiers = modifierComponents.joined(separator: "\n") + // goal: + // + // @inlinable + // public foo - let formattedInitSignature = "\n\(accessLevel) init(\(formattedParameters))" + let formattedInitSignature = "\n\(modifiers) init(\(formattedParameters))" return try! InitializerDeclSyntax(SyntaxNodeString(stringLiteral: formattedInitSignature)) { CodeBlockItemListSyntax( diff --git a/Sources/MemberwiseInitMacros/Macros/UncheckedMemberwiseInitMacro.swift b/Sources/MemberwiseInitMacros/Macros/UncheckedMemberwiseInitMacro.swift index 4eea802..6c6406b 100644 --- a/Sources/MemberwiseInitMacros/Macros/UncheckedMemberwiseInitMacro.swift +++ b/Sources/MemberwiseInitMacros/Macros/UncheckedMemberwiseInitMacro.swift @@ -22,6 +22,8 @@ public struct UncheckedMemberwiseInitMacro: MemberMacro { let accessLevel = MemberwiseInitMacro.extractConfiguredAccessLevel(from: node) ?? .internal + let inlinability = + MemberwiseInitMacro.extractInlinabilityAttribute(from: node) let optionalsDefaultNil: Bool? = MemberwiseInitMacro.extractLabeledBoolArgument("_optionalsDefaultNil", from: node) let deunderscoreParameters: Bool = @@ -36,6 +38,7 @@ public struct UncheckedMemberwiseInitMacro: MemberMacro { MemberwiseInitFormatter.formatInitializer( properties: properties, accessLevel: accessLevel, + inlinability: inlinability, deunderscoreParameters: deunderscoreParameters, optionalsDefaultNil: optionalsDefaultNil ) From c96a8254d2e98b5c1f930470e9aed5ac4164988c Mon Sep 17 00:00:00 2001 From: plx Date: Fri, 2 May 2025 16:50:31 -0500 Subject: [PATCH 4/4] Fixing up FixIts. --- Sources/MemberwiseInitClient/main.swift | 37 ++++ .../Macros/MemberwiseInitMacro.swift | 25 +-- .../Macros/Support/Diagnostics.swift | 165 +++++++++++++++++- .../Macros/Support/SyntaxHelpers.swift | 81 ++++++++- 4 files changed, 282 insertions(+), 26 deletions(-) diff --git a/Sources/MemberwiseInitClient/main.swift b/Sources/MemberwiseInitClient/main.swift index 65f1327..7aec0ec 100644 --- a/Sources/MemberwiseInitClient/main.swift +++ b/Sources/MemberwiseInitClient/main.swift @@ -122,6 +122,43 @@ public struct InferType { var dictionaryAs = ["foo": 1, 3: "bar"] as [AnyHashable: Any] } +@MemberwiseInit(.inlinable) +public struct InlinableInit_Default { + public let foo: String + + @Init(.ignore) + private var ignored: Int = 7 +} +let _ = InlinableInit_Default(foo: "bar") + +@MemberwiseInit(.internal, .inlinable) +public struct InlinableInit_Internal { + public let foo: String + + @Init(.ignore) + private var ignored: Int = 7 +} +let _ = InlinableInit_Internal(foo: "bar") + +//@MemberwiseInit(.fileprivate, .inlinable) +//internal struct InlinableInit_FilePrivate { +// fileprivate let foo: String +// +// @Init(.ignore) +// private var ignored: Int = 7 +//} +////let _ = InlinableInit_FilePrivate(foo: "bar") + +@MemberwiseInit(.package, .usableFromInline) +public struct InlinableInit_Package { + public let foo: String + + @Init(.ignore) + private var ignored: Int = 7 +} +let _ = InlinableInit_Package(foo: "bar") + + // - MARK: Usage tour public typealias SimpleClosure = () -> Void diff --git a/Sources/MemberwiseInitMacros/Macros/MemberwiseInitMacro.swift b/Sources/MemberwiseInitMacros/Macros/MemberwiseInitMacro.swift index 265b026..fd182b8 100644 --- a/Sources/MemberwiseInitMacros/Macros/MemberwiseInitMacro.swift +++ b/Sources/MemberwiseInitMacros/Macros/MemberwiseInitMacro.swift @@ -31,7 +31,7 @@ public struct MemberwiseInitMacro: MemberMacro { """ ) } - + deprecationDiagnostics(node: node, declaration: decl) .forEach(context.diagnose) @@ -52,7 +52,8 @@ public struct MemberwiseInitMacro: MemberMacro { if let incompatibilityDiagnostic = incompatibilityDiagnosticBetween( accessLevel: accessLevel, inlinability: inlinability, - in: node + in: node, + typeAccessLevel: decl.declAccessLevel ) { context.diagnose(incompatibilityDiagnostic) } @@ -73,24 +74,24 @@ public struct MemberwiseInitMacro: MemberMacro { static func extractConfiguredAccessLevel( from node: AttributeSyntax ) -> AccessLevelModifier? { - node.firstUnlabeledValue(interpretableAs: AccessLevelModifier.self) + node.firstArgumentValue(interpretableAs: AccessLevelModifier.self) } static func extractInlinabilityAttribute( from node: AttributeSyntax ) -> InlinabilityAttribute? { - node.firstUnlabeledValue(interpretableAs: InlinabilityAttribute.self) + node.firstArgumentValue(interpretableAs: InlinabilityAttribute.self) } static func incompatibilityDiagnosticBetween( accessLevel: AccessLevelModifier, inlinability: InlinabilityAttribute?, - in node: AttributeSyntax + in node: AttributeSyntax, + typeAccessLevel: AccessLevelModifier ) -> Diagnostic? { guard let inlinability else { return nil } switch (accessLevel, inlinability) { case - (.open, .inlinable), (.public, .inlinable), (.package, .inlinable), (.package, .usableFromInline), @@ -98,13 +99,17 @@ public struct MemberwiseInitMacro: MemberMacro { (.internal, .usableFromInline): return nil default: + // NOTE: + // the real issue with (.open, .inlinable) is specific + // to init: `init` can't be `open`. return Diagnostic( node: node, message: MacroExpansionErrorMessage( - """ - Inlinability `@\(inlinability.rawValue)` is incompatible-with access-level `\(accessLevel)`! - """ - ) + """ + Inlinability '.\(inlinability.rawValue)' is incompatible-with access-level '.\(accessLevel)'! + """ + ), + fixIts: node.allInlinabilityFixIts(typeAccessLevel: typeAccessLevel) ) } } diff --git a/Sources/MemberwiseInitMacros/Macros/Support/Diagnostics.swift b/Sources/MemberwiseInitMacros/Macros/Support/Diagnostics.swift index dd04bac..0f3dab5 100644 --- a/Sources/MemberwiseInitMacros/Macros/Support/Diagnostics.swift +++ b/Sources/MemberwiseInitMacros/Macros/Support/Diagnostics.swift @@ -1,5 +1,6 @@ import SwiftDiagnostics import SwiftSyntax +import SwiftSyntaxBuilder import SwiftSyntaxMacros // MARK: - Diagnose VariableDeclSyntax @@ -100,7 +101,8 @@ private func diagnoseMemberModifiers( return Diagnostic( node: modifier, message: MacroExpansionWarningMessage( - "@\(attributeName) can't be applied to 'static' members"), + "@\(attributeName) can't be applied to 'static' members" + ), fixIts: [variable.fixItRemoveCustomInit].compactMap { $0 } ) } @@ -255,10 +257,12 @@ private func diagnoseAccessibilityLeak( return FixIt( message: MacroExpansionFixItMessage( - "Add '@\(customAttribute.attributeName.trimmedDescription)(.\(targetAccessLevel))'"), + "Add '@\(customAttribute.attributeName.trimmedDescription)(.\(targetAccessLevel))'" + ), changes: [ FixIt.Change.replace( - oldNode: Syntax(variable), newNode: Syntax(newVariable) + oldNode: Syntax(variable), + newNode: Syntax(newVariable) ) ] ) @@ -314,7 +318,8 @@ private func diagnoseAccessibilityLeak( message: MacroExpansionFixItMessage(message), changes: [ FixIt.Change.replace( - oldNode: Syntax(variable), newNode: Syntax(newVariable) + oldNode: Syntax(variable), + newNode: Syntax(newVariable) ) ] ) @@ -355,7 +360,8 @@ private func diagnoseAccessibilityLeak( message: MacroExpansionFixItMessage(message), changes: [ FixIt.Change.replace( - oldNode: Syntax(variable), newNode: Syntax(newVariable) + oldNode: Syntax(variable), + newNode: Syntax(newVariable) ) ] ) @@ -457,7 +463,8 @@ extension VariableDeclSyntax { changes: [ FixIt.Change.replace( oldNode: Syntax(customAttribute), - newNode: Syntax(newAttribute)) + newNode: Syntax(newAttribute) + ) ] ) } @@ -475,7 +482,8 @@ extension VariableDeclSyntax { message: MacroExpansionFixItMessage("Remove '\(customAttribute.trimmedDescription)'"), changes: [ FixIt.Change.replace( - oldNode: Syntax(self), newNode: Syntax(newVariable) + oldNode: Syntax(self), + newNode: Syntax(newVariable) ) ] ) @@ -496,7 +504,9 @@ extension VariableDeclSyntax { newFirstBinding.typeAnnotation = TypeAnnotationSyntax( colon: .colonToken(trailingTrivia: .space), type: inferredTypeSyntax - ?? TypeSyntax.self(MissingTypeSyntax(placeholder: TokenSyntax(stringLiteral: "\u{3C}#Type#\u{3E}")))! + ?? TypeSyntax.self( + MissingTypeSyntax(placeholder: TokenSyntax(stringLiteral: "\u{3C}#Type#\u{3E}")) + )! ) newFirstBinding.pattern = newFirstBinding.pattern.trimmed } @@ -510,7 +520,8 @@ extension VariableDeclSyntax { ), changes: [ FixIt.Change.replace( - oldNode: Syntax(self), newNode: Syntax(newNode) + oldNode: Syntax(self), + newNode: Syntax(newNode) ) ] ) @@ -538,3 +549,139 @@ extension LabeledExprListSyntax { } } } + +// MARK: - Diagnose AttributeSyntax + +extension AttributeSyntax { + + func allInlinabilityFixIts(typeAccessLevel: AccessLevelModifier) -> [FixIt] { + // NB: returning nil when empty to be consistent with Diagnostic's choice of default argument value + var result: [FixIt] = [] + if let fixItRemoveInlinability { + result.append(fixItRemoveInlinability) + } + if let fixItReplaceUsableFromInlineWithInlinable { + result.append(fixItReplaceUsableFromInlineWithInlinable) + } + if let fixItsMakeAccessLevelCompatibleWithInlinabilityChoice = fixItsMakeAccessLevelCompatibleWithInlinabilityChoice(typeAccessLevel: typeAccessLevel) { + result.append(contentsOf: fixItsMakeAccessLevelCompatibleWithInlinabilityChoice) + } + + return result + } + + var fixItRemoveInlinability: FixIt? { + guard + let inlinability = firstArgumentValue( + interpretableAs: InlinabilityAttribute.self + ), + case .argumentList(let originalArgumentList) = arguments + else { + return nil + } + + return FixIt( + message: MacroExpansionFixItMessage("Remove '.\(inlinability)'."), + changes: [ + .replace( + oldNode: Syntax(self), + newNode: Syntax( + self.with( + \.arguments, + .argumentList( + originalArgumentList.removingFirstArgumentValue( + interpretableAs: InlinabilityAttribute.self + ) + ) + ) + ) + ) + ] + ) + } + + var fixItReplaceUsableFromInlineWithInlinable: FixIt? { + guard + let inlinability = firstArgumentValue( + interpretableAs: InlinabilityAttribute.self + ), + inlinability == .usableFromInline, + let accessLevel = firstArgumentValue(interpretableAs: AccessLevelModifier.self), + [.public, .open].contains(accessLevel), + case .argumentList(let originalArgumentList) = arguments + else { + return nil + } + + return FixIt( + message: MacroExpansionFixItMessage( + "Change '.\(inlinability)' to '.inlinable'." + ), + changes: [ + .replace( + oldNode: Syntax(self), + newNode: Syntax( + self.with( + \.arguments, + .argumentList( + originalArgumentList.replacingFirstArgument( + interpretableAs: InlinabilityAttribute.self, + with: LabeledExprSyntax(expression: ExprSyntax(".inlinable")) + ) + ) + ) + ) + ) + ] + ) + } + + func fixItsMakeAccessLevelCompatibleWithInlinabilityChoice(typeAccessLevel: AccessLevelModifier) -> [FixIt]? { + guard + let inlinability = firstArgumentValue(interpretableAs: InlinabilityAttribute.self), + let originalAccessLevel = firstArgumentValue( + interpretableAs: AccessLevelModifier.self + ), + [.private, .fileprivate].contains(originalAccessLevel), + case .argumentList(let originalArgumentList) = arguments + else { + return nil + } + + let accessLevels: [AccessLevelModifier] + switch inlinability { + case .usableFromInline: + accessLevels = [.internal, .package] + case .inlinable: + accessLevels = [.internal, .package, .public, .open] + } + + return accessLevels + .lazy + .filter { $0 <= typeAccessLevel } + .map { accessLevel in + FixIt( + message: MacroExpansionFixItMessage( + "Change '.\(originalAccessLevel)' to '.\(accessLevel)'." + ), + changes: [ + .replace( + oldNode: Syntax(self), + newNode: Syntax( + self.with( + \.arguments, + .argumentList( + originalArgumentList.replacingFirstArgument( + interpretableAs: AccessLevelModifier.self, + with: LabeledExprSyntax(expression: ExprSyntax(".\(raw: accessLevel)")) + ) + ) + ) + ) + ) + ] + ) + } + } + +} diff --git a/Sources/MemberwiseInitMacros/Macros/Support/SyntaxHelpers.swift b/Sources/MemberwiseInitMacros/Macros/Support/SyntaxHelpers.swift index a599d3e..3c96107 100644 --- a/Sources/MemberwiseInitMacros/Macros/Support/SyntaxHelpers.swift +++ b/Sources/MemberwiseInitMacros/Macros/Support/SyntaxHelpers.swift @@ -2,16 +2,13 @@ import SwiftSyntax extension AttributeSyntax { - func firstUnlabeledValue(interpretableAs type: T.Type) -> T? where T: RawRepresentable { - guard let arguments = arguments?.as(LabeledExprListSyntax.self) - else { return nil } + func firstArgumentValue(interpretableAs type: T.Type) -> T? where T: RawRepresentable { + guard case .argumentList(let arguments) = arguments else { return nil } // NB: Search for the first argument whose name matches an access level name for labeledExprSyntax in arguments { - if let identifier = labeledExprSyntax.expression.as(MemberAccessExprSyntax.self)?.declName, - let accessLevel = T(rawValue: identifier.baseName.trimmedDescription) - { - return accessLevel + if let interpretedValue = labeledExprSyntax.value(interpretedAs: type) { + return interpretedValue } } @@ -19,6 +16,76 @@ extension AttributeSyntax { } } +extension LabeledExprSyntax { + + func value(interpretedAs type: T.Type) -> T? where T: RawRepresentable { + guard let identifier = expression.as(MemberAccessExprSyntax.self)?.declName else { + return nil + } + + return T(rawValue: identifier.baseName.trimmedDescription) + } +} + +extension LabeledExprListSyntax { + + func removingFirstArgumentValue(interpretableAs type: T.Type) -> Self where T: RawRepresentable { + removingFirstItem { labeledExprSyntax in + labeledExprSyntax.value(interpretedAs: type) != nil + } + } + + func replacingFirstArgument( + interpretableAs type: T.Type, + with value: LabeledExprSyntax + ) -> Self where T: RawRepresentable { + var result = Self() + var hasFoundItemToReplace = false + for node in self { + if !hasFoundItemToReplace, let _ = node.value(interpretedAs: type) { + hasFoundItemToReplace = true + result.append(value) + } else { + result.append(node) + } + } + + return result.withFixedInterItemCommas() + } + + func removingFirstItem(where predicate: (Element) throws -> Bool) rethrows -> Self { + var result = Self() + var hasFoundItemToRemove = false + for node in self { + if !hasFoundItemToRemove, try predicate(node) { + hasFoundItemToRemove = true + continue + } + result.append(node) + } + + return result.withFixedInterItemCommas() + } + + func withFixedInterItemCommas() -> LabeledExprListSyntax { + guard !isEmpty else { + return self + } + + let finalIndex = count - 1 + var result = Self() + for (index, node) in enumerated() { + if index == finalIndex { + result.append(node.with(\.trailingComma, nil)) + } else { + result.append(node.with(\.trailingComma, .commaToken(trailingTrivia: Trivia.spaces(1)))) + } + } + + return result + } +} + extension VariableDeclSyntax { func modifiersExclude(_ keywords: [Keyword]) -> Bool { return !self.modifiers.containsAny(of: keywords.map { TokenSyntax.keyword($0) })