diff --git a/Sources/MacroPlugin/Definitions.swift b/Sources/MacroPlugin/Definitions.swift index bd467924a..4a3ace938 100644 --- a/Sources/MacroPlugin/Definitions.swift +++ b/Sources/MacroPlugin/Definitions.swift @@ -297,6 +297,40 @@ struct Default: PeerMacro { } } +/// Attribute type for `GroupedDefault` macro-attribute. +/// +/// This type can validate`GroupedDefault` macro-attribute +/// usage and extract data for `Codable` macro to +/// generate implementation. +struct GroupedDefault: PeerMacro { + /// Provide metadata to `Codable` macro for final expansion + /// and verify proper usage of this macro. + /// + /// This macro doesn't perform any expansion rather `Codable` macro + /// uses when performing expansion. + /// + /// This macro verifies that macro usage condition is met by attached + /// declaration by using the `validate` implementation provided. + /// + /// - Parameters: + /// - node: The attribute describing this macro. + /// - declaration: The declaration this macro attribute is attached to. + /// - context: The context in which to perform the macro expansion. + /// + /// - Returns: No declaration is returned, only attached declaration is + /// analyzed. + static func expansion( + of node: AttributeSyntax, + providingPeersOf declaration: some DeclSyntaxProtocol, + in context: some MacroExpansionContext + ) throws -> [DeclSyntax] { + return try PluginCore.GroupedDefault.expansion( + of: node, providingPeersOf: declaration, in: context + ) + } +} + + /// Attribute type for `CodedAs` macro-attribute. /// /// This type can validate`CodedAs` macro-attribute diff --git a/Sources/MacroPlugin/Plugin.swift b/Sources/MacroPlugin/Plugin.swift index 1158db571..a6181f28f 100644 --- a/Sources/MacroPlugin/Plugin.swift +++ b/Sources/MacroPlugin/Plugin.swift @@ -24,6 +24,7 @@ struct MetaCodablePlugin: CompilerPlugin { MemberInit.self, CodingKeys.self, IgnoreCodingInitialized.self, + GroupedDefault.self, Inherits.self, ] } diff --git a/Sources/MetaCodable/GroupedDefault.swift b/Sources/MetaCodable/GroupedDefault.swift new file mode 100644 index 000000000..7fb324446 --- /dev/null +++ b/Sources/MetaCodable/GroupedDefault.swift @@ -0,0 +1,29 @@ +/// Provides a `GroupedDefault` value to be used when decoding fails and +/// when not initialized explicitly in memberwise initializer(s). +/// +/// If the value is missing or has incorrect data type, the default value +/// will be used instead of throwing error and terminating decoding. +/// i.e. for a field declared as: +/// ```swift +/// @GroupedDefault("some", 10) +/// let field: String, field2: Int +/// ``` +/// if empty json provided or type at `CodingKey` is different +/// ```json +/// { "field": 5 } // or {} +/// ``` +/// the default value provided in this case `some` will be used as +/// `field`'s value. +/// +/// - Parameter defaults: The defaults value to use. +/// +/// - Note: This macro on its own only validates if attached declaration +/// is a variable declaration. ``Codable()`` macro uses this macro +/// when generating final implementations. +/// +/// - Important: The field type must confirm to `Codable` and +/// default value type `T` must be the same as field type. +@attached(peer) +@available(swift 5.9) +public macro GroupedDefault(_ defaults: repeat each T) = + #externalMacro(module: "MacroPlugin", type: "GroupedDefault") diff --git a/Sources/PluginCore/Attributes/Default.swift b/Sources/PluginCore/Attributes/Default.swift index fcdabde7a..8b270f1e8 100644 --- a/Sources/PluginCore/Attributes/Default.swift +++ b/Sources/PluginCore/Attributes/Default.swift @@ -47,7 +47,7 @@ package struct Default: PropertyAttribute { /// - Returns: The built diagnoser instance. func diagnoser() -> DiagnosticProducer { return AggregatedDiagnosticProducer { - expect(syntaxes: VariableDeclSyntax.self) + attachedToUngroupedVariable() attachedToNonStaticVariable() cantDuplicate() cantBeCombined(with: IgnoreCoding.self) diff --git a/Sources/PluginCore/Attributes/GroupedDefault.swift b/Sources/PluginCore/Attributes/GroupedDefault.swift new file mode 100644 index 000000000..907edb7d2 --- /dev/null +++ b/Sources/PluginCore/Attributes/GroupedDefault.swift @@ -0,0 +1,118 @@ +@_implementationOnly import SwiftSyntax +@_implementationOnly import SwiftSyntaxMacros + +/// Attribute type for `GroupedDefault` macro-attribute. +/// +/// This type can validate`GroupedDefault` macro-attribute +/// usage and extract data for `Codable` macro to +/// generate implementation. +package struct GroupedDefault: PropertyAttribute { + /// The node syntax provided + /// during initialization. + let node: AttributeSyntax + + /// The default value expressions provided. + var exprs: [ExprSyntax] { + node.arguments!.as(LabeledExprListSyntax.self)!.map { + $0.expression + } + } + + /// Creates a new instance with the provided node. + /// + /// The initializer fails to create new instance if the name + /// of the provided node is different than this attribute. + /// + /// - Parameter node: The attribute syntax to create with. + /// - Returns: Newly created attribute instance. + init?(from node: AttributeSyntax) { + guard + node.attributeName.as(IdentifierTypeSyntax.self)! + .name.text == Self.name + else { return nil } + self.node = node + } + + /// Builds diagnoser that can validate this macro + /// attached declaration. + /// + /// The following conditions are checked by the + /// built diagnoser: + /// * Attached declaration is a variable declaration. + /// * Attached declaration is not a static variable + /// declaration + /// * Macro usage is not duplicated for the same + /// declaration. + /// * This attribute isn't used combined with + /// `IgnoreCoding` attribute. + /// + /// - Returns: The built diagnoser instance. + func diagnoser() -> DiagnosticProducer { + return AggregatedDiagnosticProducer { + attachedToGroupedVariable() + attachedToNonStaticVariable() + cantDuplicate() + cantBeCombined(with: IgnoreCoding.self) + } + } +} + +extension Registration +where +Decl == PropertyDeclSyntax, Var: PropertyVariable & InitializableVariable, Var.Initialization == AnyRequiredVariableInitialization, Var == AnyPropertyVariable +{ + /// Update registration with binding initializer value. + /// + /// New registration is updated with default expression data that will be + /// used for decoding failure and memberwise initializer(s), if provided. + /// + /// - Returns: Newly built registration with default expression data or self. + func addDefaultValueIfInitializerExists() -> Self { + guard Default(from: self.decl) == nil, GroupedDefault(from: self.decl) == nil, let value = decl.binding.initializer?.value, let variable = self.variable.base as? AnyPropertyVariable else { + return self + } + + let newVar = variable.with(default: value) + return self.updating(with: newVar.any) + } + + /// Update registration with pattern binding default values if exists. + /// + /// New registration is updated with default expression data that will be + /// used for decoding failure and memberwise initializer(s), if provided. + /// + /// - Returns: Newly built registration with default expression data or self. + func addGroupedDefaultIfExists() -> Self { + guard let defaults = GroupedDefault(from: self.decl) else { + return self + } + + var i: Int = 0 + for (index, binding) in self.decl.decl.bindings.enumerated() { + if binding.pattern.description == self.decl.binding.pattern.description { + i = index + break + } + } + + guard let variable = self.variable.base as? AnyPropertyVariable + else { return self } + + let newVar = variable.with(default: defaults.exprs[i]) + return self.updating(with: newVar.any) + } +} + +fileprivate extension PropertyVariable +where Initialization == RequiredInitialization { + /// Update variable data with the default value expression provided. + /// + /// `DefaultValueVariable` is created with this variable as base + /// and default expression provided. + /// + /// - Parameter expr: The default expression to add. + /// - Returns: Created variable data with default expression. + func with(default expr: ExprSyntax) -> DefaultValueVariable { + return .init(base: self, options: .init(expr: expr)) + } +} diff --git a/Sources/PluginCore/Diagnostics/GroupedVariableDeclaration.swift b/Sources/PluginCore/Diagnostics/GroupedVariableDeclaration.swift index 668508bdc..b2d79ca48 100644 --- a/Sources/PluginCore/Diagnostics/GroupedVariableDeclaration.swift +++ b/Sources/PluginCore/Diagnostics/GroupedVariableDeclaration.swift @@ -19,6 +19,8 @@ struct GroupedVariableDeclaration: DiagnosticProducer { /// in generated diagnostic /// messages. let attr: Attr + /// The attribute is attatch to multiple bindings variable. + let isAttach: Bool /// Underlying producer that validates passed syntax is variable /// declaration. /// @@ -36,9 +38,11 @@ struct GroupedVariableDeclaration: DiagnosticProducer { /// /// - Parameter attr: The attribute for which /// validation performed. + /// - Parameter isAttach: can attach to multiple bindings. /// - Returns: Newly created diagnostic producer. - init(_ attr: Attr) { + init(_ attr: Attr, isAttach: Bool) { self.attr = attr + self.isAttach = isAttach self.base = .init(attr, expect: [VariableDeclSyntax.self]) } @@ -60,11 +64,11 @@ struct GroupedVariableDeclaration: DiagnosticProducer { in context: some MacroExpansionContext ) -> Bool { guard !base.produce(for: syntax, in: context) else { return true } - guard syntax.as(VariableDeclSyntax.self)!.bindings.count > 1 + guard (!isAttach && syntax.as(VariableDeclSyntax.self)!.bindings.count > 1) || (isAttach && syntax.as(VariableDeclSyntax.self)!.bindings.count == 1) else { return false } let message = attr.diagnostic( message: - "@\(attr.name) can't be used with grouped variables declaration", + isAttach ? "@\(attr.name) can't be used with single variables declaration" : "@\(attr.name) can't be used with grouped variables declaration", id: attr.misuseMessageID, severity: .error ) @@ -82,6 +86,17 @@ extension PropertyAttribute { /// /// - Returns: Grouped variable declaration validation diagnostic producer. func attachedToUngroupedVariable() -> GroupedVariableDeclaration { - return .init(self) + return .init(self, isAttach: false) + } + + /// Indicates attribute must be attached to multiple bindings variable declaration. + /// + /// The created diagnostic producer produces error diagnostic, + /// if attribute is attached to grouped variable and non-variable + /// declarations. + /// + /// - Returns: Grouped variable declaration validation diagnostic producer. + func attachedToGroupedVariable() -> GroupedVariableDeclaration { + return .init(self, isAttach: true) } } diff --git a/Sources/PluginCore/Variables/Initialization/OptionalInitialization.swift b/Sources/PluginCore/Variables/Initialization/OptionalInitialization.swift deleted file mode 100644 index 3cbc574bc..000000000 --- a/Sources/PluginCore/Variables/Initialization/OptionalInitialization.swift +++ /dev/null @@ -1,24 +0,0 @@ -/// Represents initialization is optional for the variable. -/// -/// The variable must be mutable and initialized already. -struct OptionalInitialization: VariableInitialization -where Wrapped: RequiredVariableInitialization { - /// The value wrapped by this instance. - /// - /// Only function parameter and code block provided - /// with`RequiredVariableInitialization` - /// can be wrapped by this instance. - let base: Wrapped - - /// Adds current initialization type to memberwise initialization - /// generator. - /// - /// New memberwise initialization generator is created after adding this - /// initialization as optional and returned. - /// - /// - Parameter generator: The init-generator to add in. - /// - Returns: The modified generator containing this initialization. - func add(to generator: MemberwiseInitGenerator) -> MemberwiseInitGenerator { - generator.add(optional: .init(param: base.param, code: base.code)) - } -} diff --git a/Sources/PluginCore/Variables/Initialization/RequiredVariableInitialization.swift b/Sources/PluginCore/Variables/Initialization/RequiredVariableInitialization.swift index 486f32d1a..5cd35c657 100644 --- a/Sources/PluginCore/Variables/Initialization/RequiredVariableInitialization.swift +++ b/Sources/PluginCore/Variables/Initialization/RequiredVariableInitialization.swift @@ -20,12 +20,3 @@ package protocol RequiredVariableInitialization: VariableInitialization { /// generating initializer. var code: CodeBlockItemSyntax { get } } - -extension RequiredVariableInitialization { - /// Converts initialization to optional from required initialization. - /// - /// Wraps current instance in `OptionalInitialization`. - var optionalize: OptionalInitialization { - return .init(base: self) - } -} diff --git a/Sources/PluginCore/Variables/Property/InitializationVariable.swift b/Sources/PluginCore/Variables/Property/InitializationVariable.swift index afcd323ce..096dc80d1 100644 --- a/Sources/PluginCore/Variables/Property/InitializationVariable.swift +++ b/Sources/PluginCore/Variables/Property/InitializationVariable.swift @@ -110,11 +110,7 @@ where in context: some MacroExpansionContext ) -> AnyInitialization { return if options.`init` { - if options.initialized { - base.initializing(in: context).optionalize.any - } else { - base.initializing(in: context).any - } + base.initializing(in: context).any } else { IgnoredInitialization().any } diff --git a/Sources/PluginCore/Variables/Syntax/ExprTypeInference.swift b/Sources/PluginCore/Variables/Syntax/ExprTypeInference.swift new file mode 100644 index 000000000..5f474bf75 --- /dev/null +++ b/Sources/PluginCore/Variables/Syntax/ExprTypeInference.swift @@ -0,0 +1,361 @@ +import SwiftOperators +@_implementationOnly import SwiftSyntax + +// Potential future enhancements: +// - .ternaryExpr having "then" and "else" expressions as inferrable types +// - Consider: .isExpr, switchExpr, .tryExpr, .closureExpr + +extension ExprSyntax { + var inferredTypeSyntax: TypeSyntax? { + self.inferredType?.typeSyntax + } +} + +private indirect enum ExprInferrableType: Equatable, CustomStringConvertible { + case array(ExprInferrableType) + case arrayTypeInitializer(elementType: String) + case `as`(type: String) + case bool + case closedRange(ExprInferrableType) + case dictionary(key: ExprInferrableType, value: ExprInferrableType) + case dictionaryTypeInitializer(keyType: String, valueType: String) + case double + case int + case range(ExprInferrableType) + case string + case tuple([ExprInferrableType]) + case function(type: String) + + var description: String { + switch self { + case .array(let elementType): + return "[\(elementType.description)]" + + case .arrayTypeInitializer(let elementType): + return "[\(elementType)]" + + case .as(let type): + return type + + case .bool: + return "Bool" + + case .closedRange(let containedType): + return "ClosedRange<\(containedType.description)>" + + case .dictionary(let keyType, let valueType): + // NB: swift-format prefers `[Key: Value]`, but Xcode uses `[Key : Value]`. + return "[\(keyType.description): \(valueType.description)]" + + case .dictionaryTypeInitializer(let keyType, let valueType): + return "[\(keyType): \(valueType)]" + + case .double: + return "Double" + + case .int: + return "Int" + + case .range(let containedType): + return "Range<\(containedType.description)>" + + case .string: + return "String" + + case .tuple(let elementTypes): + let typeDescriptions = elementTypes.map(\.description).joined(separator: ", ") + return "(\(typeDescriptions))" + case .function(let type): + return type + } + } + + var unwrapSingleElementTuple: ExprInferrableType? { + guard + case let .tuple(elementTypes) = self, + elementTypes.count == 1 + else { return nil } + return elementTypes.first + } + + var typeSyntax: TypeSyntax { + TypeSyntax(stringLiteral: self.description) + } +} + +enum InfixOperator { + enum ArithmeticOperator: String { + case addition = "+" + case subtraction = "-" + case multiplication = "*" + case division = "/" + case modulo = "%" + } + + enum BitwiseOperator: String { + case bitwiseAnd = "&" + case bitwiseOr = "|" + case bitwiseXor = "^" + case bitwiseShiftLeft = "<<" + case bitwiseShiftRight = ">>" + } + + enum LogicalOperator: String { + case equality = "==" + case inequality = "!=" + case lessThan = "<" + case greaterThan = ">" + case lessThanOrEqual = "<=" + case greaterThanOrEqual = ">=" + case logicalAnd = "&&" + case logicalOr = "||" + } + + enum RangeOperator: String { + case closedRange = "..." + case halfOpenRange = "..<" + } + + case arithmetic(ArithmeticOperator) + case bitwise(BitwiseOperator) + case logical(LogicalOperator) + case range(RangeOperator) + + init?(rawValue: String) { + let type: Self? = + if let arithmeticOp = ArithmeticOperator(rawValue: rawValue) { + .arithmetic(arithmeticOp) + } else if let bitwiseOp = BitwiseOperator(rawValue: rawValue) { + .bitwise(bitwiseOp) + } else if let logicalOp = LogicalOperator(rawValue: rawValue) { + .logical(logicalOp) + } else if let rangeOp = RangeOperator(rawValue: rawValue) { + .range(rangeOp) + } else { + nil + } + guard let type else { return nil } + self = type + } +} + +extension ExprSyntax { + private var inferredType: ExprInferrableType? { + switch self.kind { + case .arrayExpr: + guard let arrayExpr = self.as(ArrayExprSyntax.self) else { return nil } + + let elementTypes = arrayExpr.elements.compactMap { $0.expression.inferredType } + guard + elementTypes.count == arrayExpr.elements.count, + let firstType = elementTypes.first, + let inferredArrayType = elementTypes.dropFirst().reduce(firstType, { commonType($0, $1) }) + else { return nil } + return .array(inferredArrayType) + + case .asExpr: + guard let asExpr = self.as(AsExprSyntax.self) else { return nil } + return .as(type: asExpr.type.trimmedDescription) + + case .booleanLiteralExpr: + return .bool + + case .dictionaryExpr: + guard let dictionaryExpr = self.as(DictionaryExprSyntax.self) else { return nil } + + let keyValuePairs = + dictionaryExpr.content + .as(DictionaryElementListSyntax.self)? + .compactMap { ($0.key.inferredType, $0.value.inferredType) } + ?? [] + + guard !keyValuePairs.isEmpty else { return nil } + + let initialKeyTypes = keyValuePairs.map(\.0) + let initialValueTypes = keyValuePairs.map(\.1) + + guard + let firstKeyType = initialKeyTypes.first, + let firstValueType = initialValueTypes.first, + let inferredKeyType = initialKeyTypes.dropFirst().reduce( + firstKeyType, { commonType($0, $1) }), + let inferredValueType = initialValueTypes.dropFirst().reduce( + firstValueType, { commonType($0, $1) }) + else { return nil } + + return .dictionary(key: inferredKeyType, value: inferredValueType) + + case .floatLiteralExpr: + return .double + + case .functionCallExpr: + guard let functionCallExpr = self.as(FunctionCallExprSyntax.self) else { return nil } + + // NB: `[Type]()` + if let arrayExpr = functionCallExpr.calledExpression.as(ArrayExprSyntax.self) { + let typeString = arrayExpr.elements + .first? + .expression + .as(DeclReferenceExprSyntax.self)? + .baseName + .trimmedDescription + guard let typeString else { return nil } + return .arrayTypeInitializer(elementType: typeString) + } + + // NB: `[KeyType : ValueType]()` + if let dictionaryExpr = functionCallExpr.calledExpression.as(DictionaryExprSyntax.self) { + guard let type = dictionaryExpr.content.as(DictionaryElementListSyntax.self)?.first + else { return nil } + + return .dictionaryTypeInitializer( + keyType: type.key.trimmedDescription, + valueType: type.value.trimmedDescription + ) + } + + return .function(type: functionCallExpr.calledExpression.description) + + 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 operation = InfixOperator(rawValue: infixOperatorExpr.operator.trimmedDescription), + let inferredType = resultTypeOfInfixOperation( + lhs: lhsType, + rhs: rhsType, + operation: operation + ) + else { return nil } + return inferredType + + case .integerLiteralExpr: + return .int + + case .prefixOperatorExpr: + guard + let prefixOperatorExpr = self.as(PrefixOperatorExprSyntax.self) + else { return nil } + return prefixOperatorExpr.expression.inferredType + + case .sequenceExpr: + // NB: SwiftSyntax 509.0.2 represents `1 + 2 + 3` as a tree of InfixOperatorExprSyntax + // values, but Swift 5.9.0 represents it as SequenceExprSyntax. + guard + let sequenceExpr = self.as(SequenceExprSyntax.self), + let foldedExpr = try? OperatorTable.standardOperators.foldSingle(sequenceExpr) + else { return nil } + return foldedExpr.inferredType + + case .stringLiteralExpr, .simpleStringLiteralExpr, .simpleStringLiteralSegmentList, + .stringLiteralSegmentList: + return .string + + case .tupleExpr: + guard let tupleExpr = self.as(TupleExprSyntax.self) else { return nil } + let elementTypes = tupleExpr.elements.compactMap { $0.expression.inferredType } + guard elementTypes.count == tupleExpr.elements.count + else { return nil } + return .tuple(elementTypes) + + case .memberAccessExpr: + return nil + default: return nil + } + } +} + +private func commonType( + _ first: ExprInferrableType?, + _ second: ExprInferrableType? +) -> ExprInferrableType? { + guard let firstType = first, let secondType = second else { return nil } + + switch (firstType, secondType) { + case (.as(let firstElementType), .as(let secondElementType)): + return firstElementType == secondElementType ? firstType : nil + + case (.int, .double), (.double, .int): + return .double + + case (.int, .int): + return .int + + case (.double, .double): + return .double + + case (.string, .string): + return .string + + case (.bool, .bool): + return .bool + + case (.array(let firstElementType), .array(let secondElementType)): + if let commonElementType = commonType(firstElementType, secondElementType) { + return .array(commonElementType) + } + + case ( + .dictionary(let firstKeyType, let firstValueType), + .dictionary(let secondKeyType, let secondValueType) + ): + if let commonKeyType = commonType(firstKeyType, secondKeyType), + let commonValueType = commonType(firstValueType, secondValueType) + { + return .dictionary(key: commonKeyType, value: commonValueType) + } + + case (.closedRange(let firstContainedType), .closedRange(let secondContainedType)): + if let commonContainedType = commonType(firstContainedType, secondContainedType) { + return .closedRange(commonContainedType) + } + + case (.range(let firstContainedType), .range(let secondContainedType)): + if let commonContainedType = commonType(firstContainedType, secondContainedType) { + return .range(commonContainedType) + } + + default: + return nil + } + + return nil +} + +private func resultTypeOfInfixOperation( + lhs: ExprInferrableType, + rhs: ExprInferrableType, + operation: InfixOperator +) -> ExprInferrableType? { + let lhsType = lhs.unwrapSingleElementTuple ?? lhs + let rhsType = rhs.unwrapSingleElementTuple ?? rhs + + switch operation { + case .logical(_): + return .bool + + case .arithmetic(let op): + switch op { + case .addition, .subtraction, .multiplication, .division: + return commonType(lhsType, rhsType) + + case .modulo: + return (lhsType, rhsType) == (.int, .int) ? .int : nil + } + + case .range(let op): + guard let type = commonType(lhsType, rhsType) else { return nil } + return switch op { + case .closedRange: + ExprInferrableType.closedRange(type) + + case .halfOpenRange: + .range(type) + } + + case .bitwise(_): + guard (lhsType, rhsType) == (.int, .int) else { return nil } + return .int + } +} diff --git a/Sources/PluginCore/Variables/Syntax/PropertyDeclSyntax.swift b/Sources/PluginCore/Variables/Syntax/PropertyDeclSyntax.swift index 1cc5ed6d3..742f396ef 100644 --- a/Sources/PluginCore/Variables/Syntax/PropertyDeclSyntax.swift +++ b/Sources/PluginCore/Variables/Syntax/PropertyDeclSyntax.swift @@ -26,7 +26,7 @@ struct PropertyDeclSyntax: VariableSyntax, AttributableDeclSyntax { /// /// If type is not present in syntax, `typeIfMissing` is used. var type: TypeSyntax { - return binding.typeAnnotation?.type.trimmed ?? typeIfMissing + return (binding.typeAnnotation?.type.trimmed ?? binding.initializer?.value.inferredTypeSyntax) ?? typeIfMissing } /// The attributes attached to property. diff --git a/Sources/PluginCore/Variables/Type/MemberGroup.swift b/Sources/PluginCore/Variables/Type/MemberGroup.swift index aaa6a29b2..da7214f33 100644 --- a/Sources/PluginCore/Variables/Type/MemberGroup.swift +++ b/Sources/PluginCore/Variables/Type/MemberGroup.swift @@ -192,6 +192,8 @@ where Decl.ChildSyntaxInput == Void, Decl.MemberSyntax == PropertyDeclSyntax { .useHelperCoderIfExists() .checkForAlternateKeyValues(addTo: codingKeys, context: context) .addDefaultValueIfExists() + .addDefaultValueIfInitializerExists() + .addGroupedDefaultIfExists() .checkCanBeInitialized() .checkCodingIgnored() } diff --git a/Tests/MetaCodableTests/Attributes/GroupedDefaultTests.swift b/Tests/MetaCodableTests/Attributes/GroupedDefaultTests.swift new file mode 100644 index 000000000..f83df89c5 --- /dev/null +++ b/Tests/MetaCodableTests/Attributes/GroupedDefaultTests.swift @@ -0,0 +1,160 @@ +#if SWIFT_SYNTAX_EXTENSION_MACRO_FIXED +import SwiftDiagnostics +import XCTest + +@testable import PluginCore + +final class GroupedDefaultTests: XCTestCase { + + func testMisuseOnNonVariableDeclaration() throws { + assertMacroExpansion( + """ + struct SomeCodable { + @GroupedDefault("some") + func someFunc() { + } + } + """, + expandedSource: + """ + struct SomeCodable { + func someFunc() { + } + } + """, + diagnostics: [ + .init( + id: GroupedDefault.misuseID, + message: + "@GroupedDefault only applicable to variable declarations", + line: 2, column: 5, + fixIts: [ + .init(message: "Remove @GroupedDefault attribute") + ] + ) + ] + ) + } + + func testMisuseOnStaticVariable() throws { + assertMacroExpansion( + """ + struct SomeCodable { + @GroupedDefault("some") + static let value: String + } + """, + expandedSource: + """ + struct SomeCodable { + static let value: String + } + """, + diagnostics: [ + .init( + id: GroupedDefault.misuseID, + message: + "@GroupedDefault can't be used with single variables declaration", + line: 2, column: 5, + fixIts: [ + .init(message: "Remove @GroupedDefault attribute") + ] + ), + .init( + id: GroupedDefault.misuseID, + message: + "@GroupedDefault can't be used with static variables declarations", + line: 2, column: 5, + fixIts: [ + .init(message: "Remove @GroupedDefault attribute") + ] + ), + ] + ) + } + + func testNoPatternBindingMisuse() throws { + assertMacroExpansion( + """ + struct SomeCodable { + @GroupedDefault("other") + let one: String + } + """, + expandedSource: + """ + struct SomeCodable { + let one: String + } + """, + diagnostics: [ + .init( + id: GroupedDefault.misuseID, + message: + "@GroupedDefault can't be used with single variables declaration", + line: 2, column: 5, + fixIts: [ + .init(message: "Remove @GroupedDefault attribute") + ] + ) + ] + ) + } + + func testDuplicatedMisuse() throws { + assertMacroExpansion( + """ + struct SomeCodable { + @GroupedDefault("some") + @GroupedDefault("other") + let one: String + } + """, + expandedSource: + """ + struct SomeCodable { + let one: String + } + """, + diagnostics: [ + .init( + id: GroupedDefault.misuseID, + message: + "@GroupedDefault can't be used with single variables declaration", + line: 2, column: 5, + fixIts: [ + .init(message: "Remove @GroupedDefault attribute") + ] + ), + .init( + id: GroupedDefault.misuseID, + message: + "@GroupedDefault can only be applied once per declaration", + line: 2, column: 5, + fixIts: [ + .init(message: "Remove @GroupedDefault attribute") + ] + ), + .init( + id: GroupedDefault.misuseID, + message: + "@GroupedDefault can't be used with single variables declaration", + line: 3, column: 5, + fixIts: [ + .init(message: "Remove @GroupedDefault attribute") + ] + ), + .init( + id: GroupedDefault.misuseID, + message: + "@GroupedDefault can only be applied once per declaration", + line: 3, column: 5, + fixIts: [ + .init(message: "Remove @GroupedDefault attribute") + ] + ), + ] + ) + } +} +#endif diff --git a/Tests/MetaCodableTests/CodableTests.swift b/Tests/MetaCodableTests/CodableTests.swift index 1652b0a8f..1f19d75c3 100644 --- a/Tests/MetaCodableTests/CodableTests.swift +++ b/Tests/MetaCodableTests/CodableTests.swift @@ -326,6 +326,7 @@ let allMacros: [String: Macro.Type] = [ "MemberInit": MacroPlugin.MemberInit.self, "CodingKeys": MacroPlugin.CodingKeys.self, "IgnoreCodingInitialized": MacroPlugin.IgnoreCodingInitialized.self, + "GroupedDefault": MacroPlugin.GroupedDefault.self "Inherits": MacroPlugin.Inherits.self ] #else @@ -343,6 +344,7 @@ let allMacros: [String: Macro.Type] = [ "MemberInit": MemberInit.self, "CodingKeys": CodingKeys.self, "IgnoreCodingInitialized": IgnoreCodingInitialized.self, + "GroupedDefault": GroupedDefault.self, "Inherits": Inherits.self ] #endif diff --git a/Tests/MetaCodableTests/TypeAnnotationMisssing/TypeAnnotationMisssingTests.swift b/Tests/MetaCodableTests/TypeAnnotationMisssing/TypeAnnotationMisssingTests.swift new file mode 100644 index 000000000..a17d332a0 --- /dev/null +++ b/Tests/MetaCodableTests/TypeAnnotationMisssing/TypeAnnotationMisssingTests.swift @@ -0,0 +1,96 @@ +#if SWIFT_SYNTAX_EXTENSION_MACRO_FIXED +import SwiftDiagnostics +import XCTest + +@testable import PluginCore + +final class TypeAnnotationMisssingTests: XCTestCase { + + func testBuiltinTypeInferred() throws { + assertMacroExpansion( + """ + @MemberInit + struct SomeCodable { + var int = 0 + var float = 0.1 as Float + var double = Double(0.11) + var string = "hello" + } + """, + expandedSource: + """ + struct SomeCodable { + var int = 0 + var float = 0.1 as Float + var double = Double(0.11) + var string = "hello" + + init(int: Int = 0, float: Float = 0.1 as Float, double: Double = Double(0.11), string: String = "hello") { + self.int = int + self.float = float + self.double = double + self.string = string + } + } + """ + ) + } + + func testBuiltinTypeInferredWithDefaultAttribute() throws { + assertMacroExpansion( + """ + @MemberInit + struct SomeCodable { + @Default(10) + var int = 0 + var float = 0.1 as Float + var double = Double(0.11) + let string: String + } + """, + expandedSource: + """ + struct SomeCodable { + var int = 0 + var float = 0.1 as Float + var double = Double(0.11) + let string: String + + init(int: Int = 10, float: Float = 0.1 as Float, double: Double = Double(0.11), string: String) { + self.int = int + self.float = float + self.double = double + self.string = string + } + } + """ + ) + } + + func testBuiltinTypeInferredWithMultipleBinding() throws { + assertMacroExpansion( + """ + @MemberInit + struct SomeCodable { + @Default(10) + var int = 0 + var float = Float(0.1), string = "" + } + """, + expandedSource: + """ + struct SomeCodable { + var int = 0 + var float = Float(0.1), string = "" + + init(int: Int = 10, float: Float = Float(0.1), string: String = "") { + self.int = int + self.float = float + self.string = string + } + } + """ + ) + } +} +#endif