Is it possible to check in a Marco expansion if all stored properties the type conform to a protocol?

If I have a these code constructs defined

protocol OnlyContainsVariablesConformingToX {}
protocol X {}
extensoon Int: X {}

And wish to define a Macro ConformanceApplierForOnlyContainsVariablesConformingToX that adds conformance to OnlyContainsVariablesConformingToX protocol and behaves in the manner below

///This snippet should be allowed since `a` is `Int` and conforms to `X`
@ConformanceApplierForOnlyContainsVariablesConformingToX
struct CustomA {
    let a: Int
}

///This snippet should `NOT` be allowed and we show a diagnosic indicating that `a` which is a `String` does not conform to the protocol `X`.
@ConformanceApplierForOnlyContainsVariablesConformingToX
struct CustomB {
    let a: String
}

Not sure if Macros should be allowed access to X, because X is not part of the code to which the Macro is applied.

That’s not possible. See my answer here.

In short: SwiftSyntax is only about syntax and therefore, other than a complete compiler, knows nothing about types, conformances and inheritance. In your example, the conformance of Int to X could be defined anywhere - in another file or even in an imported module.

3 Likes

I was curious if one way to approach this problem could be to have the checks be made in the Macro defintion layer rather than in the SwiftSyntax layer

and use the Macro defintion as the compiler check

if I define a MemberMacro ConformsToX<SomeType: X>

So intial code

@ConformanceApplierForOnlyContainsVariablesConformingToX
struct CustomA {
    let a: Int
}

@ConformanceApplierForOnlyContainsVariablesConformingToX
struct CustomB {
    let a: String
}

Expands first into

struct CustomA: OnlyContainsVariablesConformingToX {
    @ConformsToX<Int>
    let a: Int
}

struct CustomB: OnlyContainsVariablesConformingToX {
    @ConformsToX<String>
    let a: String
}

This then expands into

struct CustomA: OnlyContainsVariablesConformingToX {
    let a: Int
}

struct CustomB: OnlyContainsVariablesConformingToX {
    @ConformsToX<String> ///Compiler error because this should not be possible because String does not conform to X
    let a: String
}

But this assumes that it's possible to have one Macro apply another Macro recursively which I am not sure is possible.

Closing the loop

Adding an AttributeSyntax for @ConformsToX<VariableType> did not end up expanding the inner Macro. I tried to experiment to see if MacroDecSyntax could be added as an attribute to a member, but could not find any examples that I could use for guidance

Work around that seems to work

Step 1: Defined a new type

public struct XWrapper<SomeType: X> {
    let wrapped: SomeType
}

Step 2: Generated a peer type for CustomA and CustomB structs via the Macro

///Offers compiler checks which validate that each stored member in `CustomA` conforms to `X` 
private struct CustomA_XConformanceCheck {
    private let a: XWrapper<Int>
}

///Offers compiler checks which validate that each stored member in `CustomB` conforms to `X` 
private struct CustomB_XConformanceCheck {
    private let a: XWrapper<String> ///Compiler error here
}

Mentioning it here in case this strategy is helpful for anyone else trying to solve a similar problem. My main concern was to offer immediate diagnostics to the developer when using the Macro, and this workaround offers a good solution.

Another similar idea was to use property wrappers, but the compiler diagnostics for that MacroExpansion were not as clear. Would be great if I could query the compiler if code generated by Macro is valid so I could offer custom diagnostics

NOTE: This probably also slows down compile times a bit because there are unnecessary unused types

1 Like