Will Macros ever be able to inspect types outside the members they're being applied to?

Hi!
So I just had a few ideas for macros but they all fall in the same issue.

1: Storing a publisher in a cancelation set.

@Cancelable
self.somePublisher
   .sink {
       doStuff() 
    }

:point_down:

self.somePublisher
   .sink {
        doStuff() 
   }
   .store(in: self.cancelables)

the store(in: self.cancelables) is not the problem. Making sure cancellable exists, is.

I wanted to check if the class has a cancellables property, otherwise add it. But this is not possible as I only have inspection capabilities to the member somePublisher and not the container it's included on.

2: Similarly, I'd like to do something like:

@Mock
class ClassMock: ProtocolA, ProtocolB {}

:point_down:

class ClassMock: ProtocolA, ProtocolB {
   func fromProtocolA() { … }
   func fromProtocolB() { … }
}

or even:

typealias Protocols = ProtocolA & ProtocolB
@Mockable
class ClassMock: Protocols {}

:point_down:

class ClassMock: Protocols {
   func fromProtocolA() { … }
   func fromProtocolB() { … }
}

in these cases, I would need to know what the requirements for the protocols are. (I would be thankfull with @Mock(ProtocolA.self, ProtocolB.self) too. :D

So the question is, will this ever be possible? Sorry if I don't use proper terminology.
Thank you

3 Likes

Hey there,

Just came across your question and I think I have an answer that might be beneficial to you. However, I've previously detailed this answer in another thread that addresses the same issue.

Here is the link to that thread: Introducing Spyable: A Swift Macro for Automatic Spies Generation - #10 by Matejkob. I strongly recommend you go through the comment as it contains a thorough explanation which I believe will shed light on your problem. It's quite comprehensive and should give you a deeper understanding of the matter at hand.

I hope you find it useful. Feel free to comment if you have further questions or need additional clarification on anything.

Best of luck!

1 Like

It's a fundamental property of macros that the compiler only exposes certain information to them. While it's in theory possible for us to add APIs to the MacroExpansionContext to expose more information, this takes a lot of careful engineering, and the more information we expose, the more it impacts the efficiency of tools (particularly features like code completion). As of yet, there's no way to just query an arbitrary type—even an enclosing type—and see all of its contents.

This means you often have to either figure out a place where you can attach a macro so that it can see what it needs, or you need to make assumptions about the environment and let the compiler diagnose a normal error if those assumptions are wrong.

Specifically:

The macro system doesn't currently provide enough visibility into enclosing types to implement this. Doug Gregor has pitched something that would let you see enclosing types, but their exact members are removed, so even that wouldn't help.

What I would recommmend is that you make @Cancelable be an attached member macro you apply to the type. It will add both the cancellables property and an addCancelable(_:) method that you can use like this:

addCancelable(
    self.somePublisher
       .sink {
           doStuff() 
       }
)

An ordinary method should work at the use site because there's really nothing special or macro-y that needs to happen when you add a cancelable to the property.

You can write a macro that generates a mock for one protocol as long as it's attached to the protocol:

@Mockable protocol ProtocolA {
    func fromProtocolA()
}

But there's no way for a macro to see the declarations of more than one type at a time unless you do something disgusting like passing in a chunk of code as a string literal:

// Please don't do this
#Mockable("""
    protocol ProtocolA {
        func fromProtocolA()
    }
    protocol ProtocolB {
        func fromProtocolB()
    }
    """)
7 Likes

Thank you very much! That was really insightful and exactly they answer I was waiting for (not what I wanted though :D, but I completely understand the reasons!).

I had considered the alternatives but they all have shortcomings that my "dreamy" ones didn't.

Creating a mock for a protocol is very useful indeed but often we have more than one. :)
As for addCancelable… it is a nice idea. Not sure I prefer to use it over the regular approach though.

As for the string literal approach… don't you worry ahah!

Thank you very much for you answer.

2 Likes

For the cancellable stuff you could try doing something similar to what RxSwift did and create some sort of cancellable container like their DisposeBag and extend cancellable to have a cancelled(by: cancelContainer). Your macro could simple add the container as an attribute to the type owning the Publishers. It’s not perfect but will save a bunch of repetition with redeclaring the cancellable variables a the time.

1 Like