Skip to content

Commit

Permalink
Enable opting out of Instantiable protocol conformance (#125)
Browse files Browse the repository at this point in the history
Enables `@Instantiable`-decorated types and extensions to avoid being forced to conform to `Instantiable` by providing a flag to the macro. By enabling macro-decorated declarations to avoid conformance, we enable multiple locations to create `@Instantiable` extensions.
  • Loading branch information
dfed authored Dec 27, 2024
1 parent 4805a13 commit ebb3310
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 6 deletions.
7 changes: 5 additions & 2 deletions Sources/SafeDI/PropertyDecoration/Instantiable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,13 @@
/// }
/// }
///
/// - Parameter additionalTypes: The types (in addition to the type decorated with this macro) of properties that can be decorated with `@Instantiated` and yield a result of this type. The types provided *must* be either superclasses of this type or protocols to which this type conforms.
/// - Parameters:
/// - additionalTypes: The types (in addition to the type decorated with this macro) of properties that can be decorated with `@Instantiated` and yield a result of this type. The types provided *must* be either superclasses of this type or protocols to which this type conforms.
/// - conformsElsewhere: Whether the decorated type already conforms to the `Instantiable` protocol elsewhere. If set to `true`, the macro does not enforce that this declaration conforms to `Instantiable`.
@attached(member, names: arbitrary)
public macro Instantiable(
fulfillingAdditionalTypes additionalTypes: [Any.Type] = []
fulfillingAdditionalTypes additionalTypes: [Any.Type] = [],
conformsElsewhere: Bool = false
) = #externalMacro(module: "SafeDIMacros", type: "InstantiableMacro")

/// A type that can be instantiated with runtime-injected properties.
Expand Down
14 changes: 14 additions & 0 deletions Sources/SafeDICore/Extensions/AttributeSyntaxExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,20 @@ extension AttributeSyntax {
return firstLabeledExpression.expression
}

public var conformsElsewhere: ExprSyntax? {
guard let arguments,
let labeledExpressionList = LabeledExprListSyntax(arguments),
let firstLabeledExpression = labeledExpressionList.first(where: {
// In `@Instantiated`, the `conformsElsewhere` parameter is the second parameter, though the first parameter has a default.
$0.label?.text == "conformsElsewhere"
})
else {
return nil
}

return firstLabeledExpression.expression
}

public var fulfilledByDependencyNamed: ExprSyntax? {
guard let arguments,
let labeledExpressionList = LabeledExprListSyntax(arguments),
Expand Down
24 changes: 20 additions & 4 deletions Sources/SafeDIMacros/Macros/InstantiableMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,16 @@ public struct InstantiableMacro: MemberMacro {
?? ClassDeclSyntax(declaration)
?? StructDeclSyntax(declaration)
{
let extendsInstantiable = concreteDeclaration.inheritanceClause?.inheritedTypes.contains(where: \.type.typeDescription.isInstantiable) ?? false
if !extendsInstantiable {
lazy var extendsInstantiable = concreteDeclaration.inheritanceClause?.inheritedTypes.contains(where: \.type.typeDescription.isInstantiable) ?? false
let mustExtendInstantiable = if let conformsElsewhereArgument = declaration.attributes.instantiableMacro?.conformsElsewhere,
let boolExpression = BooleanLiteralExprSyntax(conformsElsewhereArgument)
{
boolExpression.literal.tokenKind == .keyword(.false)
} else {
true
}

if mustExtendInstantiable, !extendsInstantiable {
var modifiedDeclaration = concreteDeclaration
var inheritedType = InheritedTypeSyntax(
type: IdentifierTypeSyntax(name: .identifier("Instantiable"))
Expand Down Expand Up @@ -161,8 +169,16 @@ public struct InstantiableMacro: MemberMacro {
return generateForwardedProperties(from: forwardedProperties)

} else if let extensionDeclaration = ExtensionDeclSyntax(declaration) {
let extendsInstantiable = extensionDeclaration.inheritanceClause?.inheritedTypes.contains(where: \.type.typeDescription.isInstantiable) ?? false
if !extendsInstantiable {
lazy var extendsInstantiable = extensionDeclaration.inheritanceClause?.inheritedTypes.contains(where: \.type.typeDescription.isInstantiable) ?? false
let mustExtendInstantiable = if let conformsElsewhereArgument = declaration.attributes.instantiableMacro?.conformsElsewhere,
let boolExpression = BooleanLiteralExprSyntax(conformsElsewhereArgument)
{
boolExpression.literal.tokenKind == .keyword(.false)
} else {
true
}

if mustExtendInstantiable, !extendsInstantiable {
var modifiedDeclaration = extensionDeclaration
var inheritedType = InheritedTypeSyntax(
type: IdentifierTypeSyntax(name: .identifier("Instantiable"))
Expand Down
120 changes: 120 additions & 0 deletions Tests/SafeDIMacrosTests/InstantiableMacroTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,46 @@ import SafeDICore
}
}

// MARK: Behavior Tests

func test_extension_expandsWithoutIssueOnTypeDeclarationWhenInstantiableConformanceMissingAndConformsElsewhereIsTrue() {
assertMacro {
"""
@Instantiable(conformsElsewhere: true)
public final class ExampleService {
public init() {}
}
"""
} expansion: {
"""
public final class ExampleService {
public init() {}
}
"""
}
}

func test_extension_expandsWithoutIssueOnExtensionWhenInstantiableConformanceMissingAndConformsElsewhereIsTrue() {
assertMacro {
"""
@Instantiable(conformsElsewhere: true)
extension ExampleService: CustomStringConvertible {
public static func instantiate() -> ExampleService { fatalError() }
public var description: String { "ExampleService" }
}
"""
} expansion: {
"""
extension ExampleService: CustomStringConvertible {
public static func instantiate() -> ExampleService { fatalError() }
public var description: String { "ExampleService" }
}
"""
}
}

// MARK: Error tests

func test_declaration_throwsErrorWhenOnProtocol() {
Expand Down Expand Up @@ -1401,6 +1441,44 @@ import SafeDICore
}
}

func test_declaration_fixit_addsFixitWhenInstantiableConformanceMissingAndConformsElsewhereIsFalse() {
assertMacro {
"""
@Instantiable(conformsElsewhere: false)
public final class ExampleService: CustomStringConvertible {
public init() {}
public var description: String { "ExampleService" }
}
"""
} diagnostics: {
"""
@Instantiable(conformsElsewhere: false)
┬──────────────────────────────────────
╰─ 🛑 @Instantiable-decorated type or extension must declare conformance to `Instantiable`
✏️ Declare conformance to `Instantiable`
public final class ExampleService: CustomStringConvertible {
public init() {}
public var description: String { "ExampleService" }
}
"""
} fixes: {
"""
@Instantiable(conformsElsewhere: false)
public final class ExampleService: CustomStringConvertible, Instantiable {
public init() {}
public var description: String { "ExampleService" }
}
"""
} expansion: {
"""
public final class ExampleService: CustomStringConvertible, Instantiable {
public init() {}
public var description: String { "ExampleService" }
}
"""
}
}

func test_declaration_doesNotAddFixitWhenRetroactiveInstantiableConformanceExists() {
assertMacro {
"""
Expand Down Expand Up @@ -2128,6 +2206,48 @@ import SafeDICore
}
}

func test_extension_fixit_addsFixitWhenInstantiableConformanceMissingAndConformsElsewhereIsFalse() {
assertMacro {
"""
@Instantiable(conformsElsewhere: false)
extension ExampleService: CustomStringConvertible {
public static func instantiate() -> ExampleService { fatalError() }
public var description: String { "ExampleService" }
}
"""
} diagnostics: {
"""
@Instantiable(conformsElsewhere: false)
┬──────────────────────────────────────
╰─ 🛑 @Instantiable-decorated type or extension must declare conformance to `Instantiable`
✏️ Declare conformance to `Instantiable`
extension ExampleService: CustomStringConvertible {
public static func instantiate() -> ExampleService { fatalError() }
public var description: String { "ExampleService" }
}
"""
} fixes: {
"""
@Instantiable(conformsElsewhere: false)
extension ExampleService: CustomStringConvertible, Instantiable {
public static func instantiate() -> ExampleService { fatalError() }
public var description: String { "ExampleService" }
}
"""
} expansion: {
"""
extension ExampleService: CustomStringConvertible, Instantiable {
public static func instantiate() -> ExampleService { fatalError() }
public var description: String { "ExampleService" }
}
"""
}
}

func test_extension_fixit_addsFixitWhenInstantiateMethodMissing() {
assertMacro {
"""
Expand Down

0 comments on commit ebb3310

Please sign in to comment.