Thanks a lot for your contribution to this thread @gwendal.roue!
I'm aware of this feature in the language. Probably the example in my question was a bit misleading. Default protocol implementations is a great thing but I would love to create an new declaration, add to it much more than default implementation of interface (maybe some properties, dependencies etc. etc.).
Maybe something like this:
@AutoImplemented
protocol Foo { ... }
// Code generated by macro AutoImplemented
struct AutoImplementedFoo: Foo {
var autoGeneratedPropertyCorrespondingToValueProperty: {value property type}
var value: String { ... }
var autoGeneratedPropertyCorrespondingToFooMethod: {foo metod type }
func foo() { ... }
}
I believe that an @attached(peer, names: arbitrary) macro ought to be able to do this, but keep in mind that the implementation will be pretty complex. You’ll need to create a new StructDeclSyntax, give it its name, and add the conformance to it; then you’ll need to walk over the requirements of the protocol and transform them into versions with implementations that can be inserted into the StructDeclSyntax.
There are going to be lots of edge cases; for instance, if the requirement is a computed property, you’ll have to give each accessor an implementation. The sheer number of complications might mean this isn’t the best choice for your first macro. But with enough effort, I think it would be possible to do this.
I wasn't sure if attached macros can add any code outside of the scope of the declaration. In the documentation I've find this sentence:
Attached macros modify the declaration that they’re attached to. They add code to that declaration, like defining a new method or adding conformance to a protocol.
and I've thought that this might imply that code can only be added only inside. As we know it's not possible to nested types like structs inside protocols so before diving deeper into the subject I've wanted to consult it. So I'm really glad to hear that it is possible.
Thank you very much for the advice and I really appreciate the kind words!
For my case at least (and perhaps OP's as well from the examples presented?), I was looking to create something like a Mocking library, that you can use for testing instead of having to create mocks manually and/or rely on a code generation tool for this.
Something you can use like:
func testThisWorksFine() {
@Mocked let repository: MyRepositoryProtocol
let sut = MyClass(repository: repository)
}
Either like that, or creating a
@Mocked(MyRepositoryProtocol)
class MockedRepository {}
or something that would simplify code around mocking. I realise that going through the effort of supporting every edge case is the path to insanity and to flakiness for future swift releases, so I see that as a no-go for now haha.
Maybe something simpler but still handy could be the way to go, like creating your class (maybe with a conformance macro still for protocol conformance) and a simpler macro to keep track of method call count and things like that, like:
@Mock
class MyMock: CoolProtocol {
// macro could generate this prop
var callStack: MyMockCallStack
@Mock
func coolAction() {
// and the macro would generate:
callStack.coolAction.record()
}
}
Hi! I've been playing around this during the last days and I've achieved promising results, the idea is to add using a peer macro and add it to a protocol definition
@Spy
protocol MyProtocol {
func myFunction()
}
and then generate a spy class confirming to this protocol
class MyProtocolSpy: MyProtocol {
var didCallMyFunction = false
func myFunction() {
didCallMyFunction = true
}
}
Using this site was very helpful to just understand how to build the macro expansion using SwiftSyntax
Hi! I am trying exactly this too! My idea is to make a @Mock class FooBar {}
or similar to auto implement protocol methods that call a fatalError stub. Problem is that I can’t get the tests to call the Conformance macro expansion therefor I can’t debug and iterate in this .
In main.swift file it generates something but for tests it doesn’t.
I managed to make other macros successfully but this one…
What @miibpa describes looks like the simplest option to achieve what we want, however, I wouldn't want to generate, compile, or ship this code on my app, and I would prefer to keep gencode on my test targets.
I'm trying to do something like:
@Mock
class SimpleProtocolMock: SimpleProtocol {}
But I haven't been able to retrieve the DeclSyntax for the SimpleProtocol inheritedTypeCollection. Is this going to be possible?
I looked and even started a new Macro package. In tests, it simply doesn't call the expansion method.
Here's a barebones project:
Essentially:
I copied the EquatableMacro from the swift project and replaced the stringify placeholder with my EquatableMacro.
If you take a couple of minutes to download and build the package I attached above, you can see that it builds and expands as expected in the main.swift file (in the client).
But for tests, it simply won't call the expand method, and it returns the empty class only (if I change the name of the macro to something that doesn't exist, it keeps it in the output so it's definitely doing something but not calling the expand method.
What am I doing wrong or missing?
Also, this only builds for macOS, is that correct?
Thank you very much!
I think this will make much more sense if you make a new package using the beta and name it Equatable
Then you end up with
Equatable: the package that you import which is named Equatable EquatableMacros: the module that contains the implementation of your macros (the types) EquatableMacro: The type that implements your macro.
It is a distraction that stringify has to be 'cleaned up' whenever we start a macro but it is easier to fight confusion once you see those three names and their meanings.
Sorry @griotspeak and thank you very much for your help but that doesn't compile in my case. It appears that you built this on top of my first implementation and I get External macro implementation type 'EquatableMacro.EquatableMacro' could not be found for macro 'Equatable'; the type must be public and provided via '-load-plugin-library' on main.swift because it can't find the Macro.
@attached(conformance)
public macro Equatable() = #externalMacro(
module: "EquatableMacrosMacros", // changed from EquatableMacro
type: "EquatableMacro"
)
Changing to this , the only difference to your gist is that you added the Error type to the Macro.
Now the code compiles AND main.swift file works fine (prints true) but the tests do not call the expansion macro.