diff --git a/Package.swift b/Package.swift index 70d91f0a..1ae3a0f4 100644 --- a/Package.swift +++ b/Package.swift @@ -25,15 +25,15 @@ let package = Package( dependencies: [ .package( url: "https://github.com/fetch-rewards/swift-locking.git", - exact: "0.2.0" + exact: "0.2.1" ), .package( url: "https://github.com/swiftlang/swift-syntax.git", - exact: "600.0.0" // Must match SwiftSyntaxSugar's swift-syntax version + from: "602.0.0" // Must match SwiftSyntaxSugar's swift-syntax version ), .package( url: "https://github.com/fetch-rewards/SwiftSyntaxSugar.git", - exact: "0.1.1" // Must match swift-locking's SwiftSyntaxSugar version + from: "0.1.2" // Must match swift-locking's SwiftSyntaxSugar version ), ], targets: [ diff --git a/Sources/MockingClient/GenericMethodsWithInlineArrayTypes.swift b/Sources/MockingClient/GenericMethodsWithInlineArrayTypes.swift new file mode 100644 index 00000000..3cc56acf --- /dev/null +++ b/Sources/MockingClient/GenericMethodsWithInlineArrayTypes.swift @@ -0,0 +1,30 @@ +// +// GenericMethodsWithInlineArrayTypes.swift +// +// Copyright © 2025 Fetch. +// + +import Foundation +public import Mocking + +/// A protocol for verifying Mocked's handling of generic methods that leverage +/// inline array syntax and value-generic arguments. +/// +/// - Important: Please only use this protocol for permanent verification of +/// Mocked's handling of inline array syntax. For temporary testing of Mocked's +/// expansion, use the `Playground` protocol in `main.swift`. +@available(macOS 26.0, *) +@Mocked +public protocol GenericMethodsWithInlineArrayTypes { + func genericMethodWithInlineArrayParameter( + parameter: [3 of Element] + ) -> [3 of Element] + + func genericMethodReturningInlineArray( + parameter: InlineArray<3, Element> + ) -> InlineArray<3, Element> + + func genericMethodWithInlineArraySameTypeRequirement( + parameter: InlineArray<3, Element> + ) -> Element where Element: Sendable, InlineArray<3, Element> == InlineArray<3, Element> +} diff --git a/Sources/MockingMacros/Macros/MockedMethodMacro/MockedMethodMacro+TypeErasure.swift b/Sources/MockingMacros/Macros/MockedMethodMacro/MockedMethodMacro+TypeErasure.swift index 8f002abb..130ba050 100644 --- a/Sources/MockingMacros/Macros/MockedMethodMacro/MockedMethodMacro+TypeErasure.swift +++ b/Sources/MockingMacros/Macros/MockedMethodMacro/MockedMethodMacro+TypeErasure.swift @@ -247,6 +247,15 @@ extension MockedMethodMacro { ifTypeIsContainedIn: genericParameters, typeConstrainedBy: genericWhereClause ) + case var .inlineArrayType(type): + let (element, didTypeEraseElement) = self.syntax( + type.element, + ifTypeIsContainedIn: genericParameters, + typeConstrainedBy: genericWhereClause + ) + + type = type.with(\.element, element) + result = (newType: type, didTypeErase: didTypeEraseElement) case let .tupleType(type): result = self.syntax( type, @@ -456,9 +465,7 @@ extension MockedMethodMacro { ): let (newGenericArgumentClause, didTypeErase) = self.syntax( genericArgumentClause, - withElementsInCollectionAt: \.arguments, - typeErasedAt: \.argument, - ifTypeIsContainedIn: genericParameters, + typeErasedIfElementsIn: genericParameters, typeConstrainedBy: genericWhereClause ) let newType = type.with(\.genericArgumentClause, newGenericArgumentClause) @@ -470,15 +477,25 @@ extension MockedMethodMacro { ): let (newGenericArgumentClause, didTypeErase) = self.syntax( genericArgumentClause, - withElementsInCollectionAt: \.arguments, - typeErasedAt: \.argument, - ifTypeIsContainedIn: genericParameters, + typeErasedIfElementsIn: genericParameters, typeConstrainedBy: genericWhereClause ) { index in index == .zero ? AnyHashable.self : Any.self } let newType = type.with(\.genericArgumentClause, newGenericArgumentClause) + return (newType, didTypeErase) + case let .identifierType(type) where self.isIdentifierType( + type, + named: "InlineArray" + ): + let (newGenericArgumentClause, didTypeErase) = self.inlineArrayGenericArgumentClause( + genericArgumentClause, + typeErasingElementsIn: genericParameters, + typeConstrainedBy: genericWhereClause + ) + let newType = type.with(\.genericArgumentClause, newGenericArgumentClause) + return (newType, didTypeErase) case let .memberType(type) where self.isMemberType( type, @@ -487,9 +504,7 @@ extension MockedMethodMacro { ): let (newGenericArgumentClause, didTypeErase) = self.syntax( genericArgumentClause, - withElementsInCollectionAt: \.arguments, - typeErasedAt: \.argument, - ifTypeIsContainedIn: genericParameters, + typeErasedIfElementsIn: genericParameters, typeConstrainedBy: genericWhereClause ) let newType = type.with(\.genericArgumentClause, newGenericArgumentClause) @@ -502,22 +517,31 @@ extension MockedMethodMacro { ): let (newGenericArgumentClause, didTypeErase) = self.syntax( genericArgumentClause, - withElementsInCollectionAt: \.arguments, - typeErasedAt: \.argument, - ifTypeIsContainedIn: genericParameters, + typeErasedIfElementsIn: genericParameters, typeConstrainedBy: genericWhereClause ) { index in index == .zero ? AnyHashable.self : Any.self } let newType = type.with(\.genericArgumentClause, newGenericArgumentClause) + return (newType, didTypeErase) + case let .memberType(type) where self.isMemberType( + type, + named: "InlineArray", + withBaseTypeNamed: "Swift" + ): + let (newGenericArgumentClause, didTypeErase) = self.inlineArrayGenericArgumentClause( + genericArgumentClause, + typeErasingElementsIn: genericParameters, + typeConstrainedBy: genericWhereClause + ) + let newType = type.with(\.genericArgumentClause, newGenericArgumentClause) + return (newType, didTypeErase) default: let (_, didTypeErase) = self.syntax( genericArgumentClause, - withElementsInCollectionAt: \.arguments, - typeErasedAt: \.argument, - ifTypeIsContainedIn: genericParameters, + typeErasedIfElementsIn: genericParameters, typeConstrainedBy: genericWhereClause ) @@ -531,6 +555,108 @@ extension MockedMethodMacro { } } + private static func syntax( + _ argument: GenericArgumentSyntax, + ifTypeIsContainedIn genericParameters: GenericParameterListSyntax?, + typeConstrainedBy genericWhereClause: GenericWhereClauseSyntax?, + typeErasedType: (some Any).Type = Any.self + ) -> (GenericArgumentSyntax, Bool) { + guard case let .type(type) = argument.argument else { + return (argument, false) + } + + let (erasedType, didTypeErase) = self.type( + type, + typeErasedIfNecessaryUsing: genericParameters, + typeConstrainedBy: genericWhereClause, + typeErasedType: typeErasedType + ) + + let newArgument = argument.with( + \.argument, + .type(TypeSyntax(erasedType)) + ) + + return (newArgument, didTypeErase) + } + + private static func syntax( + _ clause: GenericArgumentClauseSyntax, + typeErasedIfElementsIn genericParameters: GenericParameterListSyntax?, + typeConstrainedBy genericWhereClause: GenericWhereClauseSyntax?, + typeErasedType: (Int) -> Any.Type + ) -> (GenericArgumentClauseSyntax, Bool) { + var didTypeErase = false + var newArguments: [GenericArgumentSyntax] = [] + + for (index, argument) in clause.arguments.enumerated() { + let (newArgument, didTypeEraseArgument) = self.syntax( + argument, + ifTypeIsContainedIn: genericParameters, + typeConstrainedBy: genericWhereClause, + typeErasedType: typeErasedType(index) + ) + + newArguments.append(newArgument) + didTypeErase = didTypeErase || didTypeEraseArgument + } + + let newClause = clause.with( + \.arguments, + GenericArgumentListSyntax(newArguments) + ) + + return (newClause, didTypeErase) + } + + private static func syntax( + _ clause: GenericArgumentClauseSyntax, + typeErasedIfElementsIn genericParameters: GenericParameterListSyntax?, + typeConstrainedBy genericWhereClause: GenericWhereClauseSyntax? + ) -> (GenericArgumentClauseSyntax, Bool) { + self.syntax( + clause, + typeErasedIfElementsIn: genericParameters, + typeConstrainedBy: genericWhereClause + ) { _ in Any.self } + } + + private static func inlineArrayGenericArgumentClause( + _ clause: GenericArgumentClauseSyntax, + typeErasingElementsIn genericParameters: GenericParameterListSyntax?, + typeConstrainedBy genericWhereClause: GenericWhereClauseSyntax? + ) -> (GenericArgumentClauseSyntax, Bool) { + guard !clause.arguments.isEmpty else { + return (clause, false) + } + + var didTypeErase = false + var newArguments: [GenericArgumentSyntax] = [] + + for (index, argument) in clause.arguments.enumerated() { + if index == .zero { + newArguments.append(argument) + continue + } + + let (newArgument, didTypeEraseArgument) = self.syntax( + argument, + ifTypeIsContainedIn: genericParameters, + typeConstrainedBy: genericWhereClause + ) + + newArguments.append(newArgument) + didTypeErase = didTypeErase || didTypeEraseArgument + } + + let newClause = clause.with( + \.arguments, + GenericArgumentListSyntax(newArguments) + ) + + return (newClause, didTypeErase) + } + /// Returns a Boolean value indicating whether the provided identifier /// type's name matches any of the provided `names`. /// diff --git a/Sources/MockingMacros/Models/MockMethodNameComponents/MockMethodNameComponents.swift b/Sources/MockingMacros/Models/MockMethodNameComponents/MockMethodNameComponents.swift index dcb4a59a..d633c93c 100644 --- a/Sources/MockingMacros/Models/MockMethodNameComponents/MockMethodNameComponents.swift +++ b/Sources/MockingMacros/Models/MockMethodNameComponents/MockMethodNameComponents.swift @@ -228,6 +228,8 @@ extension MockMethodNameComponents { self.capitalizedDescription(of: type) case let .attributedType(type): self.capitalizedDescription(of: type) + case let .inlineArrayType(type): + self.capitalizedDescription(of: type) case let .classRestrictionType(type): self.capitalizedDescription(of: type.classKeyword) case let .compositionType(type): @@ -296,6 +298,8 @@ extension MockMethodNameComponents { result += specifier.arguments.reduce("") { result, argument in result + self.capitalizedDescription(of: argument.parameter) } + case let .nonisolatedTypeSpecifier(specifier): + result += self.capitalizedDescription(of: specifier) } } let baseTypeDescription = self.capitalizedDescription(of: type.baseType) @@ -440,9 +444,19 @@ extension MockMethodNameComponents { return nameDescription + "Of" + genericArgumentsDescription } - return self.capitalizedDescription( - of: DictionaryTypeSyntax(key: key, value: value) - ) + if + let keyType = Self.type(from: key), + let valueType = Self.type(from: value) + { + return self.capitalizedDescription( + of: DictionaryTypeSyntax(key: keyType, value: valueType) + ) + } + + let keyDescription = Self.capitalizedDescription(of: key) + let valueDescription = Self.capitalizedDescription(of: value) + + return "DictionaryOf" + keyDescription + "To" + valueDescription } /// Returns a capitalized description of the provided `type`. @@ -503,9 +517,20 @@ extension MockMethodNameComponents { return baseTypeDescription + nameDescription + "Of" + genericArgumentsDescription } - let dictionaryDescription = self.capitalizedDescription( - of: DictionaryTypeSyntax(key: key, value: value) - ) + let dictionaryDescription: String + + if + let keyType = Self.type(from: key), + let valueType = Self.type(from: value) + { + dictionaryDescription = self.capitalizedDescription( + of: DictionaryTypeSyntax(key: keyType, value: valueType) + ) + } else { + let keyDescription = Self.capitalizedDescription(of: key) + let valueDescription = Self.capitalizedDescription(of: value) + dictionaryDescription = "DictionaryOf" + keyDescription + "To" + valueDescription + } return baseTypeDescription + dictionaryDescription } @@ -693,4 +718,121 @@ extension MockMethodNameComponents { return description } + + /// Returns a capitalized description of the provided `type`. + /// + /// - Parameter type: The inline array type syntax to describe. + /// + /// - Returns: A capitalized description of the provided `type`. + private static func capitalizedDescription( + of type: InlineArrayTypeSyntax + ) -> String { + let countDescription = self.capitalizedDescription(of: type.count.argument) + let elementDescription = self.capitalizedDescription(of: type.element.argument) + + return "InlineArrayOf" + countDescription + "Of" + elementDescription + } + + /// Returns a capitalized description of the provided `argument`. + /// + /// - Parameter argument: The generic argument syntax to describe. + /// + /// - Returns: A capitalized description of the provided `argument`. + private static func capitalizedDescription( + of argument: GenericArgumentSyntax.Argument + ) -> String { + switch argument { + case let .type(type): + self.capitalizedDescription(of: type) + case let .expr(expr): + self.capitalizedDescription(of: expr) + } + } + + /// Returns a capitalized description of the provided `expression`. + /// + /// - Parameter expression: The expression syntax to describe. + /// + /// - Returns: A capitalized description of the provided `expression`. + private static func capitalizedDescription( + of expression: ExprSyntax + ) -> String { + expression.trimmedDescription.withFirstCharacterCapitalized() + } + + /// Returns a capitalized description of the provided `specifier`. + /// + /// - Parameter specifier: The nonisolated type specifier syntax to describe. + /// + /// - Returns: A capitalized description of the provided `specifier`. + private static func capitalizedDescription( + of specifier: NonisolatedTypeSpecifierSyntax + ) -> String { + var description = self.capitalizedDescription(of: specifier.nonisolatedKeyword) + + if let argument = specifier.argument { + description += Self.capitalizedDescription(of: argument) + } + + return description + } + + /// Returns a capitalized description of the provided `argument`. + /// + /// - Parameter argument: The nonisolated specifier argument syntax to describe. + /// + /// - Returns: A capitalized description of the provided `argument`. + private static func capitalizedDescription( + of argument: NonisolatedSpecifierArgumentSyntax + ) -> String { + argument.trimmedDescription.withFirstCharacterCapitalized() + } + + /// Returns a capitalized description of the provided `type`. + /// + /// - Parameter type: The same-type requirement's left type to describe. + /// + /// - Returns: A capitalized description of the provided `type`. + private static func capitalizedDescription( + of type: SameTypeRequirementSyntax.LeftType + ) -> String { + switch type { + case let .type(type): + self.capitalizedDescription(of: type) + case let .expr(expr): + self.capitalizedDescription(of: expr) + } + } + + /// Returns a capitalized description of the provided `type`. + /// + /// - Parameter type: The same-type requirement's right type to describe. + /// + /// - Returns: A capitalized description of the provided `type`. + private static func capitalizedDescription( + of type: SameTypeRequirementSyntax.RightType + ) -> String { + switch type { + case let .type(type): + self.capitalizedDescription(of: type) + case let .expr(expr): + self.capitalizedDescription(of: expr) + } + } + + /// Returns a `TypeSyntax` when the provided `argument` is a type argument. + /// + /// - Parameter argument: The generic argument from which to extract a type. + /// + /// - Returns: A `TypeSyntax` if the argument represents a type; otherwise, `nil`. + private static func type( + from argument: GenericArgumentSyntax.Argument + ) -> TypeSyntax? { + switch argument { + case let .type(type): + type + case .expr: + nil + } + } }